java的等待通知機制(java異步消息通知機制)

本文目錄一覽:

如何在 Java 中正確使用 wait,notify 和 notifyAll

我們先來了解一下為什麼要使用wait,notify

首先看一下以下代碼:

synchronized(a){

        …//1

synchronized(b){

}

}

synchronized(b){

… //2

synchronized(a){

}

}

假設現在有兩個線程, t1 線程運行到了//1 的位置,而 t2 線程運行到了//2 的位置,接

下來會發生什麼情況呢?

此時, a 對象的鎖標記被 t1 線程獲得,而 b 對象的鎖標記被 t2 線程獲得。對於 t1 線程

而言,為了進入對 b 加鎖的同步代碼塊, t1 線程必須獲得 b 對象的鎖標記。由於 b 對象的鎖標記被 t2 線程獲得, t1 線程無法獲得這個對象的鎖標記,因此它會進入 b 對象的鎖池,等待 b 對象鎖標記的釋放。而對於 t2 線程而言,由於要進入對 a 加鎖的同步代碼塊,由於 a 對象的鎖標記在 t1 線程手中,因此 t2 線程會進入 a 對象的鎖池。

此時, t1 線程在等待 b 對象鎖標記的釋放,而 t2 線程在等待 a 對象鎖標記的釋放。由

於兩邊都無法獲得所需的鎖標記,因此兩個線程都無法運行。這就是「死鎖」問題。

在 Java 中,採用了 wait 和 notify 這兩個方法,來解決死鎖機制。

首先,在 Java 中,每一個對象都有兩個方法: wait 和 notify 方法。這兩個方法是定義

在 Object 類中的方法。對某個對象調用 wait()方法,表明讓線程暫時釋放該對象的鎖標記。

例如,上面的代碼就可以改成:

synchronized(a){

…//1

a.wait();

synchronized(b){

}

}

synchronized(b){

… //2

synchronized(a){

a.notify();

}

}

這樣的代碼改完之後,在//1 後面, t1 線程就會調用 a 對象的 wait 方法。此時, t1 線程

會暫時釋放自己擁有的 a 對象的鎖標記,而進入另外一個狀態:等待狀態。

要注意的是,如果要調用一個對象的 wait 方法,前提是線程已經獲得這個對象的鎖標

記。如果在沒有獲得對象鎖標記的情況下調用 wait 方法,則會產生異常。

由於 a 對象的鎖標記被釋放,因此, t2 對象可以獲得 a 對象的鎖標記,從而進入對 a

加鎖的同步代碼塊。在同步代碼塊的最後,調用 a.notify()方法。這個方法與 wait 方法相對應,是讓一個線程從等待狀態被喚醒。

那麼 t2 線程喚醒 t1 線程之後, t1 線程處於什麼狀態呢?由於 t1 線程喚醒之後還要在

對 a 加鎖的同步代碼塊中運行,而 t2 線程調用了 notify()方法之後,並沒有立刻退出對 a 鎖的同步代碼塊,因此此時 t1 線程並不能馬上獲得 a 對象的鎖標記。因此,此時, t1 線程會在 a 對象的鎖池中進行等待,以期待獲得 a 對象的鎖標記。也就是說,一個線程如果之前調用了 wait 方法,則必須要被另一個線程調用 notify()方法喚醒。喚醒之後,會進入鎖池狀態。線程狀態轉換圖如下:

由於可能有多個線程先後調用 a 對象 wait 方法,因此在 a 對象等待狀態中的線程可能

有多個。而調用 a.notify()方法,會從 a 對象等待狀態中的多個線程里挑選一個線程進行喚醒。

與之對應的,有一個 notifyAll()方法, 調用 a.notifyAll() 會把 a 對象等待狀態中的所有線程都喚醒。

下面結合一個實際的例子,來演示如何使用 wait 和 notify / notifyAll 方法。

我使用一個數組來模擬一個比較熟悉的數據結構:棧。代碼如下所示:

class MyStack{

    private char[] data = new char[5];

    private int index = 0;

    public char pop(){

        index — ;

        return data[index];

    }

    public void push(char ch){

        data[index] = ch;

        index++;

    }

    public void print(){

        for (int i=0; iindex; i++){

            System.out.print(data[i] + “\t”);

        }

        System.out.println();

    }

    public boolean isEmpty(){

        return index == 0;

    }

    public boolean isFull(){

        return index == 5;

    }

}

注意,我們為 MyStack 增加了兩個方法,一個用來判斷棧是否為空,一個用來判斷棧是

否已經滿了。

然後我們創建兩個線程,一個線程每隔一段隨機的時間,就會往棧中增加一個數據;另

一個線程每隔一段隨機的時間,就會從棧中取走一個數據。為了保證 push 和 pop 的完整性,在線程中應當對 MyStack 對象加鎖。

但是我們發現,入棧線程和出棧線程並不是在任何時候都可以工作的。當數組滿了的時

候,入棧線程將不能工作;當數組空了的時候,出棧線程也不能工作。違反了上面的條件,我們將得到一個數組下標越界異常。

為此,我們可以用 wait/notify 機制。在入棧線程執行入棧操作時,如果發現數組已滿,

則會調用 wait 方法,去等待。同樣,出棧線程在執行出棧操作時,如果發現數組已空,同

樣調用 wait 方法去等待。在入棧線程結束入棧工作之後,會調用 notifyAll 方法,釋放那些正在等待的出棧線程(因為數組現在已經不是空的了,他們可以恢復工作了)。同樣,當出棧線程結束出棧工作之後,也會調用 notifyAll 方法,釋放正在等待的入棧線程。

一段生產者/消費者相關代碼:

class Consumer extends Thread{

    private MyStack ms;

    public Consumer(MyStack ms) {

        this.ms = ms;

    }

    public void run(){

        while(true){

        //為了保證 push 和 pop 操作的完整性

        //必須加 synchronized

            synchronized(ms){

            //如果棧空間已滿,則 wait()釋放 ms 的鎖標記

                while(ms.isEmpty()){

                    try {

                        ms.wait();

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

                char ch = ms.pop();

                System.out.println(“Pop ” + ch);

                ms.notifyAll();

            }

            //push 之後隨機休眠一段時間

            try {

                sleep( (int)Math.abs(Math.random() * 100) );

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

//生產者

class Producer extends Thread{

    private MyStack ms;

    public Producer(MyStack ms) {

        this.ms = ms;

    }

    public void run(){

        while(true){

        //為了保證 push 和 pop 操作的完整性

        //必須加 synchronized

            synchronized(ms){

            //如果棧空間已滿,則 wait()釋放 ms 的鎖標記

                while(ms.isFull()){

                    try {

                        ms.wait();

                    } catch (InterruptedException e) {

                        e.printStackTrace();

                    }

                }

                ms.push(‘A’);

                System.out.println(“push A”);

                ms.notifyAll();

            }

            //push 之後隨機休眠一段時間

            try {

                sleep( (int)Math.abs(Math.random() * 200) );

            } catch (InterruptedException e) {

                e.printStackTrace();

            }

        }

    }

}

//主方法中,啟用生產者與消費者兩個線程

public class TestWaitNotify {

    public static void main(String[] args) {

        MyStack ms = new MyStack();

        Thread t1 = new Producer(ms);

        Thread t2 = new Consumer(ms);

        t1.start();

        t2.start();

    }

}

部分代碼純手打,望採納~

java同步中,為什麼要wait,又notify誰?

對象鎖與同步塊或者實例同步方法相關係,但如果線程進入靜態同步方法,就必須獲得類鎖。用鎖只能達到這樣的目的:使得一個任務不會幹涉另一個任務的資源,保證在任何時刻都只有一個任務可以訪問某個資源。但兩個任務要協同作戰,互相通信,要一起工作去解決某個問題,必須使他們友好握手、共商國事。這種機制靠Object的方法wait()和notify()來安全地實現。在Thread對象上調用wait()方法將釋放線程所有的鎖定,這種說法是錯誤的。Thread類對象也是對象也有wait()方法,它釋放的只是它自己作為線程對象的鎖,這在線程池的概念級上理解。 似乎理解起來,wait()是自己停止,等待被喚醒;notify()也是自己停止,通知別人。那麼感覺沒什麼大的區別,不急,先仔細分析他們的來歷。wait()通常線程要執行下去需要等待某個條件發生變化,但改變這個條件已經超出了當前方法的控制能力。通常,這種條件由另一個任務來改變。既然執行不下去,傻等,又改變不了現實,那還不如交出執行權,令當前線程掛起,同步資源解鎖,使別的線程可以訪問並修改共享資源,自己進行排隊隊列,等候別人的通知。經過測試,好象是先入後出的順序被喚醒的。釋放了鎖意味着另一個任務可以獲得這個鎖,這一點至關重要,因為這些其他的方法再入處理通常會引起wait()感興趣的變化。wait()和notify()必須包括在synchronized代碼塊中,等待中的線程必須由notify()方法顯式地喚醒,否則它會永遠地等待下去。很多人初級接觸多線程時,會習慣把wait()和notify()放在run()方法里,一定要謹記,這兩個方法屬於某個對象,應在對象所在的類方法中定義它,然後run中去調用它。 這裡不得不提下,在Object的wait方法是重載的。有三個方法,了解一下除無參之外的另一個方法wait(毫秒數 n); 這裡毫秒數是指,如果沒有notify通知的情況下,當前被wait線程,經過n毫秒之後依然可以回到可運行狀態。如果參數為零,則不考慮實際時間,在獲得通知前該線程將一直等待。wait(0, 0) 與 wait(0) 相同。 notify()喚醒正在隊列中等待資源的優先級最高的線程。但它自己不馬上退出資源,繼續執行,等全部執行完了,退出,釋放鎖,這樣才讓wait()的線程進入。所以說在對象(當前線程具有其鎖定)調用notify()方法一定釋放鎖定是只是一廂情願的。至於與notifyAll()區別,後者更加安全。使用notify(),在眾多等待同一個鎖的任務中只有一個會被喚醒,因此如果你希望使用notify(),就必須保證被喚醒的是恰當的任務。notify()也就是this.notify(),喚醒所有爭搶自己的線程,與別的對象產生的wait()沒有關係。 synchronized (a) {

System.out.println(“notify”);

a.notifyAll(); //假如這裡是wait(),下句代碼就暫時不會執行!

System.out.println(“continue”); //notifyAll()以後,這句代碼還是要執行的

} 我曾經自己寫過如下非常幼稚的代碼,寫在public void run()裡邊,目的是在zoneRectangleSize 1的情況下,使自己的線程處於阻塞狀態,雖然不會出錯,但這個wait調用的是線程類對象本身的wait(),畢竟它也是來自Object,所以肯定達不到預期的效果: synchronized (mainApp) {

} Thread的靜態方法sleep()是不釋放鎖的,也不用操作鎖,所以可以在非同步控制方法和run方法內部調用。也就是說當前線程即使進入sleep狀態也抱着這把鎖睡覺,保持高度的監控狀態,即使其它線程在外邊踢門叫嚷罵娘,他是心安理得,堅決不釋放。如果一直昏睡下去,擁有同一對象資源的線程們都會玩完的。

java中notify怎麼使用?

notify(),notifyAll()都是要喚醒正在等待的線程,前者明確喚醒一個,後者喚醒全部。

當程序不明確知道下一個要喚醒的線程時,需要採用notifyAll()喚醒所有在wait池中的線程,讓它們競爭而獲取資源的執行權,但使用notifyAll()時,會出現死鎖的風險,因此,如果程序中明確知道下一個要喚醒的線程時,儘可能使用notify()而非notifyAll()。

java的等待喚醒機制必須要讓線程等待嗎

1. 線程的掛起和喚醒

掛起實際上是讓線程進入「非可執行」狀態下,在這個狀態下CPU不會分給線程時間片,進入這個狀態可以用來暫停一個線程的運行;在線程掛起後,可以通過重新喚醒線程來使之恢復運行。

掛起的原因可能是如下幾種情況:

(1)通過調用sleep()方法使線程進入休眠狀態,線程在指定時間內不會運行。

(2)通過調用join()方法使線程掛起,使自己等待另一個線程的結果,直到另一個線程執行完畢為止。

(3)通過調用wait()方法使線程掛起,直到線程得到了notify()和notifyAll()消息,線程才會進入「可執行」狀態。

(4)使用suspend掛起線程後,可以通過resume方法喚醒線程。

雖然suspend和resume可以很方便地使線程掛起和喚醒,但由於使用這兩個方法可能會造成死鎖,因此,這兩個方法被標識為deprecated(抗議)標記,這表明在以後的jdk版本中這兩個方法可能被刪除,所以盡量不要使用這兩個方法來操作線程。

調用sleep()、yield()、suspend()的時候並沒有被釋放鎖

調用wait()的時候釋放當前對象的鎖

wait()方法表示,放棄當前對資源的占有權,一直等到有線程通知,才會運行後面的代碼。

notify()方法表示,當前的線程已經放棄對資源的佔有,通知等待的線程來獲得對資源的占有權,但是只有一個線程能夠從wait狀態中恢復,然後繼續運行wait()後面的語句。

notifyAll()方法表示,當前的線程已經放棄對資源的佔有,通知所有的等待線程從wait()方法後的語句開始運行。

2.等待和鎖實現資源競爭

等待機制與鎖機制是密切關聯的,對於需要競爭的資源,首先用synchronized確保這段代碼只能一個線程執行,可以再設置一個標誌位condition判斷該資源是否準備好,如果沒有,則該線程釋放鎖,自己進入等待狀態,直到接收到notify,程序從wait處繼續向下執行。

synchronized(obj) {

while(!condition) {

obj.wait();

}

obj.doSomething();

}

以上程序表示只有一個線程A獲得了obj鎖後,發現條件condition不滿足,無法繼續下一處理,於是線程A釋放該鎖,進入wait()。

在另一線程B中,如果B更改了某些條件,使得線程A的condition條件滿足了,就可以喚醒線程A:

synchronized(obj) {

condition = true;

obj.notify();

}

需要注意的是:

# 調用obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) {…} 代碼段內。

# 調用obj.wait()後,線程A就釋放了obj的鎖,否則線程B無法獲得obj鎖,也就無法在synchronized(obj) {…} 代碼段內喚醒A。

# 當obj.wait()方法返回後,線程A需要再次獲得obj鎖,才能繼續執行。

# 如果A1,A2,A3都在obj.wait(),則B調用obj.notify()只能喚醒A1,A2,A3中的一個(具體哪一個由JVM決定)。

# obj.notifyAll()則能全部喚醒A1,A2,A3,但是要繼續執行obj.wait()的下一條語句,必須獲得obj鎖,因此,A1,A2,A3隻有一個有機會獲得鎖繼續執行,例如A1,其餘的需要等待A1釋放obj鎖之後才能繼續執行。

# 當B調用obj.notify/notifyAll的時候,B正持有obj鎖,因此,A1,A2,A3雖被喚醒,但是仍無法獲得obj鎖。直到B退出synchronized塊,釋放obj鎖後,A1,A2,A3中的一個才有機會獲得鎖繼續執行。

原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/272287.html

(0)
打賞 微信掃一掃 微信掃一掃 支付寶掃一掃 支付寶掃一掃
小藍的頭像小藍
上一篇 2024-12-17 13:55
下一篇 2024-12-17 13:55

相關推薦

  • Java JsonPath 效率優化指南

    本篇文章將深入探討Java JsonPath的效率問題,並提供一些優化方案。 一、JsonPath 簡介 JsonPath是一個可用於從JSON數據中獲取信息的庫。它提供了一種DS…

    編程 2025-04-29
  • java client.getacsresponse 編譯報錯解決方法

    java client.getacsresponse 編譯報錯是Java編程過程中常見的錯誤,常見的原因是代碼的語法錯誤、類庫依賴問題和編譯環境的配置問題。下面將從多個方面進行分析…

    編程 2025-04-29
  • Java騰訊雲音視頻對接

    本文旨在從多個方面詳細闡述Java騰訊雲音視頻對接,提供完整的代碼示例。 一、騰訊雲音視頻介紹 騰訊雲音視頻服務(Cloud Tencent Real-Time Communica…

    編程 2025-04-29
  • Java Bean加載過程

    Java Bean加載過程涉及到類加載器、反射機制和Java虛擬機的執行過程。在本文中,將從這三個方面詳細闡述Java Bean加載的過程。 一、類加載器 類加載器是Java虛擬機…

    編程 2025-04-29
  • Java Milvus SearchParam withoutFields用法介紹

    本文將詳細介紹Java Milvus SearchParam withoutFields的相關知識和用法。 一、什麼是Java Milvus SearchParam without…

    編程 2025-04-29
  • Java 8中某一周的周一

    Java 8是Java語言中的一個版本,於2014年3月18日發佈。本文將從多個方面對Java 8中某一周的周一進行詳細的闡述。 一、數組處理 Java 8新特性之一是Stream…

    編程 2025-04-29
  • Java判斷字符串是否存在多個

    本文將從以下幾個方面詳細闡述如何使用Java判斷一個字符串中是否存在多個指定字符: 一、字符串遍歷 字符串是Java編程中非常重要的一種數據類型。要判斷字符串中是否存在多個指定字符…

    編程 2025-04-29
  • VSCode為什麼無法運行Java

    解答:VSCode無法運行Java是因為默認情況下,VSCode並沒有集成Java運行環境,需要手動添加Java運行環境或安裝相關插件才能實現Java代碼的編寫、調試和運行。 一、…

    編程 2025-04-29
  • Java任務下發回滾系統的設計與實現

    本文將介紹一個Java任務下發回滾系統的設計與實現。該系統可以用於執行複雜的任務,包括可回滾的任務,及時恢復任務失敗前的狀態。系統使用Java語言進行開發,可以支持多種類型的任務。…

    編程 2025-04-29
  • Java 8 Group By 會影響排序嗎?

    是的,Java 8中的Group By會對排序產生影響。本文將從多個方面探討Group By對排序的影響。 一、Group By的概述 Group By是SQL中的一種常見操作,它…

    編程 2025-04-29

發表回復

登錄後才能評論