Java之Object對象中的wait()和notifyAll()用法

用一個例子來講明Object對象中的wait方法和notifyAll方法的使用。java

 

首先定義一個消息類,用於封裝數據,以供讀寫線程進行操做:服務器

 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() 方法一直阻塞等待結果(等待後面的寫線程寫入數據):app

 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塊中。dom

這裏的讀線程和寫線程使用的同一個對象是message,讀線程調用message.wait()方法進行阻塞,寫線程調用message.notifyAll()方法喚醒全部(由於調用message.wait()方法的可能會有對個線程,在本例中就有兩個讀線程調用了message.wait() 方法)讀線程的阻塞。異步

 

寫一個測試類,啓動兩個讀線程,從Message對象中讀取數據,再啓動一個寫線程,往Message對象中寫數據:ide

 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是同一個,只不過被兩個讀線程都接收到了而已。ui

 

拋出一個問題:Object對象的這個特性有什麼用呢?this

它比較適合用在同步等待異步處理結果的場景中。好比,在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()阻塞線程。

以此來達到同步等待獲取異步執行結果的目的。

 

 

參考文章:https://cloud.tencent.com/developer/article/1155102

相關文章
相關標籤/搜索