等待喚醒(wait / notify)機制

若是一個線程從頭至尾執行完也不和別的線程打交道的話,那就不會有各類安全性問題了。可是協做愈來愈成爲社會發展的大勢,一個大任務拆成若干個小任務以後,各個小任務之間可能也須要相互協做最終才能執行完整個大任務。因此各個線程在執行過程當中能夠相互通訊,所謂通訊就是指相互交換一些數據或者發送一些控制指令,好比一個線程給另外一個暫停執行的線程發送一個恢復執行的指令,下邊詳細看都有哪些通訊方式。java

volatile和synchronized

可變共享變量是自然的通訊媒介,也就是說一個線程若是想和另外一個線程通訊的話,能夠修改某個在多線程間共享的變量,另外一個線程經過讀取這個共享變量來獲取通訊的內容。安全

因爲原子性操做、內存可見性和指令重排序的存在,java提供了volatilesynchronized的同步手段來保證通訊內容的正確性,假如沒有這些同步手段,一個線程的寫入不能被另外一個線程當即觀測到,那這種通訊就是不靠譜的~多線程

wait/notify機制

故事背景

也不知道是那個遭天殺的給咱們學校廁所的坑裏塞了個塑料瓶,致使樓道里如黃河氾濫通常,臭味熏天。更加悲催的是整個樓只有這麼一個廁所,比這個更悲催的是這個廁所裏只有一個坑!!!!!好吧,讓咱們用java來描述一下這個廁所:ide

public class Washroom {

    private volatile boolean isAvailable = false;    //表示廁所是不是可用的狀態

    private Object lock = new Object(); //廁所門的鎖

    public boolean isAvailable() {
        return isAvailable;
    }

    public void setAvailable(boolean available) {
        this.isAvailable = available;
    }

    public Object getLock() {
        return lock;
    }
}

isAvailable字段表明廁所是否可用,因爲廁所損壞,默認是false的,lock字段表明這個廁所門的鎖。須要注意的是isAvailable字段被volatile修飾,也就是說有一個線程修改了它的值,它能夠當即對別的線程可見~this

因爲廁所資源寶貴,英明的學校領導當即擬定了一個修復任務:線程

public class RepairTask implements Runnable {

    private Washroom washroom;

    public RepairTask(Washroom washroom) {
        this.washroom = washroom;
    }

    @Override
    public void run() {

        synchronized (washroom.getLock()) {
            System.out.println("維修工 獲取了廁所的鎖");
            System.out.println("廁所維修中,維修廁所是一件辛苦活,須要很長時間。。。");

            try {
                Thread.sleep(5000L);    //用線程sleep表示維修的過程
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            washroom.setAvailable(true);        //維修結束把廁所置爲可用狀態
            System.out.println("維修工把廁所修好了,準備釋放鎖了");
        }
    }
}

這個維修計劃的內容就是當維修工進入廁所以後,先把門鎖上,而後開始維修,維修結束以後把WashroomisAvailable字段設置爲true,以表示廁所可用。翻譯

與此同時,一羣急得像熱鍋上的螞蟻的傢伙在廁所門前打轉轉,他們想作神馬不用我明說了吧😏😏:code

public class ShitTask implements Runnable {

    private Washroom washroom;

    private String name;

    public ShitTask(Washroom washroom, String name) {
        this.washroom = washroom;
        this.name = name;
    }

    @Override
    public void run() {
        synchronized (washroom.getLock()) {
            System.out.println(name + " 獲取了廁所的鎖");
            while (!washroom.isAvailable()) {
                // 一直等
            }
            System.out.println(name + " 上完了廁所");
        }
    }
}

這個ShitTask描述了上廁所的一個流程,先獲取到廁所的鎖,而後判斷廁所是否可用,若是不可用,則在一個死循環裏不斷的判斷廁所是否可用,直到廁所可用爲止,而後上完廁所釋放鎖走人。對象

而後咱們看看現實世界都發生了什麼吧:排序

public class Test {
    public static void main(String[] args) {
        Washroom washroom = new Washroom();
        new Thread(new RepairTask(washroom), "REPAIR-THREAD").start();

        try {
            Thread.sleep(1000L);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }

        new Thread(new ShitTask(washroom, "狗哥"), "BROTHER-DOG-THREAD").start();
        new Thread(new ShitTask(washroom, "貓爺"), "GRANDPA-CAT-THREAD").start();
        new Thread(new ShitTask(washroom, "王尼妹"), "WANG-NI-MEI-THREAD").start();
    }
}

學校先讓維修工進入廁所維修,而後包括狗哥、貓爺、王尼妹在內的上廁所大軍就開始圍着廁所打轉轉的旅程,咱們看一下執行結果:

維修工 獲取了廁所的鎖
廁所維修中,維修廁所是一件辛苦活,須要很長時間。。。
維修工把廁所修好了,準備釋放鎖了
王尼妹 獲取了廁所的鎖
王尼妹 上完了廁所
貓爺 獲取了廁所的鎖
貓爺 上完了廁所
狗哥 獲取了廁所的鎖
狗哥 上完了廁所

看起來沒有神馬問題,可是再回頭看看代碼,發現有兩處特別彆扭的地方:

  1. 在main線程開啓REPAIR-THREAD線程後,必須調用sleep方法等待一段時間才容許上廁所線程開啓。

    若是REPAIR-THREAD線程和其餘上廁所線程一起開啓的話,就有可能上廁所的人,好比狗哥先獲取到廁所的鎖,而後維修工壓根兒連廁所也進不去。可是真實狀況可能真的這樣的,狗哥先到了廁所,而後維修工纔到。不過狗哥的處理應該不是一直待在廁所裏,而是先出來等着,啥時候維修工說修好了他再進去。因此這點有些彆扭~

  2. 在一個上廁所的人獲取到廁所的鎖的時候,必須不斷判斷WashroomisAvailable字段是否爲true

    若是一我的進入到廁所發現廁所仍然處在不可用狀態的話,那它應該在某個地方休息,啥時候維修工把廁所修好了,再叫一下等着上廁所的人就行了嘛,不必本身不停的去檢查廁所是否被修好了。

總結一下,就是一個線程在獲取到鎖以後,若是指定條件不知足的話,應該主動讓出鎖,而後到專門的等待區等待,直到某個線程完成了指定的條件,再通知一下在等待這個條件完成的線程,讓它們繼續執行。

若是你以爲上邊這句話比較繞的話,我來給你翻譯一下:當上狗哥獲取到廁所門鎖以後,若是廁所處於不可用狀態,那就主動讓出鎖,而後到等待上廁所的隊伍裏排隊等待,直到維修工把廁所修理好,把廁所的狀態置爲可用後,維修工再通知須要上廁所的人,然他們正常上廁所。

具體使用方式

爲了實現這個構想,java裏提出了一套叫wait/notify的機制。當一個線程獲取到鎖以後,若是發現條件不知足,那就主動讓出鎖,而後把這個線程放到一個等待隊列等待去,等到某個線程把這個條件完成後,就通知等待隊列裏的線程他們等待的條件知足了,能夠繼續運行啦!

若是不一樣線程有不一樣的等待條件腫麼辦,總不能都塞到同一個等待隊列裏吧?是的,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() 通知一個在與該鎖對象關聯的等待隊列的線程,使它從wait()方法中返回繼續往下執行
notifyAll() 與上邊的相似,只不過通知該等待隊列中的全部線程

瞭解了這些方法的意思之後咱們再來改寫一下ShitTask

public class ShitTask implements Runnable {

    // ... 爲節省篇幅,省略相關字段和構造方法

    @Override
    public void run() {
        synchronized (washroom.getLock()) {
            System.out.println(name + " 獲取了廁所的鎖");
            while (!washroom.isAvailable()) {
                try {
                    washroom.getLock().wait();  //調用鎖對象的wait()方法,讓出鎖,並把當前線程放到與鎖關聯的等待隊列
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println(name + " 上完了廁所");
        }
    }
}

看,原來咱們在判斷廁所是否可用的死循環里加了這麼一段代碼:

washroom.getLock().wait();

這段代碼的意思就是讓出廁所的鎖,而且把當前線程放到與廁所的鎖相關聯的等待隊列裏。

而後咱們也須要修改一下維修任務:

public class RepairTask implements Runnable {

    // ... 爲節省篇幅,省略相關字段和構造方法

    @Override
    public void run() {

        synchronized (washroom.getLock()) {
            System.out.println("維修工 獲取了廁所的鎖");
            System.out.println("廁所維修中,維修廁所是一件辛苦活,須要很長時間。。。");

            try {
                Thread.sleep(5000L);    //用線程sleep表示維修的過程
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            washroom.setAvailable(true);    //維修結束把廁所置爲可用狀態
            
            washroom.getLock().notifyAll(); //通知全部在與鎖對象關聯的等待隊列裏的線程,它們能夠繼續執行了
            System.out.println("維修工把廁所修好了,準備釋放鎖了");
        }
    }
}

你們能夠看出來,咱們在維修結束後加了這麼一行代碼:

washroom.getLock().notifyAll();

這個代碼表示將通知全部在與鎖對象關聯的等待隊列裏的線程,它們能夠繼續執行了。

在使用java的wait/notify機制修改了ShitTaskRepairTask後,咱們在復原一下整個現實場景:

public class Test {
    public static void main(String[] args) {
        Washroom washroom = new Washroom();
        new Thread(new ShitTask(washroom, "狗哥"), "BROTHER-DOG-THREAD").start();
        new Thread(new ShitTask(washroom, "貓爺"), "GRANDPA-CAT-THREAD").start();
        new Thread(new ShitTask(washroom, "王尼妹"), "WANG-NI-MEI-THREAD").start();

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        
        new Thread(new RepairTask(washroom), "REPAIR-THREAD").start();
    }
}

在這個場景中,咱們能夠刻意讓着急上廁所的先到達了廁所,維修工最後抵達廁所,來看一下加了wait/notify機制的代碼的執行結果是:

狗哥 獲取了廁所的鎖
貓爺 獲取了廁所的鎖
王尼妹 獲取了廁所的鎖
維修工 獲取了廁所的鎖
廁所維修中,維修廁所是一件辛苦活,須要很長時間。。。
維修工把廁所修好了,準備釋放鎖了
王尼妹 上完了廁所
貓爺 上完了廁所
狗哥 上完了廁所

從執行結果能夠看出來,狗哥、貓爺、王尼妹雖然先到達了廁所而且獲取到鎖,可是因爲廁所處於不可用狀態,因此都先調用wait()方法讓出了本身得到的鎖,而後躲到與這個鎖關聯的等待隊列裏,直到維修工修完了廁所,通知了在等待隊列中的狗哥、貓爺、王尼妹,他們才又開始繼續執行上廁所的程序~

通用模式

通過上邊的廁所案例,你們應該對wait/notify機制有了大體瞭解,下邊咱們總結一下這個機制的通用模式。首先看一下等待線程的通用模式:

  1. 獲取對象鎖。

  2. 若是某個條件不知足的話,調用鎖對象的wait方法,被通知後仍要檢查條件是否知足。

  3. 條件知足則繼續執行代碼。

通用的代碼以下:

synchronized (對象) {
    處理邏輯(可選)
    while(條件不知足) {
        對象.wait();
    }
    處理邏輯(可選)
}

除了判斷條件是否知足和調用wait方法之外的代碼,其餘的處理邏輯是可選的。

下邊再來看通知線程的通用模式:

  1. 得到對象的鎖。
  2. 完成條件。
  3. 通知在等待隊列中的等待線程。
synchronized (對象) {
    完成條件
    對象.notifyAll();、
}

小貼士:別忘了同步方法也是使用鎖的喔,靜態同步方法的鎖對象是該類的Class對象,成員同步方法的鎖對象是this對象。因此若是沒有刻意強調,下邊所說的同步代碼塊也包含同步方法。

瞭解了wait/notify的通用模式以後,使用的時候須要特別當心,須要注意下邊這些方面:

  • 必須在同步代碼塊中調用waitnotify或者notifyAll方法。

    有的童鞋會有疑問,爲啥wait/notify機制的這些方法必須都放在同步代碼塊中才能調用呢?wait方法的意思只是讓當前線程中止執行,把當前線程放在等待隊列裏,notify方法的意思只是從等待隊列裏移除一個線程而已,跟加鎖有什麼關係?

    答:由於wait方法是運行在等待線程裏的,notify或者notifyAll是運行在通知線程裏的。而執行wait方法前須要判斷一下某個條件是否知足,若是不知足纔會執行wait方法,這是一個先檢查後執行的操做,不是一個原子性操做,因此若是不加鎖的話,在多線程環境下等待線程和通知線程的執行順序多是這樣的:

    img

    也就是說當等待線程已經判斷條件不知足,正要執行wait方法,此時通知線程搶先把條件完成而且調用了notify方法,以後等待線程才執行到wait方法,這會致使等待線程永遠停留在等待隊列而沒有人再去notify它。因此等待線程中的判斷條件是否知足、調用wait方法和通知線程中完成條件、調用notify方法都應該是原子性操做,彼此之間是互斥的,因此用同一個鎖來對這兩個原子性操做進行同步,從而避免出現等待線程永久等待的尷尬局面。

    若是不在同步代碼塊中調用waitnotify或者notifyAll方法,也就是說沒有獲取鎖就調用wait方法,就像這樣:

    對象.wait();

    是會拋出IllegalMonitorStateException異常的。

  • 在同步代碼塊中,必須調用獲取的鎖對象的waitnotify或者notifyAll方法。

    也就是說不能隨便調用一個對象的waitnotify或者notifyAll方法。好比等待線程中的代碼是這樣的:

    synchronized (對象1) {
        while(條件不知足) {
            對象2.wait();    //隨便調用一個對象的wait方法
        }
    }
  • 通知線程中的代碼是這樣的:

    synchronized (對象1) {
        完成條件
        對象2.notifyAll();
    }

    對於代碼對象2.wait(),表示讓出當前線程持有的對象2的鎖,而當前線程持有的是對象1的鎖,因此這麼寫是錯誤的,也會拋出IllegalMonitorStateException異常的。意思就是若是當前線程不持有某個對象的鎖,那它就不能調用該對象的wait方法來讓出該鎖。因此若是想讓等待線程讓出當前持有的鎖,只能調用對象1.wait()。而後這個線程就被放置到與對象1相關聯的等待隊列中,在通知線程中只能調用對象1.notifyAll()來通知這些等待的線程了。

  • 在等待線程判斷條件是否知足時,應該使用while,而不是if

    也就是說在判斷條件是否知足的時候要使用while

    while(條件不知足) { //正確✅
        對象.wait();
    }

    而不是使用if

    if(條件不知足) { //錯誤❌
        對象.wait();
    }

    這個是由於在多線程條件下,可能在一個線程調用notify以後當即又有一個線程把條件改爲了不知足的狀態,好比在維修工把廁所修好以後通知你們上廁所吧的瞬間,有一個小屁孩以迅雷不及掩耳之勢又給廁所坑裏塞了個瓶子,廁所又被置爲不可用狀態,等待上廁所的仍是須要再判斷一下條件是否知足才能繼續執行。

  • 在調用完鎖對象的notify或者notifyAll方法後,等待線程並不會當即從wait()方法返回,須要調用notify()或者notifyAll()的線程釋放鎖以後,等待線程才從wait()返回繼續執行。

    也就是說若是通知線程在調用完鎖對象的notify或者notifyAll方法後還有須要執行的代碼,就像這樣:

    synchronized (對象) {
        完成條件
        對象.notifyAll();
        ... 通知後的處理邏輯
    }

    須要把通知後的處理邏輯執行完成後,把鎖釋放掉,其餘線程才能夠從wait狀態恢復過來,從新競爭鎖來執行代碼。比方說在維修工修好廁所並通知了等待上廁所的人們以後,他尚未從廁所出來,而是在廁所的牆上寫了 "XXX到此一遊"之類的話以後才從廁所出來,從廁所出來才表明着釋放了鎖,狗哥、貓爺、王尼妹纔開始爭搶進入廁所的機會。

  • notify方法只會將等待隊列中的一個線程移出,而notifyAll方法會將等待隊列中的全部線程移出。

    你們能夠把上邊代碼中的notifyAll方法替換稱notify方法,看看執行結果~

wait和sleep的區別

眼尖的小夥伴確定發現,waitsleep這兩個方法均可以讓線程暫停執行,並且都有InterruptedException的異常說明,那麼它們的區別是啥呢?

  • waitObject的成員方法,而sleepThread的靜態方法。

    只要是做爲鎖的對象均可以在同步代碼塊中調用本身的wait方法,sleepThread的靜態方法,表示的是讓當前線程休眠指定的時間。

  • 調用wait方法須要先得到鎖,而調用sleep方法是不須要的。

    再一次強調,必定要在同步代碼塊中調用鎖對象的wait方法,前提是要得到鎖!前提是要得到鎖!前提是要得到鎖!而sleep方法隨時調用~

  • 調用wait方法的線程須要用notify來喚醒,而sleep必須設置超時值。

  • 線程在調用wait方法以後會先釋放鎖,而sleep不會釋放鎖。

    這一點多是最重要的一點不一樣點了吧,狗哥、貓爺、王尼妹這些線程一開始是獲取到廁所的鎖了,可是調用了wait方法以後主動把鎖讓出,從而讓維修工得以進入廁所維修。若是狗哥在發現廁所是不可用的條件時選擇調用sleep方法的話,線程是不會釋放鎖的,也就是說維修工沒法得到廁所的鎖,也就修不了廁所了~ 你們必定要謹記這一點啊!

總結

  1. 線程間須要經過通訊才能協做解決某個複雜的問題。

  2. 可變共享變量是自然的通訊媒介,可是使用的時候必定要保證線程安全性,一般使用volatile變量或synchronized來保證線程安全性。

  3. 一個線程在獲取到鎖以後,若是指定條件不知足的話,應該主動讓出鎖,而後到專門的等待區等待,直到某個線程完成了指定的條件,再通知一下在等待這個條件完成的線程,讓它們繼續執行。這個機制就是wait/notify機制。

  4. 等待線程的通用模式:

    synchronized (對象) {
        處理邏輯(可選)
        while(條件不知足) {
            對象.wait();
        }
        處理邏輯(可選)
    }

    能夠分爲下邊幾個步驟:

. - 獲取對象鎖。

  • 若是某個條件不知足的話,調用鎖對象的wait方法,被通知後仍要檢查條件是否知足。
  • 條件知足則繼續執行代碼。
  1. 通知線程的通用模式:

    synchronized (對象) {
        完成條件
        對象.notifyAll();、
    }

    能夠分爲下邊幾個步驟:

    • 得到對象的鎖。

    • 完成條件。

    • 通知在等待隊列中的等待線程。

  2. waitsleep的區別

    • wait是Object的成員方法,而sleep是Thread的靜態方法。
    • 調用wait方法須要先得到鎖,而調用sleep方法是不須要的。
    • 調用wait方法的線程須要用notify來喚醒,而sleep必須設置超時值。
    • 線程在調用wait方法以後會先釋放鎖,而sleep不會釋放鎖。
相關文章
相關標籤/搜索