Java并发知识点总结(上)

1. 并发的多面性:

并发可以解决两个问题:
一是“速度”:
并发编程可以提高多处理器机器的处理速度;
它通常是提高运行在单处理器上的程序的性能,顺序执行中阻塞是其重要原因。

二是改进代码设计:
一些问题,如仿真,没有并发很难支持。

2. 线程基础:

线程是操作系统能够进行运算调度的最小单位,被包含在进程之中。一个进程可以有多个线程,进程使用不同的内存空间,而线程共享一片相同的内存空间(这里不是指栈内存,栈内存是用来存储本地数据的)。

2.1 Java中实现线程的方法:

方法一:Thread实现;

方法二:Runnbale实现:从Java不能多重继承的角度来看,Runnable要比Thread合适一些;

方法三:使用Executor:
不同类型的Executor:
CachedThreadPool:可缓存的线程池,如果线程池大小超过了处理任务的需要,会回收空闲(60s不执行任务)的线程,线程数量不会限制,大小依赖JVM;
SingleThreadExecutor:同时只有一个线程在工作,异常后创建一个新线程,所有的任务按顺序提交运行;
FixedThreadPool:指定一个最大线程数,每提交一个任务创建一个线程直到达到最大线程数,保持线程数不变;
ScheduledThreadPool:创建一个大小无限的线程池,支持定时周期性执行的任务;

2.2 有返回的的任务:Callable(还可以抛出异常)和Future;

通过Executor.submit(new SubCallable());
Future对象可以保存返回的结果:
get()获取返回的结果,该方法是阻塞的;
isDone():查看是否已经完成;

2.3 休眠:

sleep:可以抛出InterruptedException;

2.4 优先级(MAX_PRIORITY、NORM_PRIORITY、MIN_PRIORITY):

优先级的实现还是依赖操作系统和底层实现,但是调度器将倾向于优先权最高的线程先进行。但是优先权较低的还是可以得到执行的,而是执行的频率较低(优先权不会导致死锁)。

2.5  让步(yield函数):

这也是“建议”线程调度机制,具有同优先级的线程可以执行。

2.6 后台线程(Thread.setDaemon方法):

只要有非后台线程运行,后台线程就不会终止;
后台线程派生的子线程也是后台线程;
后台线程中finally子句不会执行;
线程不能将自己设置成后台线程,否则会抛出IllegalThreadStateException;

2.7 自管理Runnable:

与直接继承Thread相比,使用组合的方式更灵活和可扩展:
class SelfManagerRunnbale implements Runnable {
    private Thread thread = new Thread(this);

    public void run() {
        //...
    }
}

2.8 加入一个线程(join):

在一个线程中对另一个线程调用join(),原线程挂起(可以通过isAlive查看)执行直到join的线程运行结束。
在调用线程中调用join线程的interrupt()方法可以中断join线程。
在catch到InterruptedException后,将清除interrupt标志,调用isInterrupt返回false;


2.9 捕获异常(典型的捕获子线程中逃逸的RuntimeException):

如何在线程外捕获逃逸的异常(RunntimeException);
Java SE5后,可以用Executor解决这个问题——Thread.UncaughtExceptionHandler.uncaughtException();
使用Executor我们可以通过传入特定ThreadFactory的方式设置Thread的特性,这是一种很好的分离,好的设计。
另外,在一个线程中捕获子线程异常,可以通过设置默认异常处理器的方式:
Thread.setDefaultUncaughtExceptionHandler();默认处理器在子线程没有设置异常处理器时调用。


3. 共享受限资源:

超过一个线程处理临界数据,必须使用相关同步方法,每个有操作的方法都必须同步。
存在过个线程共享统一资源时,使用序列化访问共享资源方案,通常使用锁语句,这是一种互斥量。
使用并发时,将域设置成private,防止其他任务直接访问域,用synchronized关键字锁定;
JVM锁跟踪计数,同一任务每进入一次synchronized时计数加1,每出一次synchronized计数减一;


java中lock和synchronized临界区都是可重入的锁;


3.1 synchronized关键字:

Java中对象和类都有对应的锁,可以保证synchronized static在类范围控制并发。


3.2 使用显式的Lock对象:

使用显式的Lock对象可以获得更细粒度的控制力。
ReentrantLock类:与try-finally结合使用


try {
     lock.lock();
} finally {
     lock.unlock();
}


异常清理:
使用try-finally的好处是可以在发生异常事在finally做清理工作,单单使用synchronized不行,同时保证unlock不会过早发生;
尝试锁定(synchronized无法做到):


try {
     captured = lock.trylock(2, timeout);
} finally {
    if(caputred)
        lock.unlock();
}


实现锁耦合(链式列表遍历节节加锁);


3.3 原子性与易变性:

volatile:保证64位数据类型读写的原子性(防止字撕裂);可见性(数据存入主存,没有本地缓存);不能保证递增/减操作或者某个域受到其他域限制的情况下的线程安全;原子类:Atomic类,用于保证一些读写操作的原子性;

临界区(同步控制块):synchronized(){};

在其他对象上同步:synchronized(object),如果获取了synchronized块上的锁,那么该对象其他synchronized方法和临界区就不能调用;

线程本地存储:一种自动化机制,为使用相同变量的每个不同的线程都创建不同的存储。ThreadLocal<T>,通过get/set方法存取内容;



4. 终结任务:

4.1 正常情况下安全退出的一种自定义方法:使用volatile的布尔值变量作为canceled标志,退出任务;


4.2 在阻塞时终结:

线程状态:
新建(new):创建时短暂的时间处于状态,分配资源,初始化,有资格获取CPU时间;
就绪(Runnable):该状态内,只有CPU分配时间片就可以运行;
阻塞(Blocked):阻塞时不可能获得CPU时间,直到重新进入到就绪状态;
死亡(Dead):run返回或者中断,线程终止;

进入阻塞状态的原因:
1. sleep();
2. wait()挂起,thread.join()挂起;
3. 等待某个输入/输出完成;
4. 等待暂不可用的锁;

4.3 中断:

具体的方法:
(1)interrupt():对于运行状态下的线程调用该方法不会有效果,对于阻塞调用中断,如可中断抛出InterruptedExceptioin;
(2)Thread.interrupted():检查是否中断,并复位跳出run循环,可以不抛出异常,和volatile布尔变量作用类似;
(3)Futrue.cancel(true):传true表示如果对应任务在运行,则中断;
(4)executor.shutdownNow();所有线程一起中断;
(5)自己维护本地布尔变量用于跳出run()循环;

总结:(2)(5)是用来跳出run循环的,对于资源一定要结合try-finally来进行清理;
(1),(3),(4)是用来在阻塞时终止任务的,通过异常来终止,对于资源要在catch子句中进行清理。
interrupt(),Future.cancel(true),executor.shutdownNow()都是向线程发送interrupt()改变该线程的中断标志,如下情况会抛出异常;

中断的影响:
(1)线程阻塞时:sleep,等待IO,等待lock,thread.join()挂起当前线程;中断抛出异常;

(2)非阻塞时对中断状态敏感的方法:
Thread.interrupted()检查并复位,不抛出异常;
已中断时,调用Thread.sleep()如果中断标志产生,调用sleep会抛出InterruptedException;
Lock.lockInterruptibly(),如果已处于中断,调用该方法抛出InterruptedException;

Thread.isInterrupted(),不会复位;
InterruptedExceptioin:抛出后也会复位;

阻塞状态的中断:
(1)sleep产生的阻塞可以被中断;
(2)io、临界区产生的等待阻塞不可以被中断;
(3)对于IO,关闭阻塞的资源可以产生中断;
对于使用NIO,通过futrue.cancel(true)/shutdownNow()产生的中断抛出ClosedByInterruptException;通过资源的close方法关闭抛出AsynchronousCloseException;
(4)使用ReentranLock代替synchronized临界区,可以通过中断终止;

5. 线程间协作:

任务协作:互斥,挂起,唤醒;

5.1 基础知识:

wait(),notify()和notifyAll():
wait(时间参数和无参数两个版本):挂起,释放锁,相比sleep()并不会释放锁,因此sleep可以为Thread的类方法;
notify:唤醒一个挂起线程,但不能决定具体是哪一个线程;

notifyAll:唤醒对应锁上所有挂起的线程;


使用显示Lock和Condition条件对象:
JSE 1.5 的新工具,可以从lock对象中获得condition,调用condition对象上的await,signal,signalAll来代替wait,notify,notifyAll,更为安全;


注意:
(1)只能在同步控制块中调用它们;
(2)它们是基于锁的,锁是基于对象的,因此是对象的一部分;

例子和问题:
【1】经典的汽车上漆和抛光例子;这里使用notifyAll,因为有一些任务出于不同的原因在等待锁,必须要使用notifyAll;

【2】什么时候使用notify和notifyAll:
notify只有一个被唤醒的线程,你必须确定是合适的线程;
只要有多个任务在等待不同的条件就必须使用notifyAll;
大部分时候使用notifyAll更为安全;

【3】错失的信号(死锁发生的原因之一):
while(someCondition) {
     //这里someCondition可能被其他线程修改,造成了无限的wait
     synchronized(shareMonitor) {
          sharedMonitor.wait();
     }
}

5.2 消费者和生产者:

厨师和服务员这个例子非常经典,也可以看到wait和notifyAll需要比较细致的控制,较难使用:

服务员:

class Waitress implements Runnable {
    private final Restaurant restaurant;
    public Waitress(Restaurant restaurant) {
        this.restaurant = restaurant;
    }
    @Override
    public void run() {
        try {
            while(!Thread.interrupted()) {
                synchronized (this) {
                    while(restaurant.meal == null) {
                        wait();
                    }
                }
                //got a meal
                logger.debug("got meal " + restaurant.meal);
                synchronized (restaurant.cook) {
                    restaurant.meal = null;
                    restaurant.cook.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            logger.error(e);
        }
    }
}
厨师:

class Cook implements Runnable {
    private final Restaurant restaurant;
    public Cook(Restaurant restaurant) {
        this.restaurant = restaurant;
    }
    @Override
    public void run() {
        try {
            while(!Thread.interrupted()) {
                synchronized (this) {
                    while (restaurant.meal != null) {
                        wait();
                    }
                }
                //check count
                if(++restaurant.count == 10) {
                    logger.debug("out of food, stop");
                    restaurant.executorService.shutdownNow();
                }
                synchronized (restaurant.waitress) {
                    restaurant.meal = new Meal(restaurant.count);
                    logger.debug("cooking " + restaurant.meal);
                    TimeUnit.MILLISECONDS.sleep(500); //here, throw a InterruptedException when shutdownNow
                    restaurant.waitress.notifyAll();
                }
            }
        } catch (InterruptedException e) {
            logger.error(e);
        }
    }
}

注意在厨师的run(),restaurant.executorService.shutdownNow()后,厨师线程并没有立刻停止;依然可以执行到logger.debug("cooking " + restaurant.meal);直到调用sleep()抛出java.lang.InterruptedException: sleep interrupted;前面也提到了sleep是对中断敏感的方法;

5.3 生产者和消费者与队列:

使用wait/notify的方式较为原始且很难控制,JAVA提供了更高的抽象:同步队列(阻塞队列):


阻塞队列的种类:
ArrayBlockingQueue:固定大小,可以实现公平和非公平;
LinkedBlockingQueue:基于链表实现的一个阻塞队列,如不指定容量,默认容量为Integer.MAX_VALUE;
PriorityBlockingQueue:出队顺序和优先级有关,无界阻塞队列,不进行容量检查;
DelayQueue:基于PriorityQueue,只有元素指定的延时到了,才可以获取数据;


重要方法:
add:不能传入null,否则抛出NPE,队列满(有界队列)时,抛出异常IllegalStateException;成功返回true;
remove:队列为空时抛出异常,成功返回true;
offer:插入元素到队尾,成功返回true;
peek:返回队首元素,空时返回null;
poll:返回并移除队首元素,空时返回努力来;
实现原理:通过Lock,Condition实现,put,take方法的等待条件分别是while(count == length)和while(count==0);


PS:任务间管道I/O:
使用PipedWriter/Reader进行通信;
没有阻塞队列健壮;
可以被中断;


6. 死锁:

发生的条件:
(1)互斥条件;
(2)保持与请求条件;
(3)独占条件(不能剥夺);
(4)循环等待条件;

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值