0%

java | wait && notify 正确姿势

下面的代码循序渐进,用来说明正确姿势。

使用变量进行开关传递

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
package com.redisc;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

class A {
};

class B {
};

@Slf4j(topic = "c.Run")
public class Run {

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws Exception {
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();
}

Thread.sleep(1000);

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

}

输出

1
2
3
4
5
6
7
8
9
10
00:28:48.506 [小南] DEBUG c.Run - 有烟没?[false]
00:28:48.511 [小南] DEBUG c.Run - 没烟,先歇会
00:28:49.507 [送烟的] DEBUG c.Run - 烟到了
00:28:50.511 [小南] DEBUG c.Run - 有烟没?[true]
00:28:50.511 [小南] DEBUG c.Run - 可以干活了
00:28:50.512 [其他人] DEBUG c.Run - 可以干活了
00:28:50.512 [其他人] DEBUG c.Run - 可以干活了
00:28:50.512 [其他人] DEBUG c.Run - 可以干活了
00:28:50.512 [其他人] DEBUG c.Run - 可以干活了
00:28:50.513 [其他人] DEBUG c.Run - 可以干活了

确实可以实现功能,但是,有以下的缺点

  • 小南休息 2 秒的时候,其他人干不了活
  • 小南必须睡足 2 秒才能醒来,即便是烟到了,也无法立刻醒来

wait && notify

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
59
package com.redisc;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

class A {
};

class B {
};

@Slf4j(topic = "c.Run")
public class Run {

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws Exception {
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("可以干活了");
}
}
}, "小南").start();

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

Thread.sleep(1000);

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

}

输出

1
2
3
4
5
6
7
8
9
10
00:31:14.501 [小南] DEBUG c.Run - 有烟没?[false]
00:31:14.506 [小南] DEBUG c.Run - 没烟,先歇会
00:31:14.506 [其他人] DEBUG c.Run - 可以干活了
00:31:14.506 [其他人] DEBUG c.Run - 可以干活了
00:31:14.506 [其他人] DEBUG c.Run - 可以干活了
00:31:14.507 [其他人] DEBUG c.Run - 可以干活了
00:31:14.507 [其他人] DEBUG c.Run - 可以干活了
00:31:15.505 [送烟的] DEBUG c.Run - 烟到了
00:31:15.505 [小南] DEBUG c.Run - 有烟没?[true]
00:31:15.505 [小南] DEBUG c.Run - 可以干活了

但是,这个有下面缺点

  • 就是叫醒线程是随机的,如果,有其他线程会导致叫醒错误线程,即虚假唤醒

所以,可以把

1
2
3
room.notify();
改成
room.notifyAll();

但是,依然会导致新的问题

虽然能全部唤醒进程,但是,有的进程可能没有满足条件,叫醒也没用。

把 if 换成 while 解决虚假唤醒

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
package com.redisc;

import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;

class A {
};

class B {
};

@Slf4j(topic = "c.Run")
public class Run {

static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;

public static void main(String[] args) throws Exception {
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("可以干活了");
}
}
}, "小南").start();

new Thread(() -> {
synchronized (room) {
log.debug("有外卖没?[{}]", hasTakeout);
while (!hasTakeout) {
log.debug("没外卖,先歇会");
try {
room.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有外卖没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以干活了");
}
}
}, "小女").start();

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

Thread.sleep(1000);

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

}

输出为

1
2
3
4
5
6
7
8
9
10
11
12
00:39:26.208 [小南] DEBUG c.Run - 有烟没?[false]
00:39:26.212 [小南] DEBUG c.Run - 没烟,先歇会
00:39:26.212 [其他人] DEBUG c.Run - 可以干活了
00:39:26.212 [其他人] DEBUG c.Run - 可以干活了
00:39:26.212 [其他人] DEBUG c.Run - 可以干活了
00:39:26.212 [其他人] DEBUG c.Run - 可以干活了
00:39:26.212 [其他人] DEBUG c.Run - 可以干活了
00:39:26.212 [小女] DEBUG c.Run - 有外卖没?[false]
00:39:26.212 [小女] DEBUG c.Run - 没外卖,先歇会
00:39:27.209 [送烟的] DEBUG c.Run - 烟到了
00:39:27.209 [小南] DEBUG c.Run - 有烟没?[true]
00:39:27.209 [小南] DEBUG c.Run - 可以干活了

总结

标准模版。

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

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

这里可能存在一个疑问点是

1
2
3
while(条件不成立){
lock.wait();
}

当条件不成立的时候,会一直在 while 的体内,不断的循环吗?

答案是,会。当条件不成立,会进入到 lock.wait() ,然后线程会进入到 waitset ,等再次被唤醒,如果条件不成立,还是会进入到循环。

可以运行下面的代码进行验证。

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
package com.redisc;

import lombok.extern.slf4j.Slf4j;

class Downloader {
public static String download() throws InterruptedException {
Thread.sleep(5000);
return "success";
}
};

@Slf4j(topic = "c.Run")
public class Run {

public static void main(String[] args) throws Exception {
// 线程1 等待 线程2 的下载结果
GuardedObject guardedObject = new GuardedObject();
new Thread(() -> {
log.debug("等待结果");
Object state = (String) guardedObject.get();
log.debug("下载结果[{}]", state);
}, "t1").start();

new Thread(() -> {
log.debug("开始下载");
try {
String state = Downloader.download();
log.debug("真实唤醒");
guardedObject.complete(state);
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t2").start();

new Thread(() -> {
log.debug("虚假唤醒");
try {
Thread.sleep(1000);
guardedObject.complete(null);
} catch (Exception e) {
e.printStackTrace();
}
}, "t3").start();
}

}

class GuardedObject {
private Object response;

// 获取结果
public Object get() {
synchronized (this) {
// 解决虚假唤醒
while (response == null) {
System.out.println("我被唤醒了");
try {
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return response;
}
}

// 产生结果
public void complete(Object response) {
synchronized (this) {
// 给结果成员变量赋值
this.response = response;
this.notifyAll();
}
}
}

输出

1
2
3
4
5
6
7
12:09:48.655 [t2] DEBUG c.Run - 开始下载
12:09:48.655 [t1] DEBUG c.Run - 等待结果
我被唤醒了
12:09:48.655 [t3] DEBUG c.Run - 虚假唤醒
我被唤醒了
12:09:53.660 [t2] DEBUG c.Run - 真实唤醒
12:09:53.660 [t1] DEBUG c.Run - 下载结果[success]
请我喝杯咖啡吧~