本文是异步编程学习之路(三)-多线程之间的协作与通信,若要关注前文,请点击传送门:
异步编程学习之路(二)-通过Synchronize实现线程安全的多线程
通过前文,我们学习到如何实现同步的多线程,但是在很多情况下,仅仅同步是不够的,还需要线程与线程协作(通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
假设有这样一种情况,有一个西餐厅,厨师负责制作牛排并放到顾客的盘子里,顾客负责享用盘子里的牛排,A线程可以看做厨师,B线程可以看做是顾客,如果盘子里面没有牛排,则B线程进入阻塞队列,此时唤醒A线程制作牛排,如果盘子里有牛排,则A线程进入阻塞队列,此时唤醒B线程享用牛排,如此循环有序的进行。代码如下:
/**
* @Description:多线程之间的协作与通信
* @Author:zhangzhixiang
* @CreateDate:2018/12/21 12:53:36
* @Version:1.0
*/
public class Plate {
private List<Object> foods = new ArrayList<>();
public synchronized void enjoy() {
while (foods.size() == 0) {
try {
wait();
} catch(InterruptedException e) {
e.printStackTrace();
}
}
Object food = foods.get(0);
foods.clear();
notify();//唤醒阻塞队列线程到就绪队列
System.out.println(String.format("顾客正在享受%s,好吃点赞。", food));
}
public synchronized void cooking() {
while (foods.size() > 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
Object food = "牛排";
foods.add(food);
notify();//唤醒阻塞队列线程到就绪队列
System.out.println(String.format("厨师制作%s,并放到顾客的盘子里。", food));
}
public static void main(String[] args) {
Plate plate = new Plate();
for (int i = 0; i < 10; i++) {
new Thread(() -> plate.cooking()).start();
new Thread(() -> plate.enjoy()).start();
}
}
}
运行结果:
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
以上代码有几点需要注意:
1、运行结果中只打印了8次,并且程序还处于运行状态?很明显这里出现了死锁,A和B两个线程都进入了休眠状态,等待对方唤醒自己。
这里的死锁有三种解决方案:
(1)给wait()设置最大等待时间(ms),超过最大等待时间后对应线程会自动消亡。
wait(10);//最大等待时间10ms
(2)将notify()改为notifyAll(),notify()和notifyAll的区别,前一个是唤醒阻塞队列中的任意线程,后一个是唤醒就绪阻塞队列中的全部线程。(synchronize中有两个队列,一个是阻塞队列,一个是就绪队列)。
notifyAll();//唤醒阻塞队列线程到就绪队列
(3)通过Jdk中JUC包下的ReentrantLock和Condition来替换这里的synchronize、wait、notify。(ReentrantLock和Condition在后文中会进行详细讲解)
2、Jdk5中notify()唤醒的是任意一个线程,这就说明有可能唤醒的是厨师线程,也有可能唤醒的是顾客线程,这就造成了一个唤醒的不明确性,你不知道唤醒的是什么线程。在JDK7中给出了解决方案,Jdk7中可以根据业务通过Condition建立单独的阻塞队列,就比如这里的厨师放到厨师的阻塞队列中,顾客放到顾客的阻塞队列中,这样的话你也就知道唤醒的到底是什么类型的线程了。
本文中提到了Jdk7中JUC包下的Lock锁相关的方法,Lock相关介绍在后文中会进行详细讲解。本文中提到了wait、notify、notifyAll等多线程之间协作通信的相关方法,下面的文章中我们会更详细的介绍这些方法。