Java 多线程 —— wait 与 notify
引言
認識一下 Object 類中的兩個和多線程有關的方法:wait 和 notify。
wait,當前線程進入 WAITING 狀態,釋放鎖資源。
notify,喚醒等待中的線程,不釋放鎖資源。
一、使用 wait-notify 實現一個監控程序
實現一個容器報警功能,兩個線程分別執行以下任務:
t1 為容器添加10個元素;
t2實時監控容器中元素的個數,當個數為5時,線程2給出提示并結束。
1.1 簡單的 while-true 版本
/*** 一個普通的容器*/ class Container {private volatile List<Object> values = new ArrayList<>();public void add(Object value) {values.add(value);}public Integer sise() {return values.size();} } public class T_Alert01 {public static void main(String[] args) {Container container = new Container();new Thread(() -> {while (true) {if (container.sise() == 5) {System.out.println("alert 5!");break;}}}, "T2").start();new Thread(() -> {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加:" + container.sise());}}, "T1").start();} }程序解析:監控線程 T2 通過 while-true 監控容器內的元素數量,但當 size == 5 時,還未來得及報警,T1 就又添加了多個元素;而且 while-true 會浪費很多CPU 資源。
顯然,這么做無法滿足我們的要求。
1.2 wait-notify 初版
public class T_Alert02 {public static void main(String[] args) throws InterruptedException {Container container = new Container();final Object lock = new Object();new Thread(() -> {synchronized (lock) {if (container.sise() != 5) {try {lock.wait();} catch (InterruptedException e) {}}System.out.println("alert 5!");}}, "T2").start();TimeUnit.SECONDS.sleep(1);new Thread(() -> {synchronized (lock) {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加:" + container.sise());if (container.sise() == 5) {lock.notify();}}}}, "T1").start();} }程序解析:T2監控線程先去判斷容器大小,如果未達到報警標準,則等待在 lock 對象上,WAITING 是一種掛起狀態,不消耗CPU資源。
T1 線程在達到 5 個時,觸發一個 notify方法,喚醒其他等待中的線程。然而,僅僅是 notify 還不足以做到實時喚醒 T2 報警,上述代碼無論執行多少次都是最后輸出報警信息,想想這是為什么?
?1.3 wait-notify 完整版
public class T_Alert03 {public static void main(String[] args) throws InterruptedException {Container container = new Container();final Object lock = new Object();new Thread(() -> {synchronized (lock) {if (container.sise() != 5) {try {lock.wait();} catch (InterruptedException e) {}}System.out.println("alert 5!");lock.notify();}}, "T2").start();TimeUnit.SECONDS.sleep(1);new Thread(() -> {synchronized (lock) {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加:" + container.sise());if (container.sise() == 5) {lock.notify();try {lock.wait();} catch (InterruptedException e) {}}}}}, "T1").start();} }程序解析:該版本的 wait-notify 可以滿足實際題目要求,達到實時觸發警報,在 T1 notify 之后立刻調用 wait 進入狀態;另一邊T2在被喚醒并輸出報警信息后,也需要再次調用 notify 喚醒 其他等待線程繼續執行任務。
二、notify 和 notifyAll
如果有多個線程等待同一個對象鎖,那么 notify 方法會隨機喚醒一個線程,它無法做到精準喚醒。notifyAll 是喚醒全部等待線程,但需要明確是是,由于wait-notify 的操作模式是基于鎖對象的,所以即便是 notifyAll 也是非公平競爭鎖資源,即哪個線程搶到鎖就去執行同步代碼。
三、wait-notify 的原理
我們聲明了一個 Object 對象,直接調用 wait 方法會怎樣呢?
public class T_Wait_Notify {public static void main(String[] args) throws InterruptedException {final Object lock = new Object();lock.wait();} }監視器狀態異常。這是因為 wait-notify 必須基于“鎖對象”,而這個鎖對象可不是普通的一個什么對象都可以。在了解了 synchronized 關鍵字的實現原理后,我們知道,JVM 為每個對象都關聯了一個 monitor 對象,進入同步代碼塊和結束同步代碼塊就對應著 monitor enter 和 monitor exit 兩條指令,也就是說,如果不使用 synchronized,就不存在所謂的 wait 和 notify。所以正確的寫法一定是:
synchronized (lock) {lock.wait(); }在 wait 方法的 Java doc 中這樣說明,wait 方法會令當前線程將自己放入鎖對象的 wait set 中,并且放棄此對象上所有同步的同步聲明。
當喚醒時,當前線程必須持有該對象的監視器,才能繼續執行。
總之,wait 和 notify 是和 對象的 monitor 緊密相關的,而 monitor 又是?synchronized 重量級鎖模式的實現原理,所以理解wait 和 notify的 同時 也需要深入理解 synchronized 關鍵字。
擴展:閉鎖實現的監控報警
閉鎖是一種同步工具類,可以延遲線程的進度直到其到達終止狀態。
閉鎖相當于一扇門:在閉鎖到達結束狀態之前,這扇門一直是關閉的,不允許任何線程通過,當到達結束狀態時,這扇門會打開并允許所有的線程通過。
下面的程序是通過 CountDownLatch 閉鎖來實現的一個監控容器的版本,雖然可以達到要求,但很遺憾,程序中必須通過 sleep 方法讓線程跑的“沒那么快”,否則,去掉 sleep 的話依然會出現 while-true 的執行結果,即警報沒那么實時了:
public class T_Alert04CountDownLatch {public static void main(String[] args) {Container container = new Container();CountDownLatch latch = new CountDownLatch(1);new Thread(() -> {if (container.sise() != 5) {try {latch.await();} catch (InterruptedException e) {}}System.out.println("alert 5!");}, "T2").start();new Thread(() -> {for (int i = 0; i < 10; i++) {container.add(new Object());System.out.println("已添加" + container.sise());if (container.sise() == 5) {latch.countDown();}try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {}}}, "T1").start();} }總結
使用 wait-notify 切換線程狀態是一種細粒度操作,開發者需要非常了解他們的執行邏輯以及線程的生命周期。
wait 和 notify使用時必須將對象鎖定,否則無法使用。
線程在使用對象的wait方法后會進入等待狀態,notify() 和 notifyAll() 可以喚醒其他線程,注意 notify 是隨機喚醒一個線程。
wait-notify的操作是相對復雜的,雖然強大,但是在處理復雜的業務邏輯中書寫較麻煩,相當于多線程中的匯編語言。
使用CountDownLatch可以有效的替代wait和notify的使用場景,而且不受鎖的限制,書寫簡便。
總結
以上是生活随笔為你收集整理的Java 多线程 —— wait 与 notify的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java8————方法引用
- 下一篇: Java核心篇之HashMap--day