wait nofity

一、原理

分析

  • Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态
  • BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片
  • BLOCKED 线程会在 Owner 线程释放锁时唤醒
  • WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入EntryList 重新竞争

二、API介绍

方法 说明
obj.wait() wait方法让进入object监视器的线程到waitSet等待。wait后会释放对象锁,让其他线程竞争。
obj.wait(Long timeout) 限时等待。导致当前的线程等待,等待被其他线程唤醒,或者指定的时间timeout用完,线程不再等待。
obj.notify() 在 object 上正在waitSet 等待的线程中挑一个唤醒。
obj.notifyAll() 让 object 上正在 waitSet 等待的线程全部唤醒。

它们都是线程之间进行协作的手段,都属于 Object 对象的方法。必须获得此对象的锁,才能调用这几个方法。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
@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(2);
log.debug("唤醒 obj 上其它线程");
synchronized (obj) {
obj.notify(); // 唤醒obj上一个线程
// obj.notifyAll(); // 唤醒obj上所有等待线程
}
}
}
  • notify运行结果
1
2
3
4
22:36:43.159 c.TestWaitNotify [t1] - 执行...
22:36:43.161 c.TestWaitNotify [t2] - 执行...
22:36:45.171 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
22:36:45.171 c.TestWaitNotify [t1] - 其它代码...
  • notifyAll运行结果
1
2
3
4
5
22:39:49.383 c.TestWaitNotify [t1] - 执行...
22:39:49.385 c.TestWaitNotify [t2] - 执行...
22:39:51.389 c.TestWaitNotify [main] - 唤醒 obj 上其它线程
22:39:51.389 c.TestWaitNotify [t2] - 其它代码...
22:39:51.389 c.TestWaitNotify [t1] - 其它代码...

wait() 方法会释放对象的锁,进入 WaitSet 等待区,从而让其他线程就机会获取对象的锁。无限制等待,直到 notify 为止。

wait(long n) 有时限的等待, 到 n 毫秒后结束等待,或是被 notify。

三、wait notify的正确姿势

3.1 sleep(long n) 和 wait(long n) 的区别

  • sleep 是 Thread 的方法,而 wait 是 Object 的方法
  • sleep 不需要强制和 synchronized 配合使用,但 wait 需要和 synchronized 一起用
  • sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
  • 无时限wait方法执行后线程变为WAITING状态,有时限的wait方法与sleep方法执行后变为TIMED_WAITING状态

3.2 演变步骤

step 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@Slf4j(topic = "c.TestCorrectPostureStep1")
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("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]",hasCigarette);
if (hasCigarette){
log.debug("可以开始干活了");
}
}
},"Leefs").start();

for (int i = 0; i < 5; i++){
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
},"其他人").start();
}

sleep(1);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了!");
}
},"送烟的").start();
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
22:50:16.626 c.TestCorrectPostureStep1 [Leefs] - 有烟没?[false]
22:50:16.628 c.TestCorrectPostureStep1 [Leefs] - 没烟,先歇会!
---------- 中间间隔2s -------
22:50:18.638 c.TestCorrectPostureStep1 [Leefs] - 有烟没?[false]
22:50:18.638 c.TestCorrectPostureStep1 [送烟的] - 烟到了!
22:50:18.638 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了
22:50:18.638 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了
22:50:18.638 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了
22:50:18.639 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了
22:50:18.639 c.TestCorrectPostureStep1 [其他人] - 可以开始干活了

分析

  • 通过sleep方法来使线程处于等待状态;
  • 【Leefs线程】必须睡足2s后才能醒来,就算烟提前送到,也无法立刻醒来;
  • 如果送烟的线程也加了 synchronized (room) 后,就好比【Leefs】在里面反锁了门睡觉,烟根本没法送进门,main 没加synchronized 就好像 main 线程是翻窗户进来的;
  • 当【Leefs线程】获得锁在睡眠时,其它干活的线程,都要一直阻塞,效率太低;
  • 解决方法,使用 wait - notify 机制。
step 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
@Slf4j(topic = "c.TestCorrectPostureStep2")
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("可以开始干活了");
}
}
},"Leefs").start();

for (int i = 0; i < 5; i++){
new Thread(() -> {
synchronized (room){
log.debug("可以开始干活了");
}
},"其他人").start();
}

sleep(1);
new Thread(() -> {
synchronized (room){
hasCigarette = true;
log.debug("烟到了!");
room.notify();
}
},"送烟的").start();
}
}

运行结果

1
2
3
4
5
6
7
8
9
10
23:05:18.333 c.TestCorrectPostureStep2 [Leefs] - 有烟没?[false]
23:05:18.336 c.TestCorrectPostureStep2 [Leefs] - 没烟,先歇会!
23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了
23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了
23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了
23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了
23:05:18.336 c.TestCorrectPostureStep2 [其他人] - 可以开始干活了
23:05:19.342 c.TestCorrectPostureStep2 [送烟的] - 烟到了!
23:05:19.343 c.TestCorrectPostureStep2 [Leefs] - 有烟没?[true]
23:05:19.343 c.TestCorrectPostureStep2 [Leefs] - 可以开始干活了
  • 解决了当【Leefs线程】处于睡眠状态时,其他线程可以获取锁不需等待释放锁的问题
  • 但如果有其它线程也在等待条件呢?送烟的主线程notify会不会错误地叫醒其他线程呢?
step 3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Slf4j(topic = "c.TestCorrectPostureStep3")
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("没干成活...");
}
}
},"Leefs").start();

sleep(1);
new Thread(() -> {
synchronized (room){
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("没干成活...");
}
}
},"Jeyoo").start();

sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖送到了!");
room.notify();
}
},"送外卖的").start();
}
}

运行结果

1
2
3
4
5
6
7
23:12:26.760 c.TestCorrectPostureStep3 [Leefs] - 有烟没?[false]
23:12:26.762 c.TestCorrectPostureStep3 [Leefs] - 没烟,先歇会!
23:12:27.766 c.TestCorrectPostureStep3 [Jeyoo] - 外卖送到没?[false]
23:12:27.766 c.TestCorrectPostureStep3 [Jeyoo] - 没外卖,先歇会!
23:12:28.771 c.TestCorrectPostureStep3 [送外卖的] - 外卖送到了!
23:12:28.772 c.TestCorrectPostureStep3 [Leefs] - 有烟没?[false]
23:12:28.773 c.TestCorrectPostureStep3 [Leefs] - 没干成活...
  • notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】
  • 解决方法,改为 notifyAll
step 4
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Slf4j(topic = "c.TestCorrectPostureStep4")
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("没干成活...");
}
}
},"Leefs").start();

sleep(1);
new Thread(() -> {
synchronized (room){
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("没干成活...");
}
}
},"Jeyoo").start();

sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖送到了!");
room.notifyAll();
}
},"送外卖的").start();
}
}

运行结果

1
2
3
4
5
6
7
8
10:42:37.796 c.TestCorrectPostureStep5 [Leefs] - 有烟没?[false]
10:42:37.798 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会!
10:42:38.801 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[false]
10:42:38.801 c.TestCorrectPostureStep5 [Jeyoo] - 没外卖,先歇会!
10:42:39.809 c.TestCorrectPostureStep5 [送外卖的] - 外卖送到了!
10:42:39.810 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[true]
10:42:39.810 c.TestCorrectPostureStep5 [Jeyoo] - 可以开始干活了
10:42:39.810 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会!
  • 用 notifyAll 仅解决某个线程的唤醒问题,但使用 if + wait 判断仅有一次机会,一旦条件不成立,就没有重新判断的机会了
  • 解决方法,用 while + wait,当条件不成立,再次 wait
step 5

将 if 改为 while

1
2
3
4
5
6
7
8
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

改动后

  • wait方法被唤醒后才会执行后面代码,因此此处不会导致while循环空转
1
2
3
4
5
6
7
8
while (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

运行结果

1
2
3
4
5
6
7
8
23:20:07.016 c.TestCorrectPostureStep5 [Leefs] - 有烟没?[false]
23:20:07.019 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会!
23:20:08.030 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[false]
23:20:08.031 c.TestCorrectPostureStep5 [Jeyoo] - 没外卖,先歇会!
23:20:09.033 c.TestCorrectPostureStep5 [送外卖的] - 外卖送到了!
23:20:09.034 c.TestCorrectPostureStep5 [Jeyoo] - 外卖送到没?[true]
23:20:09.034 c.TestCorrectPostureStep5 [Jeyoo] - 可以开始干活了
23:20:09.034 c.TestCorrectPostureStep5 [Leefs] - 没烟,先歇会!

总结:正确姿势

1
2
3
4
5
6
7
8
9
10
11
synchronized(lock) {
while(条件不成立) {
lock.wait();
}
// 干活
}

//另一个线程
synchronized(lock) {
lock.notifyAll();
}

wait nofity
http://example.com/2023/03/27/wait-nofity/
作者
程序员小魏
发布于
2023年3月27日
许可协议