【多线程:wait/notify详解】原理及错误用法(虚假唤醒等)
01.介绍
我们之前学习的过程中浅显的了解过wiat/notify,但是没有系统的介绍过wait/notify,wait是使线程陷入等待 notify是随机唤醒一个被wait的线程。
02.工作原理
当一个线程获取锁后 但是发现自己不满足某些条件 不能执行锁住部分的代码块时 需要进入等待列表 直到满足条件时才会重新竞争线程
上图为它的工作原理
注意
1.Owner发现条件某个线程不满足条件,调用wait方法,此时这个线程进入WaitSet,并且这个线程的状态变为WAITING状态2.BLOCKED和WAITING状态的线程都不参与cpu调度,不占用cpu时间片
3.WAITING线程会在Owner线程调用notify或notifyAll时唤醒,但唤醒后仍然要进入EntryList重新竞争锁
03.API介绍
obj.wait():wait方法让进入object监视器的线程到waitSet等待。wait后会释放对象锁,让其他线程竞争
obj.wait(Long timeout):wait的有时限方法,如果在时限内没有其他线程唤醒,则自己直接唤醒自己,若期间有别的线程唤醒那就正常唤醒。wait后会释放对象锁,让其他线程竞争
obj.notify():notify方法让正在waitSet等待的线程挑一个唤醒
obj.notifyAll():notifyAll方法让正在waitSet等待的线程全部唤醒
它们都是线程之间进行协作的手段,都属于Object对象方法,必须获取此对象的锁,才能调用这几个方法,如果不加锁直接调用这些方法会报错
notify与notifyAll的对比
@Slf4j(topic = "c.TestWaitNotify")
public class TestWaitNotify {
final static Object obj = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t1").start();
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 让线程在obj上一直等待下去
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码....");
}
},"t2").start();
// 主线程两秒后执行
sleep(0.5);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
// obj.notify(); // 唤醒obj上一个线程
obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
结果
调用notify时:23:11:55.798 c.TestWaitNotify [t1] - 执行....
23:11:55.801 c.TestWaitNotify [t2] - 执行....
23:11:56.300 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
23:11:56.300 c.TestWaitNotify [t1] - 其它代码....调用notifyAll时:
23:12:26.195 c.TestWaitNotify [t1] - 执行....
23:12:26.198 c.TestWaitNotify [t2] - 执行....
23:12:26.699 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
23:12:26.699 c.TestWaitNotify [t2] - 其它代码....
23:12:26.699 c.TestWaitNotify [t1] - 其它代码....
解释
可以看出notify是随机唤醒一个线程,notifyAll则是唤醒全部线程
04.wait与sleep方法的区别
区别
1.sleep是Thread的类方法,而wait是Object的对象方法2.sleep不需要强制和synchronized配合使用,但是wait需要和synchronized一起用
3.sleep在睡眠的同时,不会释放对象锁,但wait在等待时会释放对象锁
4.无时限wait方法执行后 线程变为WAITING状态,有时限的wait方法与sleep方法执行后变为TIMED_WAITING状态
分析下面代码
@Slf4j(topic = "c.Test19")
public class Test19 {
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lock) {
log.debug("获得锁");
try {
// Thread.sleep(2000);
lock.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}, "t1").start();
Sleeper.sleep(1);
synchronized (lock) {
log.debug("获得锁");
}
}
}
结果
当调用sleep时的情况:23:20:48.788 c.Test19 [t1] - 获得锁
当调用wait时的情况:
23:21:27.759 c.Test19 [t1] - 获得锁
23:21:28.768 c.Test19 [main] - 获得锁
解释
上述结果说明sleep在暂停期间 不会释放锁 导致 这期间其他线程不能运行,而wait则可以释放锁
05.wait/notify的正确使用情况
既然是正确的使用情况,那就需要一步一步来,把不正确的部分逐渐优化。
例子说明
现在有一群人需要干活,其中一个人叫做小南 他必须吸烟时才能干活。现在就是针对这个问题进行模拟。
模拟一
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep1 {
static final Object room = new Object();
static boolean hasCigarette = false; // 有没有烟
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
// 这里能不能加 synchronized (room)?
synchronized (room) { // 33行
hasCigarette = true;
log.debug("烟到了噢!");
}
}, "送烟的").start();
}
}
结果
33行加了synchronized:23:2516.146 c.TestCorrectPosture [小南] - 有烟没?[false]
23:25:16.149 c.TestCorrectPosture [小南] - 没烟,先歇会!
23:25:18.157 c.TestCorrectPosture [小南] - 有烟没?[false]
23:25:18.157 c.TestCorrectPosture [送烟的] - 烟到了噢!
23:25:18.157 c.TestCorrectPosture [其它人] - 可以开始干活了
23:25:18.157 c.TestCorrectPosture [其它人] - 可以开始干活了
23:25:18.158 c.TestCorrectPosture [其它人] - 可以开始干活了
23:25:18.158 c.TestCorrectPosture [其它人] - 可以开始干活了
23:25:18.158 c.TestCorrectPosture [其它人] - 可以开始干活了33行不加synchronized:
23:26:19.464 c.TestCorrectPosture [小南] - 有烟没?[false]
23:26:19.468 c.TestCorrectPosture [小南] - 没烟,先歇会!
23:26:20.475 c.TestCorrectPosture [送烟的] - 烟到了噢!
23:26:21.470 c.TestCorrectPosture [小南] - 有烟没?[true]
23:26:21.470 c.TestCorrectPosture [小南] - 可以开始干活了
23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
23:26:21.471 c.TestCorrectPosture [其它人] - 可以开始干活了
解释
我们来分析这个模拟的缺陷:
33行不加synchronized:当我们不加synchronized时我们发现的问题是 由于小南调用了sleep 睡眠2s期间没有释放锁 所以此时其他线程加锁的代码块不能运行,导致其他人没有办法工作
33行加synchronized:
当加synchronize的时上述问题依旧没有解决,且出现一个新的问题,小南在sleep 2s期间 主线程的第33行因为加了synchronized导致hasCigarette并没有改变为true,所以此时小南在1s后没有收到烟 所以小南没有工作
模拟二
可以看出模拟一的主要问题是 小南不干活 其他人也要等,而且小南有可能会由于送烟代码块被加锁 导致收不到烟不干活
我们用wait/notify来优化
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep2 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify();
}
}, "送烟的").start();
}
}
结果
23:30:41.080 c.TestCorrectPosture [小南] - 有烟没?[false]
23:30:41.083 c.TestCorrectPosture [小南] - 没烟,先歇会!
23:30:41.084 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.084 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.084 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.085 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:41.085 c.TestCorrectPosture [其它人] - 可以开始干活了
23:30:42.085 c.TestCorrectPosture [送烟的] - 烟到了噢!
23:30:42.085 c.TestCorrectPosture [小南] - 有烟没?[true]
23:30:42.086 c.TestCorrectPosture [小南] - 可以开始干活了
看起来好像没有问题了,但是如果此时还有另外一个需要条件的才能工作的线程呢?
模拟三
我们在之前的题目上再加一个 人物 小女,小女需要外卖才能工作,此时我们再来用模拟二的方法进行模拟
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep3 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
// 虚假唤醒
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify();
}
}, "送外卖的").start();
}
}
结果
23:45:35.476 c.TestCorrectPosture [小南] - 有烟没?[false]
23:45:35.486 c.TestCorrectPosture [小南] - 没烟,先歇会!
23:45:35.486 c.TestCorrectPosture [小女] - 外卖送到没?[false]
23:45:35.486 c.TestCorrectPosture [小女] - 没外卖,先歇会!
23:45:36.483 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
23:45:36.483 c.TestCorrectPosture [小南] - 有烟没?[false]
23:45:36.483 c.TestCorrectPosture [小南] - 没干成活...
解释
我们发现在新增一个小女 之后 这个代码又出现了问题,刚开始 小南 小女条件都不满足不能工作,但是因为notify是唤醒某一个线程,导致 本来应该唤醒小女的线程 把小南唤醒了 但是没有给小南需要的条件,而且小女也因此没有机会活动外卖,导致小女与小女都没有干活
这种把不该唤醒的线程唤醒的情况叫做虚假唤醒
模拟四
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep4 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
结果
00:00:01.274 c.TestCorrectPosture [小南] - 有烟没?[false]
00:00:01.274 c.TestCorrectPosture [小南] - 没烟,先歇会!
00:00:01.274 c.TestCorrectPosture [小女] - 外卖送到没?[false]
00:00:01.274 c.TestCorrectPosture [小女] - 没外卖,先歇会!
00:00:02.284 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
00:00:02.284 c.TestCorrectPosture [小女] - 外卖送到没?[true]
00:00:02.284 c.TestCorrectPosture [小女] - 可以开始干活了
00:00:02.284 c.TestCorrectPosture [小南] - 有烟没?[false]
00:00:02.284 c.TestCorrectPosture [小南] - 没干成活...
解释
我们这次把notify换成了notifyAll,使得小女一定可以被唤醒 并且收到外卖。事实也是如此,小女获得了外卖 并且开始工作,但是小南依旧被唤醒了 并且没有收到烟 导致小南没有干活。
现在的情况是 虽然唤醒了应该唤醒的 小女线程,但是小南线程还是被错误唤醒了,依旧是虚假唤醒
模拟五
@Slf4j(topic = "c.TestCorrectPosture")
public class TestCorrectPostureStep5 {
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
public static void main(String[] args) {
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
new Thread(() -> {
synchronized (room) {
Thread thread = Thread.currentThread();
log.debug("外卖送到没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
}
}
结果
00:10:36.991 c.TestCorrectPosture [小南] - 有烟没?[false]
00:10:36.991 c.TestCorrectPosture [小南] - 没烟,先歇会!
00:10:36.991 c.TestCorrectPosture [小女] - 外卖送到没?[false]
00:10:36.991 c.TestCorrectPosture [小女] - 没外卖,先歇会!
00:10:37.990 c.TestCorrectPosture [送外卖的] - 外卖到了噢!
00:10:37.990 c.TestCorrectPosture [小女] - 外卖送到没?[true]
00:10:37.990 c.TestCorrectPosture [小女] - 可以开始干活了
00:10:37.990 c.TestCorrectPosture [小南] - 没烟,先歇会!
解释
这里我们用了一个很巧妙的处理 解决了模拟四种小南被虚假唤醒的情况,我们这里把小南的if判断改为while,使得如果判断失败 会再次循环 执行wait 进入WaitSet
06.wait/notify建议使用的格式
synchronized(lock){
while(条件不成立){
lock.wait();
}
// 后续代码
}
// 另一个线程
synchronized(lock){
lock.notifyAll();
}
这样的写法避免了 虚假唤醒的情况 也保证了 唤醒的线程一定可以获得需要的条件 从而进行工作。