20.2 线程的协作机制

20.2 线程的协作机制

多线程之间的核心问题,除了竞争,就是协作。我们在15.3节介绍了多种协作场景,比如生产者/消费者协作模式、主从协作模式、同时开始、集合点等。之前章节探讨了协作的多种机制:

  • wait/notify;
  • 显式条件;
  • 线程的中断;
  • 协作工具类;
  • 阻塞队列;
  • Future/FutureTask。

(1)wait/notify

wait/notify与synchronized配合一起使用,是线程的基本协作机制。每个对象都有一把锁和两个等待队列,一个是锁等待队列,放的是等待获取锁的线程;另一个是条件等待队列,放的是等待条件的线程,wait将自己加入条件等待队列,notify从条件等待队列上移除一个线程并唤醒,notifyAll移除所有线程并唤醒。

需要注意的是,wait/notify方法只能在synchronized代码块内被调用,调用wait时,线程会释放对象锁,被notify/notifyAll唤醒后,要重新竞争对象锁,获取到锁后才会从wait调用中返回,返回后,不代表其等待的条件就一定成立了,需要重新检查其等待的条件。

wait/notify方法看上去很简单,但往往难以理解wait等的到底是什么,而notify通知的又是什么,只能有一个条件等待队列,这也是wait/notify机制的局限性,这使得对于等待条件的分析变得复杂,15.3节通过多个例子演示了其用法,这里就不赘述了。

(2)显式条件

显式条件与显式锁配合使用,与wait/notify相比,可以支持多个条件队列,代码更为易读,效率更高。使用时注意不要将signal/signalAll误写为notify/notifyAll。

(3)线程的中断

Java中取消/关闭一个线程的方式是中断。中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,但是由线程来决定如何以及何时退出,线程在不同状态和IO操作时对中断有不同的反应。作为线程的实现者,应该提供明确的取消/关闭方法,并用文档清楚描述其行为;作为线程的调用者,应该使用其取消/关闭方法,而不是贸然调用interrupt。

(4)协作工具类

除了基本的显式锁和条件,针对常见的协作场景,Java并发包提供了多个用于协作的工具类。

信号量类Semaphore用于限制对资源的并发访问数。

倒计时门栓CountDownLatch主要用于不同角色线程间的同步,比如在裁判/运动员模式中,裁判线程让多个运动员线程同时开始,也可以用于协调主从线程,让主线程等待多个从线程的结果。

循环栅栏CyclicBarrier用于同一角色线程间的协调一致,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后再一起通过,它是循环的,可以用作重复的同步。

(5)阻塞队列

对于最常见的生产者/消费者协作模式,可以使用阻塞队列,阻塞队列封装了锁和条件,生产者线程和消费者线程只需要调用队列的入队/出队方法就可以了,不需要考虑同步和协作问题。

阻塞队列有普通的先进先出队列,包括基于数组的ArrayBlockingQueue和基于链表的LinkedBlockingQueue/LinkedBlockingDeque,也有基于堆的优先级阻塞队列PriorityBlock-ingQueue,还有可用于定时任务的延时阻塞队列DelayQueue,以及用于特殊场景的阻塞队列SynchronousQueue和LinkedTransferQueue。

(6)Future/FutureTask

在常见的主从协作模式中,主线程往往是让子线程异步执行一项任务,获取其结果。手工创建子线程的写法往往比较麻烦,常见的模式是使用异步任务执行服务,不再手工创建线程,而只是提交任务,提交后马上得到一个结果,但这个结果不是最终结果,而是一个Future。Future是一个接口,主要实现类是FutureTask。

Future封装了主线程和执行线程关于执行状态和结果的同步,对于主线程而言,它只需要通过Future就可以查询异步任务的状态、获取最终结果、取消任务等,不需要再考虑同步和协作问题。