1. 多线程的协作场景有哪些
- 生产者/消费者模式
- 主从协作模式
- 同时开始
- 集合点等待
2. 线程的协作机制有哪些
wait()/notify()
wait()/notify()
与synchronized
配合一起使用,是线程的基本协作机制- 每个对象都有一把锁和两个等待队列,一个是锁等待队列,里面放的是等待获取锁的线程;另一个是条件等待队列,里面放的是等待条件的线程:
wait()
的作用是将自己加入条件等待队列,notify()
的作用是从条件等待队列上移除一个线程并唤醒,notifyAll()
移除所有线程并唤醒 wait()/notify()
方法只能在synchronized
代码块内被调用。调用wait()
时,线程会释放对象锁,被notify()/notifyAll()
唤醒后,要重新竞争对象锁,获取到锁后才会从wait()
调用中返回,返回后,不代表其等待的条件就一定成立了,需要重新检查其等待的条件wait()/nofity()
方法看上去简单,但往往难以理解wait()
等的到底是什么,而notify()
通知的又是什么,只能有一个条件等待队列,这也是wait()/notify()
机制的局限性,这使得对于等待条件的分析变得复杂
显示条件
- 显示条件与显示锁配合使用,与
wait()/notify()
相比,可以支持多个条件队列,代码更为易读、效率更高 - 使用时注意不要将
signal()/signalAll()
误写为notify()/notifyAll()
- 显示条件与显示锁配合使用,与
线程的中断
- Java 中取消/关闭一个线程的方式是中断。中断并不是强迫终止一个线程,它是一种协作机制,是给线程传递一个取消信号,由线程来决定如何以及何时退出
- 线程在不同状态和 IO 操作时对中断有不同的反应。作为线程的实现者,应该提供明确的取消/关闭方法,并用文档清楚描述其行为;作为线程的调用者,应该使用其取消/关闭方法,而不应该贸然调用
interrupt()
方法
协作工具类
- 信号量类
Semaphore
用于限制对资源的并发访问数 - 倒计时门栓
CountDownLatch
主要用于不同角色线程间的同步。比如在裁判/运动员模式中,裁判线程让多个运动员线程同时开始,也可以用于协调主从线程,让主线程等待多个线程的结果 - 循环栅栏
CyclicBarrier
用于同一角色线程间的协调一致,所有线程在到达栅栏后都需要等待其他线程,等所有线程都到达后再一起通过。它是循环的,可以用作重复的同步
- 信号量类
阻塞队列
- 对于最常见的生产者/消费者模式,可以使用阻塞队列
- 阻塞队列封装了锁和条件,生产者线程和消费者线程只需要调用队列的入队/出队方法就可以了,不需要考虑同步和协作问题
- 阻塞队列有普通的先进先出队列,包括基于数组的
ArrayBlockingQueue
和基于链表的LinkedBlockingQueue/LinkedBlockingDeque
,也有基于堆的优先级阻塞队列PriorityBlockingQueue
,还有可用于定时任务的延时阻塞队列DelayQueue
,以及用于特殊场景的阻塞队列SynchronousQueue
和LinkedTransferQueue
Future/FutureTask
- 在常见的主从协作模式中,主线程往往是让子线程异步执行一项任务,获取其结果。手工创建子线程的写法往往比较麻烦,常见的模式是使用异步任务执行服务:不再手工创建线程,而只是提交任务,提交后马上得到一个结果,但这个结果不是最终结果,而是一个
Future
。Future
是一个接口,主要实现类是FutureTask
Future
封装了主线程和执行线程关于执行状态和结果的同步,对于主线程而言,它只需要通过Future
就可以查询异步任务的状态、获取最终结果、取消任务等,不需要再考虑同步和协作问题
- 在常见的主从协作模式中,主线程往往是让子线程异步执行一项任务,获取其结果。手工创建子线程的写法往往比较麻烦,常见的模式是使用异步任务执行服务:不再手工创建线程,而只是提交任务,提交后马上得到一个结果,但这个结果不是最终结果,而是一个