一文讲清JAVA的wait和notify,notifyAll
前言
很多同学不太能搞明白wait,notify和notifyAll的用法,实现原理,应用场景今天试着来从一个故事入手来讲一下这几个问题。
头牌‘赛铁花’
京城有一名仕张三和好友李四,今天要款待俩人多年未见好友王五。酒过三巡,王五提议:‘早听说八大胡同头牌赛铁花的名号,不知今日能否同她共饮一杯。’张三附和:“好啊,和她喝完酒咱们还能再去听个戏”,李四说我可以和你们一起去找赛铁花,但是之后我得去卖炊饼了。张王李三人遂来到赛金花的小院。
小院门口有一丫鬟给他们介绍了一下规矩,一:要见姐姐得我带你们去(synchronized);二:赛铁花姐姐一次只和一人喝酒,其他人都得等着(wait);三::等着的人等我通知(notify)。
王五最心急,一把来着小丫鬟说走,先带我去。等到了赛铁花的小花园了,发现赛铁花出门遛狗了。丫鬟说:“得,王五大爷,您就在这花园等着吧。(wait)”,然后丫鬟就回到了小院门口。张三李四也陆续抓着丫鬟,分别带到了小凉亭和小池塘边。这时候后门管家赵六拉着丫鬟给她说把赛铁花姐姐带回来了。于是丫鬟就用‘点兵点将点到谁就是谁的方式’通知了被她最后点到的张三。但是丫鬟并没有直接去找张三,而是先系了下鞋带,伸了伸懒腰,然后才带着张三从小凉亭出来去找赛铁花小姐姐喝酒并在酒局上服侍着他俩。等张三喝完酒走后,丫鬟去门口大喊了一嗓子“还有谁?”(notifyAll),王五抓到了小丫鬟让带着去找赛铁花。最后李四才被带去。
注意细节
从上面例子可以看出几个细节:
1:赛铁花才是张王李三人的真实目标。但是却必须由小丫鬟带着他们去才行
2:在和赛铁花喝完酒之前,张三和王五的听戏、李四的卖炊饼都干不成
3:听戏和卖炊饼其实都没有小丫鬟和赛铁花什么事儿
4:wait,notify,notifyAll都是小丫鬟告诉张王李三人的
5:赛铁花出去了他们也就喝不成酒了,只能先等着
6:赛铁花是看门管家赵六带回来的
7:赛铁花回来以后丫鬟用非常随意的方式就决定了要带谁先去见姐姐
8:去带人见姐姐前丫鬟先作了点其他事儿
9:张王李三人被重新通知可以去见姐姐的时候都是从半路继续往前走的而不是从小院门口重新走一下
10:丫鬟第二次通知人时候大喊了一下通知给了所有人,他们都去抢着抓丫鬟,但是只有王五抓到了丫鬟。
如果你坚持把这个故事看完了,并且根据细节又仔细看了下小故事,那么恭喜你,wait,notify,notifyAll这三个概念你已经完全了解他们的用法,实现原理,和应用场景了。剩下下的知识替换一下名词的事儿。接下来我们就通过替换名词来详解下本文主题。
白话wait/notify/notifyAll原理
首先我们把上面的故事换一种理解方式。张三李四王五都是是三个消费者线程,他们要先喝酒,然后去消费“赛铁花”这个共享的资源,完事儿以后张三王五去听戏,李四去卖炊饼。那他们执行到“消费赛铁花”这个环节的时候,发现得先用synchronized抓住丫鬟这个对象,结果抓住以后发现赛铁花不在,即要消费的内容还没有生产出来,那他们就都得等着,而等着的这个操作是丫鬟这个锁用wait命令分别通知对应线程的(谁抓住我,我通知谁)。同时应该想到,系统为丫鬟这个对象分配了一个wait的set。等待(不是阻塞)这个资源的线程都在这个waitingSet中。在wait过程中,张三李四王五想去听戏或者想去卖炊饼都是进行不下去的,因为他们这几个线程现在都处于WAITING状态。
直到管家赵六这个生产者线程将赛铁花带回来,才完成了生产内容,然后这时候管家赵六让丫鬟用notify或者notifyAll来通知哪些等待着的线程们。notify和notifyAll的区别是什么呢?小故事里面,丫鬟用notify的方式居然是点兵点将的方式抽中一个线程,通知他可以继续了,可见notify并不是先到先得或者谁出钱多就先通知谁(牵涉线程优先级的内容暂不展开讨论),而是很随机的方式。那这个张三线程要想继续执行消费赛铁花的内容,还是得先抓住丫鬟才行,如过这时候恰好管家赵六不知道用什么手段抓住了丫鬟,那张三就得继续等,如果张三线程恰好抓住了丫鬟,那就可以进行消费赛铁花的操作了。那notifyAll呢?大喊一声通知了所有人,也就是唤醒了所有等待线程,张三李四王五及管家赵六,谁抓住丫鬟谁就能执行。这里我们应该看出来了,notify和notifyAll的随机性都很高,notify操作更轻一点,因为只是唤醒了一个线程,notifyAll会唤醒所有,动静很大。如果你能很好的hold主所有线程,推荐用notify,如果不太行,可以用notifyAll。以免只用notify造成通知到一个已经不进行生产的生产者问题,导致大家都永远等待下去。
Wait/notify/notifyAll用法
public class WaitNotifyConsumption {
/**
* 丫鬟,用来锁定同步块
*/
private Object yaHuan = new Object();
/**
* 赛铁花,真正被生产和消费的对象
*/
private volatile Object saiTieHua = null;
/**
* 设置个初始排队人数以便程序退出
*/
private AtomicInteger waitingSize = new AtomicInteger(2);
private final static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
/**
* 张三消费线程,先喝酒,再去找赛铁花,然后去听戏
*/
private static class ZhangConsumptionThread implements Runnable {
private final Object lock;
private Object consumptionObj;
private AtomicInteger waitingSize;
public ZhangConsumptionThread(Object lock, Object consumptionObj, AtomicInteger waitingSize) {
this.lock = lock;
this.consumptionObj = consumptionObj;
this.waitingSize = waitingSize;
}
@Override
public void run() {
try {
print("我张三在和李四喝酒。");
/* 无其他意义,只是让看到张三和李四一起喝酒了的输出 */
TimeUnit.SECONDS.sleep(1);
synchronized (lock) { // 获取丫鬟这个锁
print("我张三锁定了丫鬟。");
if (consumptionObj == null) {
print("我张三在等赛铁花。。。");
lock.wait();
}
print("我张三见到赛铁花了!");
TimeUnit.SECONDS.sleep(3);
lock.notify();
print("我张三要听戏去喽~");
waitingSize.decrementAndGet(); // 等待队列数-1
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 李四消费线程,喝酒,见赛铁花,卖炊饼
*/
private static class LiConsumptionThread implements Runnable {
private final Object lock;
private Object consumptionObj;
private AtomicInteger waitingSize;
public LiConsumptionThread(Object lock, Object consumptionObj, AtomicInteger waitingSize) {
this.lock = lock;
this.consumptionObj = consumptionObj;
this.waitingSize = waitingSize;
}
@Override
public void run() {
try {
print("我李四在和张三喝酒。");
/* 无其他意义,只是让看到张三和李四一起喝酒了的输出 */
TimeUnit.SECONDS.sleep(1);
synchronized (lock) { // 获取丫鬟这个锁
print("我李四锁定了丫鬟。");
if (consumptionObj == null) {
print("我李四在等赛铁花。。。");
lock.wait();
}
print("我李四见到赛铁花了!");
TimeUnit.SECONDS.sleep(5);
lock.notify();
print("李四炊饼~~又香又脆");
waitingSize.decrementAndGet(); // 等待队列数-1
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
/**
* 管家赵六生产者线程,把赛铁花带回来
*/
private static class ZhaoProductionThread implements Runnable {
private final Object lock;
private Object consumptionObj;
private AtomicInteger waitingSize;
public ZhaoProductionThread(Object lock, Object consumptionObj, AtomicInteger waitingSize) {
this.lock = lock;
this.consumptionObj = consumptionObj;
this.waitingSize = waitingSize;
}
@Override
public void run() {
try {
/* 无其他意义,只是等张三李四先去锁定赛铁花 */
TimeUnit.SECONDS.sleep(2);
while (waitingSize.get() != 0) {
print("我管家赵六准备锁定丫鬟。");
synchronized (lock) {
print("我管家赵六锁定了丫鬟。");
if (consumptionObj != null) {
print("我管家赵六要去将赛铁花带回来1。");
lock.notifyAll();
TimeUnit.SECONDS.sleep(2);
print("赛铁花在屋呢。。。");
// lock.wait();
} else {
print("我管家赵六要去将赛铁花带回来2。");
consumptionObj = new Object();
lock.notify();
TimeUnit.SECONDS.sleep(5);
print("我赵六把赛铁花带回来了");
lock.wait();
}
print("我管家赵六的工作做完一轮了。" + waitingSize.get());
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
WaitNotifyConsumption waitNotify = new WaitNotifyConsumption();
Thread zhangSan = new Thread(new ZhangConsumptionThread(waitNotify.yaHuan, waitNotify.saiTieHua, waitNotify.waitingSize));
Thread liSi = new Thread(new LiConsumptionThread(waitNotify.yaHuan, waitNotify.saiTieHua, waitNotify.waitingSize));
Thread zhaoLiu = new Thread(new ZhaoProductionThread(waitNotify.yaHuan, waitNotify.saiTieHua, waitNotify.waitingSize));
zhangSan.start();
liSi.start();
zhaoLiu.start();
}
private static void print(String content) {
System.out.println(dateFormat.format(new Date()) + "\t" + content);
}
}
以上代码解释了小故事的过程,为了简洁去掉了王五的戏份。张三李四为两个消费者线程,赛铁花是被消费对象,丫鬟是要获取的锁,管家赵六是一个生产者线程。运行以上程序很大概率会输出下面的内容
2021-02-10 23:24:32 我张三在和李四喝酒。
2021-02-10 23:24:32 我李四在和张三喝酒。
2021-02-10 23:24:33 我张三锁定了丫鬟。
2021-02-10 23:24:33 我张三在等赛铁花。。。
2021-02-10 23:24:33 我李四锁定了丫鬟。
2021-02-10 23:24:33 我李四在等赛铁花。。。
2021-02-10 23:24:34 我管家赵六准备锁定丫鬟。
2021-02-10 23:24:34 我管家赵六锁定了丫鬟。
2021-02-10 23:24:34 我管家赵六要去将赛铁花带回来2。
2021-02-10 23:24:39 我赵六把赛铁花带回来了
2021-02-10 23:24:39 我张三见到赛铁花了!
2021-02-10 23:24:42 我张三要听戏去喽~
2021-02-10 23:24:42 我李四见到赛铁花了!
2021-02-10 23:24:47 李四炊饼~~又香又脆
2021-02-10 23:24:47 我管家赵六的工作做完一轮了。0
wait/notify/notifyAll实现原理及结论
对应上面提到的10个小细节我们重新表达一下
1,要对可能有同步问题的对象操作需要synchronized一个final对象作为锁
2,wait以后就该线程进入waiting状态,后续的内容不再执行
3,和共享对象无关的内容不要放到同步块中,否则会使系统运行效率降低
4,wait,notify,notifyAll都是对象的方法,这三个方法是Object类的方法
5,消费对象为null或者empty,由代码中的lock.wait来通知线程进入waiting状态,这时候该线程会进入由jvm维护的对该synchronized锁定对象的一个waitingSet中。
6,生产者线程负责生产,然后再有lock.notify或者lock.notifyAll,来唤醒在waitingSet中的线程,使其进入runnable状态
7,notify具体会唤醒哪个线程是随机的
8,当前线程在执行完lock.notify以后并不会立即释放锁,而是要等后续内容执行完以后或者后续内容有lock.wait才会释放锁,这时候被选中的线程才能重新尝试获取锁(注意看以上输出的时间,第9,10行)
9,当被唤醒的线程成功获取到锁以后,并不会从synchronized那里开始执行,而是从wait的地方开始执行。
10,notifyAll是唤醒所有线程,但不是被唤醒的线程一定能获取到锁,也不是立即还行,而是进入获取锁的阻塞队列。
同时,细心的朋友通过以上例子还可以看出来:sleep和wait都可以让线程暂停一会儿,但是wait会让当前线程释放锁,而sleep并不会。
wait/notify/notifyall的应用场景
1,生产消费者场景
这类场景要注意假死问题,即:生产者的notify唤醒的还是生产者,或者消费者notify唤醒的还是消费者。
2,future模式
future模式中getRealData时候数据并没有号,需要等通知。
纯手打,求赞!!!如果您对文章有疑问欢迎留言讨论。