Java等待喚醒機制wait/notify

爲了弄明白wait/notify機制,我們需要了解線程通信、volatile和synchronized關鍵字、wait/notify方法、Object的monitor機制等相關知識。本文將會從這幾個方面詳細講解Java的wait/notify等待喚醒機制。

一、線程通信


       如果一個線程從頭到尾執行完也不和別的線程打交道的話,那就不會有各種安全性問題了。但是協作越來越成爲社會發展的大勢,一個大任務拆成若干個小任務之後,各個小任務之間可能也需要相互協作最終才能執行完整個大任務。所以各個線程在執行過程中可以相互通信,所謂通信就是指相互交換一些數據或者發送一些控制指令,比如一個線程給另一個暫停執行的線程發送一個恢復執行的指令。

       wait和notify/notifyAll就是線程通信的一種方式。

       volatile和synchronized

       可變共享變量是天然的通信媒介,也就是說一個線程如果想和另一個線程通信的話,可以修改某個在多線程間共享的變量,另一個線程通過讀取這個共享變量來獲取通信的內容。

       由於原子性操作、內存可見性和指令重排序的存在,java提供了volatile和synchronized的同步手段來保證通信內容的正確性,假如沒有這些同步手段,一個線程的寫入不能被另一個線程立即觀測到,那這種通信就是不靠譜的。

二、wait、notify/notifyAll方法


       當一個線程獲取到鎖之後,如果發現條件不滿足,那就主動讓出鎖,然後把這個線程放到一個等待隊列裏等待去,等到某個線程把這個條件完成後,就通知等待隊列裏的線程他們等待的條件滿足了,可以繼續運行了。

       如果不同線程有不同的等待條件怎麼辦,總不能都塞到同一個等待隊列裏吧?是的java裏規定了每一個鎖都對應了一個等待隊列,也就是說如果一個線程在獲取到鎖之後發現某個條件不滿足,就主動讓出鎖然後把這個線程放到與它獲取到的鎖對應的那個等待隊列裏,另一個線程在完成對應條件時需要獲取同一個鎖,在條件完成後通知它獲取的鎖對應的等待隊列。這個過程意味着鎖和等待隊列建立了一對一關聯。

       怎麼讓出鎖並且把線程放到與鎖關聯的等待隊列中以及怎麼通知等待隊列中的線程,相關條件java已經爲我們規定好了。僅就代碼層面來說,我們可以理解爲,鎖其實就是個對象而已。在所有對象的父類Object中定義了這麼幾個方法:

public final void wait() throws InterruptedException
public final void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
 
public final void notify();
public final void notifyAll();

各個方法的詳細說明如下:

       wait():在線程獲取到鎖後,調用鎖對象的本方法,線程釋放鎖並且把該線程放置到與鎖對象關聯的等待隊列(等待線程池)。

       wait(long timeout):與wait()方法相似,只不過等待指定的毫秒數,如果超過指定時間則自動把該線程從等待隊列中移出

       wait(long timeout, int nanos): 與上邊的一樣,只不過超時時間粒度更小,即指定的毫秒數加納秒數

       notify(): 喚醒一個在與該鎖對象關聯的等待隊列中線程。一次喚醒一個,而且是任意的(究竟是不是任意的呢?後文會詳細介紹)。

       notifyAll():喚醒與該鎖對象關聯的等待隊列中的所有線程。可以將線程池中的所有wait() 線程都喚醒。

       其實,所謂喚醒的意思就是讓等待隊列中的線程具備執行資格。必須注意的是,這些方法都是在同步中才有效(爲什麼呢?下文會詳細介紹)。同時這些方法在使用時必須標明所屬鎖,這樣纔可以明確出這些方法操作的到底是哪個鎖上的線程。

 

       另一個很重要的問題是,notify()會立刻釋放鎖麼?

       notify()或者notifyAll()調用時並不會真正釋放對象鎖, 必須等到synchronized方法或者語法塊執行完才真正釋放鎖!!!

舉個例子:

public void test() {
    Object object = new Object();
    synchronized (object){
        object.notifyAll();
 
        while (true){
        }
    }
}

如上, 雖然調用了notifyAll, 但是緊接着進入了一個死循環。

這會導致一直不能出臨界區, 一直不能釋放對象鎖。

所以,即使它把所有在等待池中的線程都喚醒放到了對象的鎖池中,

但是鎖池中的所有線程都不會運行,因爲他們始終拿不到鎖。

三、Java對象的monitor機制


Java虛擬機給每個對象和class字節碼都設置了一個監聽器Monitor,用於檢測併發代碼的重入,同時在Object類中還提供了notify和wait方法來對線程進行控制。

1、圖解monitor機制

【圖註解:Java監視器】

       結合上圖來分析Object的Monitor機制。

       Monitor可以類比爲一個特殊的房間,這個房間中有一些被保護的數據,Monitor保證每次只能有一個線程能進入這個房間進行訪問被保護的數據,進入房間即爲持有Monitor,退出房間即爲釋放Monitor。

       當一個線程需要訪問受保護的數據(即需要獲取對象的Monitor)時,它會首先在entry-set入口隊列中排隊(這裏並不是真正的按照排隊順序),如果沒有其他線程正在持有對象的Monitor,那麼它會和entry-set隊列、wait-set隊列中的被喚醒的其他線程進行競爭(即通過CPU調度),選出一個線程來獲取對象的Monitor,執行受保護的代碼段,執行完畢後釋放Monitor,如果已經有線程持有對象的Monitor,那麼需要等待其釋放Monitor後再進行競爭。

       再說一下wait-set隊列。當一個線程擁有Monitor後,經過某些條件的判斷(比如用戶取錢發現賬戶沒錢),這個時候需要調用Object的wait方法,線程就釋放了Monitor,進入wait-set隊列,等待Object的notify方法(比如用戶向賬戶裏面存錢)。當該對象調用了notify方法或者notifyAll方法後,wait-set中的線程就會被喚醒,然後在wait-set隊列中被喚醒的線程和entry-set隊列中的線程一起通過CPU調度來競爭對象的Monitor,最終只有一個線程能獲取對象的Monitor。

       需要注意的是:

    【1】當一個線程在wait-set中被喚醒後,並不一定會立刻獲取Monitor,它需要和其他線程去競爭

    【2】如果一個線程是從wait-set隊列中喚醒後,獲取到的Monitor,它會去讀取它自己保存的PC計數器中的地址,從它調用wait方法的地方開始執行。

    【3】擁有monitor的是線程

    【4】同時只能有一個線程可以獲取某個對象的monitor

    【5】一個線程通過調用某個對象的wait()方法釋放該對象的monitor並進入等待隊列,直到其他線程獲取了被該線程釋放的monitor並調用該對象的notify()或者notifyAll()後再次競爭獲取該對象的monitor。

    【6】只有擁有該對象monitor的線程纔可以調用該對象的notify()和notifyAll()方法。如果沒有該對象monitor的線程調用了該對象的notify()或者notifyAll()方法將會拋出java.lang.IllegalMonitorStateException

2、Monitor的實現
       java中每個對象都有唯一的一個monitor,可以通過synchronized關鍵字實現線程同步來獲取對象的Monitor。

       先來看下利用synchronized實現同步的基礎:Java中的每個對象都可以作爲鎖。具體表現爲以下三種形式:

【1】對於普通同步方法,鎖是當前實例對象。
【2】對於靜態同步方法,鎖是當前類的Class對象。
【3】對於同步方法塊,鎖是Synchonized括號裏配置的對象。

三種方式具體代碼實例如下:

同步代碼塊

synchronized(Obejct obj) {
 
    //同步代碼塊
 
    ...
 
}

上述代碼表示在進入同步代碼塊之前,先要去獲取obj的Monitor,如果已經被其他線程獲取了,那麼當前線程必須等待直至其他線程釋放obj的Monitor

這裏的obj可以是類.class,表示需要去獲取該類的字節碼的Monitor,獲取後,其他線程無法再去獲取到class字節碼的Monitor了,即無法訪問屬於類的同步的靜態方法了,但是對於對象的實例方法的訪問不受影響
同步方法

public class Test {
 
    public static Test instance;
    public int val;
    
    public synchronized void set(int val) {
        this.val = val; 
    } 
 
    public static synchronized void set(Test instance) { 
        Test.instance = instance; 
    }
}

上述使用了synchronized分別修飾了非靜態方法和靜態方法。

       非靜態方法可以理解爲,需要獲取當前對象this的Monitor,獲取後,其他需要獲取該對象的Monitor的線程會被堵塞。

       靜態方法可以理解爲,需要獲取該類字節碼的Monitor(因爲static方法不屬於任何對象,而是屬於類的方法),獲取後,其他需要獲取字節碼的Monitor的線程會被堵塞。

3、圖解釋放鎖和獲取鎖的過程
       調用對象obj的wait(), notify()方法前,必須獲得obj鎖,也就是必須寫在synchronized(obj) 代碼段內。

       一個對象釋放鎖和獲取鎖的過程如下:

(同步隊列、入口隊列、鎖池是一個意思)      

       過程分析:

    【1】線程1獲取對象A的鎖,正在使用對象A。

    【2】線程1調用對象A的wait()方法。

    【3】線程1釋放對象A的鎖,並馬上進入等待隊列。

    【4】鎖池(入口隊列、同步隊列)裏面的對象爭搶對象A的鎖。

    【5】線程5獲得對象A的鎖,進入synchronized塊,使用對象A。

    【6】線程5調用對象A的notifyAll()方法,喚醒所有線程,所有線程進入同步隊列。若線程5調用對象A的notify()方法,則喚醒一個線程,不知道會喚醒誰,被喚醒的那個線程進入同步隊列。

    【7】notifyAll()方法所在synchronized結束,線程5釋放對象A的鎖。

    【8】同步隊列的線程爭搶對象鎖,但線程1什麼時候能搶到就不知道了。

 

       同步隊列狀態

    【1】當前線程想調用對象A的同步方法時,發現對象A的鎖被別的線程佔有,此時當前線程進入同步隊列。簡言之,同步隊列裏面放的都是想爭奪對象鎖的線程。

    【2】當一個線程1被另外一個線程2喚醒時,1線程進入同步隊列,去爭奪對象鎖。

    【3】同步隊列是在同步的環境下才有的概念,一個對象對應一個同步隊列。

    【4】線程等待時間到了或被notify/notifyAll喚醒後,會進入同步隊列競爭鎖,如果獲得鎖,進入RUNNABLE狀態,否則進入BLOCKED狀態等待獲取鎖。

四、隨機喚醒


       等待隊列裏許許多多的線程都wait()在一個對象上,此時某一線程調用了對象的notify()方法,那喚醒的到底是哪個線程?隨機?隊列FIFO?or sth else?Java文檔就簡單的寫了句:選擇是任意性的(The choice is arbitrary and occurs at the discretion of the implementation)。

       既然官方文檔都寫了是任意的,那麼真的是任意的嗎?

感興趣的key參考下面的文章。這裏賣個關子,不說結論。

13.1 大佬問我: notify()是隨機喚醒線程麼? - 簡書 (jianshu.com)

五、爲什麼wait和notify方法要在同步塊中調用?


       先回答問題

       爲什麼wait()必須在同步(Synchronized)方法/代碼塊中調用?

       答:調用wait()就是釋放鎖,釋放鎖的前提是必須要先獲得鎖,先獲得鎖才能釋放鎖。

       爲什麼notify(),notifyAll()必須在同步(Synchronized)方法/代碼塊中調用?

       notify(),notifyAll()是將鎖交給含有wait()方法的線程,讓其繼續執行下去,如果自身沒有鎖,怎麼叫把鎖交給其他線程呢;(本質是讓處於入口隊列的線程競爭鎖)

 

       下面來詳細說明。

       首先,要明白,每個Java對象都有唯一一個監視器monitor,這個監視器由三部分組成(一個獨佔鎖,一個入口隊列,一個等待隊列)。注意是一個對象只能有一個獨佔鎖,但是任意線程線程都可以擁有這個獨佔鎖。

      對於對象的非同步方法而言,任意時刻可以有任意個線程調用該方法。(即普通方法同一時刻可以有多個線程調用)

       對於對象的同步方法而言,只有擁有這個對象的獨佔鎖才能調用這個同步方法。如果這個獨佔鎖被其他線程佔用,那麼另外一個調用該同步方法的線程就會處於阻塞狀態,此線程進入入口隊列。

       若一個擁有該獨佔鎖的線程調用該對象同步方法的wait()方法,則該線程會釋放獨佔鎖,並加入對象的等待隊列;(爲什麼使用wait()?希望某個變量被設置之後再執行,notify()通知變量已經被設置。)

       某個線程調用notify(),notifyAll()方法是將等待隊列的線程轉移到入口隊列,然後讓他們競爭鎖,所以這個調用線程本身必須擁有鎖。

六、wait/notify應用舉例:生產者消費者模型


       下面,在不考慮實用性等前提下,我們會實現一個最簡單的生產者、消費者模型,僅僅只用來理解wait/notify的機制。

       在這個例子裏,將啓動一個生產者線程、一個消費者線程。生產者檢測到有產品可供消費時,通知消費者(notify)進行消費,同時自己進入等待狀態(wait),如果檢測到沒有產品可供消費,則進行生產。消費者檢測到有產品可供消費時,則進行消費,消費結束沒通知生產者進行生產,如果檢測到沒有產品可供消費,自然也通知生產者進行生產。

       也就是說,生產者線程和消費者線程會互相等待和互相通知。他們會爭奪同一個對象obj的鎖,實現線程之間的通信。

Product類:

public class Product {
    private static Integer count = 0;
 
    public static void add() {
        count++;
    }
 
    public static void delete() {
        count--;
    }
 
    public static Integer getCount(){
        return count;
    }
}

Produce類:

public class Produce implements Runnable {
 
    private Object object;
 
    public Produce(Object object) {
        this.object = object;
    }
 
    @Override
    public void run() {
        synchronized (object) {
            System.out.println("++++ 進入生產者線程");
            System.out.println("++++ 產品數量:" + Product.getCount());
            while (true) {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
 
                if (Product.getCount() <= 0) {
                    System.out.println("++++ 開始生產!");
                    Product.add();
                    System.out.println("++++ 生產後產品數量:" + Product.getCount());
                }else {
                    try {
                        // 通知消費者進行消費,自己進入等待
                        object.notify();
                        object.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
 
            }
 
        }
    }
}

Consume類:

import java.util.concurrent.TimeUnit;
 
public class Consume implements Runnable {
 
    private Object object;
 
    public Consume(Object object) {
        this.object = object;
    }
 
    @Override
    public void run() {
        synchronized (object) {
            System.out.println("---- 進入消費者線程");
            System.out.println("---- 當前產品數量:" + Product.getCount());
            // 判斷條件是否滿足(有沒有產品可以消費),若不滿足則等待
            while (true) {
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (Product.getCount() <= 0) {
                    try {
                        System.out.println("---- 沒有產品,進入等待");
                        // 通知生產者生產,自己進入等待
                        object.notify();
                        object.wait();
                        System.out.println("---- 結束等待,開始消費");
                        Product.delete();
                        System.out.println("---- 消費後產品數量:" + Product.getCount());
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                } else {
                    System.out.println("---- 已有產品,直接消費");
                    Product.delete();
                    System.out.println("---- 消費後產品數量:" + Product.getCount());
                }
 
 
            }
 
        }
    }
}

測試主類:

public class ThreadTest {
 
    static final Object obj = new Object();
  
    public static void main(String[] args) throws Exception {
 
        Thread consume = new Thread(new Consume(obj), "Consume");
        Thread produce = new Thread(new Produce(obj), "Produce");
        // 先啓動消費者
        consume.start();
        produce.start();
    }
}

運行結果:

程序會一直運行下去。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章