在前两篇的文章,我们逐步介绍了Java中的线程基本知识,常规操作,以及部分源码的实现
这一篇我们将介绍线程间是如何协作运行的?
其实这个问题说到底就是线程间是如何通信的问题,此处的通信并非是我们理解广义的通信(A和B两个人互通消息),而是指多个线程是如何通过某些方式能够协作完成一个或者多个任务,这也是我们多线程并发编程意义所在,可参考文章:# Java多线程第一篇--聊聊并发
当然,实现线程间协作的方法有很多,总结如下:
- join方法
- 经典的wait/notify范式
- 基于AQS同步等待队列,即Lock 的 Condition 的 await/signal(park/unpark)
- 倒计时器CountdownLatch(当然底层也是基于AQS)
- 典型的生产者/消费者组件-阻塞队列BlockingQueue
- Callable、Future、FutureTask组合拳
- ForkJoinPool(先拆后合)
- ... 当然还有其他很多,就看你怎么理解协作这一词和具体的业务场景了。本篇将着重介绍经典的wait/notify方法,join方法可以参考这篇。
wait/notify介绍和使用
这两个方法具体的实现是在顶级父类Object中定义
wait方法的作用是使当前执行代码的线程进入等待队列,并且在wait方法所在的代码行处停止执行,直到接到通知或被中断为止。在调用wait方法之前,线程必须获得该对象的对象锁,即wait方法只能在同步块中调用。在执行wait方法后,当前线程释放同步锁。在从wait方法返回前,线程与其他线程竞争重新获取到锁。
notify方法和wait一样也要在同步块中调用,即也要获取对象锁。如果调用notify方法时没有持有适当的锁,则会抛出IllegalMonitorStateException。notify用来通知那些可能在等待该对象锁的其他线程,如果有多个线程等待,则由线程规划器挑选一个呈wait状态的线程,对其发送notify唤醒,并使它等待获取该对象的对象锁。其中,在执行notify方法后,当前线程并不会马上释放对象锁,呈wait状态的线程并不能马上获取该对象锁,要等到notify方法的线程执行完并退出同步代码块,也就是释放对象锁后,wait状态的线程才可以获取到锁,然后再执行相应的操作。
下面展示个实例来看下具体怎么使用
public class WaitNotify {
// 临界资源flag,true 等待
static boolean flag = true;
static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
Thread waitThread = new Thread(new WaitTask());
waitThread.start();
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
System.out.println((i + 1) + "秒...");
}
Thread notifyThread = new Thread(new NotifyTask());
notifyThread.start();
}
static class WaitTask implements Runnable {
@Override
public void run() {
synchronized (lock) {
while (flag) {
try {
System.out.println(Thread.currentThread().getName() + "flag is true wait...");
lock.wait();
} catch (Exception e) {
// TODO: handle exception
}
}
System.out.println(Thread.currentThread().getName() + "flag is false restart working...");
}
}
}
static class NotifyTask implements Runnable {
@Override
public void run() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + "ready notify ");
flag = false;
lock.notifyAll();
System.out.println(Thread.currentThread().getName() + "end notify ");
}
}
}
}
效果如图所示:等待线程一直在等待临界资源变为false,5秒后,通知线程将flag变成false后,通知等待线程恢复,这时等待线程将继续工作
从实例可以看出,不管在使用wait还是notify的时候,都必须在一个同步块中,即使用了synchronized关键字。synchronized此处不做详细说明,后续会有介绍。其实在Object的wait方法的注释中,官方已经给出了如何使用wait的方法,见下图
由此我们引出wait/notify的使用经典范式
wait/notify经典范式
//等待方
//获取对象锁
//如果条件不满足,那么调用对象的wait方法,被通知后仍要检查条件
//条件满足,则执行相应操作
synchronized(object){
while(条件不满足){
object.wait();
}
//执行相应操作
}
//通知方
//获取对象锁
//改变条件
//通知所有等待在对象上的线程
synchronized(object){
改变条件
object.notifyAll();
}
wait/notify经典问题-生产者/消费者
从上面介绍wait/notify使用方法实例时,我们从中还可以引出一个经典问题,那就是生产者/消费者问题,waitTask其实就是一直在等待某个条件达成,就像是一个消费者,一直在等待着生产者notifyTask生产,一旦生产出产品,即达到了有产品这个条件后,立刻通知消费者waitTask进行消费。
其实道理很简单,下面展示一幅图,饭店上菜的过程
菜品传递台即为资源临界区,服务员(消费者),从传递台取菜(消费的过程),厨师做菜(生产者)将菜放到传递台;当传递台没有菜时,服务员可能就会在旁边等着上菜,一旦厨师菜做完了放到传递台,并且按了一个铃铛之类的通告器,唤醒服务员有菜要上,这时服务员就从等待状态出来取菜上菜(消费)。
当然这里面的服务员和厨师肯定是由于很多个的,但是临界资源只有一个地方,所以不管是厨师上菜,还是服务员取菜,他们都应该先打开门(同步器synchronized锁)(PS:我见过很多饭店传菜台好像都有这样一个门的,没有的话就当我硬加的吧),才能使得这个过程不至于很乱。
这是一个简单的例子,当然这个例子不能说明我们编写程序如此简单,比如刚才说的多个服务员和多个厨师,如果使用notify可能会出现假死的状态,原因是:假设当前多个生产者线程会调用 wait 方法阻塞等待,当其中的生产者线程获取到对象锁之后使用 notify 通知处于 WAITTING 状态的线程,如果唤醒的仍然是生产者线程,就会造成所有的生产者线程都处于等待状态。
解决办法:将 notify 方法替换成 notifyAll 方法,如果使用的是 lock 的话,就将 signal 方法替换成 signalAll 方法。
当然我们在我们的并发包(J.U.C)中,已经有很多的经典组件实现了生产者消费者的问题,比如阻塞队列BlockingQueue(自行查看源码便可知一二),如下截了源码ArrayBlockingQueue的部分源码的实现:
从上面这些图中你可能觉得很懵,为啥具体调用等待/通知的对象不一样,因为这里面他其实维护了两套等待队列,具体详细的介绍将在以后的篇幅中会说的。
当然本篇一开始说了很多的线程间协作的方法,本篇目前只是基于线程篇,所以只说了wait/notify的方式,后续有机会再来说说其他的方式,到时候可能就是举个例子或者简单的一句话会带过哈~
生产者/消费者的应用
这边就做个总结吧,这个经典模型真的是太多的地方应用了:解耦,异步执行,延迟队列,高并发,缓冲区等等太多了。至于这些关键词什么意思就不用讲了吧...
到此,聊聊线程篇结束