用一个例子来说明Object对象中的wait方法和notifyAll方法的使用。
首先定义一个消息类,用于封装数据,以供读写线程进行操作:
1 /**
2 * 消息
3 *
4 * @author syj
5 */
6 public class Message {
7
8 private String msg;
9
10 public String getMsg() {
11 return msg;
12 }
13
14 public void setMsg(String msg) {
15 this.msg = msg;
16 }
17
创建一个读线程,从Message对象中读取数据,如果没有数据,就使用 wait() 方法一直阻塞等待结果(等待后面的写线程写入数据):
1 /**
2 * 读线程
3 *
4 * @author syj
5 */
6 public class Reader implements Runnable {
7
8 private Message message;
9
10 public Reader(Message message) {
11 this.message = message;
12 }
13
14 @Override
15 public void run() {
16 synchronized (message) {
17 try {
18 // 务必加上该判断,否则可能会因某个读线程在写线程的 notifyAll() 之后执行,
19 // 这将导致该读线程永远无法被唤醒,程序会一直被阻塞
20 if (message.getMsg() == null) {
21 message.wait();// 等待被 message.notify() 或 message.notifyAll() 唤醒
22 }
23 } catch (InterruptedException e) {
24 e.printStackTrace();
25 }
26 // 读取 message 对象中的数据
27 System.out.println(Thread.currentThread().getName() + " - " + message.getMsg());
28 }
29 }
30
创建一个写线程,往Message对象中写数据,写入成功就调用 message.notifyAll() 方法来唤醒在 message.wait() 上阻塞的线程(上面的读线程将被唤醒,读线程解除阻塞继续执行):
1 import java.util.UUID;
2
3 /**
4 * 写线程
5 *
6 * @author syj
7 */
8 public class Writer implements Runnable {
9
10 private Message message;
11
12 public Writer(Message message) {
13 this.message = message;
14 }
15
16 @Override
17 public void run() {
18 synchronized (message) {
19 try {
20 Thread.sleep(1000L);// 模拟业务耗时
21 } catch (InterruptedException e) {
22 e.printStackTrace();
23 }
24 // 向 message 对象中写数据
25 message.setMsg(Thread.currentThread().getName() + ":" + UUID.randomUUID().toString().replace("-", ""));
26 message.notifyAll();// 唤醒所有 message.wait()
27 }
28 }
29
注意,读线程的等待和写线程的唤醒,必须调用同一个对象上的wait或notifyAll方法,并且对这两个方法的调用一定要放在synchronized块中。
这里的读线程和写线程使用的同一个对象是message,读线程调用message.wait()方法进行阻塞,写线程调用message.notifyAll()方法唤醒所有(因为调用message.wait()方法的可能会有对个线程,在本例中就有两个读线程调用了message.wait() 方法)读线程的阻塞。
写一个测试类,启动两个读线程,从Message对象中读取数据,再启动一个写线程,往Message对象中写数据:
1 /**
2 * 测试 Object 对象中的 wait()/notifyAll() 用法
3 *
4 * @author syj
5 */
6 public class LockApp {
7 public static void main(String[] args) {
8 Message message = new Message();
9 new Thread(new Reader(message), "R1").start();// 读线程 名称 R1
10 new Thread(new Reader(message), "R2").start();// 读线程 名称 R2
11 new Thread(new Writer(message), "W").start();// 写线程 名称 W
12 }
13
控制台打印结果:
R2 - W:4840dbd6b312489a9734414dd99a4bcb
R1 - W:4840dbd6b312489a9734414dd99a4bcb
其中R2代表第二个读线程,R2是这个读线程的名字。R1是第一个读线程,线程名叫R2。后面的uui就是模拟的异步执行结果了,W代表写线程的名字,表示数据是由写线程写入的。 由于我们只开启一个写线程,所有两条数据的uuid是同一个,只不过被两个读线程都接收到了而已。
抛出一个问题:Object对象的这个特性有什么用呢?
它比较适合用在同步等待异步处理结果的场景中。比如,在RPC框架中,Netty服务器通常返回结果是异步的,而Netty客户端想要拿到这个异步结果进行处理,该怎么做呢?
下面使用伪代码来模拟这个场景:
1 import java.util.UUID;
2 import java.util.concurrent.ConcurrentHashMap;
3
4 /**
5 * 使用 Object对象的 wait() 和 notifyAll() 实现同步等待异步结果
6 *
7 * @author syj
8 */
9 public class App {
10
11 // 用于存放异步结果, key是请求ID, value是异步结果
12 private static ConcurrentHashMap<String, String> resultMap = new ConcurrentHashMap<>();
13 private Object lock = new Object();
14
15 /**
16 * 写数据到 resultMap,写入成功唤醒所有在 lock 对象上等待的线程
17 *
18 * @param requestId
19 * @param message
20 */
21 public void set(String requestId, String message) {
22 resultMap.put(requestId, message);
23 synchronized (lock) {
24 lock.notifyAll();
25 }
26 }
27
28 /**
29 * 从 resultMap 中读数据,如果没有数据则等待
30 *
31 * @param requestId
32 * @return
33 */
34 public String get(String requestId) {
35 synchronized (lock) {
36 try {
37 if (resultMap.get(requestId) == null) {
38 lock.wait();
39 }
40 } catch (InterruptedException e) {
41 e.printStackTrace();
42 }
43 }
44 return resultMap.get(requestId);
45 }
46
47 /**
48 * 移除结果
49 *
50 * @param requestId
51 */
52 public void remove(String requestId) {
53 resultMap.remove(requestId);
54 }
55
56 /**
57 * 测试方法
58 *
59 * @param args
60 */
61 public static void main(String[] args) {
62 // 请求唯一标识
63 String requestId = UUID.randomUUID().toString();
64 App app = new App();
65 try {
66 // 模拟Netty服务端异步返回结果
67 new Thread(new Runnable() {
68 @Override
69 public void run() {
70 try {
71 Thread.sleep(2000L);// 模拟业务耗时
72 } catch (InterruptedException e) {
73 e.printStackTrace();
74 }
75 // 写入数据
76 app.set(requestId, UUID.randomUUID().toString().replace("-", ""));
77 }
78 }).start();
79
80 // 模拟Netty客户端同步等待读取Netty服务器端返回的结果
81 String message = app.get(requestId);
82 System.out.println(message);
83 } catch (Exception e) {
84 e.printStackTrace();
85 } finally {
86 // 结果不再使用,一定要移除,以防止内容溢出
87 app.remove(requestId);
88 }
89 }
90
这里定义了一个静态的ConcurrentHashMap容器,来存放Netty服务器返回的异步结果,key是请求的id,value就是异步执行结果。
调用set方法可以往容器中写入数据(写入请求ID和相对应的执行结果),调用get方法可以从容器读取数据(根据请求ID获取对应的执行结果)。
get方法中调用lock对象的wait方法进行阻塞等待结果,set方法往容器中写入结果之后,紧接着调用的是同一个lock对象的notifyAll方法来唤醒该lock对象上的所有wait()阻塞线程。
以此来达到同步等待获取异步执行结果的目的。