用一个例子来说明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()阻塞线程。
以此来达到同步等待获取异步执行结果的目的。