線程的通知與等待

Java中的Object類是所有類的父類,鑒于繼承機(jī)制卖词,Java把所有的類都需的方法放在了Object類里面吭练,其中就包含要說的通知與等待饱溢。

1.wait()方法

當(dāng)一個(gè)線程調(diào)用一個(gè)共享變量的wait()方法時(shí),該調(diào)用線程會(huì)被阻塞掛起,直到發(fā)生下面幾件事情之一才返回是嗜。

? 1.其他線程調(diào)用了該共享對(duì)象的 notify()或者 notifyAll() 方法痹筛。

? 2.其他線程調(diào)用了該線程的 interrupt() 方法可免,該線程拋出 InterruptedException 異常返回

另外需要注意的是耕漱,如果調(diào)用 wait() 方法的線程沒有事先獲取該對(duì)象的監(jiān)視器鎖,則調(diào)用wait()方法時(shí)調(diào)用線程會(huì)拋出 IllegalMonitorStateException異常尽爆。

那么一個(gè)線程如何才能獲取一個(gè)共享變量的監(jiān)視器鎖呢?

? 1.執(zhí)行synchronized同步代碼塊時(shí)使用該共享變量作為參數(shù)怎顾。

synchronized(共享變量){
    // doSomething
}

? 2.調(diào)用該共享變量的方法,并且該方法使用了 synchronized 修飾漱贱。

synchronized void method(int a, int b){
    // doSomething
}

另外需要注意的時(shí)槐雾,一個(gè)線程可以從掛起狀態(tài)變?yōu)榭梢赃\(yùn)行的狀態(tài)(也就是被喚醒),即使該線程沒有被其他線程調(diào)用notify(), notifyAll() 方法進(jìn)行通知幅狮,或者被中斷募强,或者等待超時(shí)。也就是所謂的虛假喚醒崇摄。

雖然虛假喚醒在應(yīng)用實(shí)踐中很少發(fā)生擎值,但要防患于未然,做法就是不停地去測試該線程被喚醒狀態(tài)的條件是否滿足配猫,不滿足則繼續(xù)等待幅恋,也就是說在一個(gè)循環(huán)中調(diào)用wait()方法進(jìn)行防范。退出循環(huán)的條件就是滿足了喚醒該線程的條件泵肄。

synchronized (obj) {
    while(條件不滿足){
        obj.wait();
    }
}

如上代碼呢也是經(jīng)典的調(diào)用共享變量wait()方法的實(shí)例,首先通過同步塊獲取obj上面的監(jiān)視器鎖淑翼,然后再while 循環(huán)內(nèi)調(diào)用obj的wait()方法腐巢。

下面從一個(gè)簡單的生產(chǎn)者和消費(fèi)者例子來加深下理解。如下面代碼所示玄括,其中queue為共享變量冯丙,生產(chǎn)者線程在調(diào)用queue的wait()方法前,使用synchronized關(guān)鍵字拿到了該共享變量queue的監(jiān)視器鎖,所以調(diào)用wait()方法不會(huì)拋出 IllegalMonitorStateException 異常胃惜。如果當(dāng)前隊(duì)列沒有空閑容量則會(huì)調(diào)用wait()方法掛起當(dāng)前線程泞莉,這里使用循環(huán)是為了避免上面說的虛假喚醒。假如當(dāng)前線程被虛假喚醒了船殉,但是隊(duì)列還是沒有空余的容量鲫趁,那么當(dāng)前線程還是會(huì)調(diào)用wait()方法把自己掛起。

public class ObjectMethodTest {

    Queue<String> queue = new LinkedList<String>();

    int MAX_SIZE = 1; // 假設(shè)隊(duì)列長度只有1 利虫, 只能存放一條數(shù)據(jù)


    public void produce(){
        synchronized (queue){

            // 隊(duì)列滿則等待隊(duì)列空間
            while (queue.size() == MAX_SIZE) {
                // 掛起當(dāng)前線程挨厚,并釋放通過同步塊獲取的queue上的鎖,讓消費(fèi)者線程可以獲取該鎖糠惫,然后獲取隊(duì)列里面的元素疫剃。
                try {
                    queue.wait();
                    System.out.println("-----等待消費(fèi)----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }


            queue.add("hahaha");
            queue.notifyAll();
        }
    }

    public void consume(){
        synchronized (queue) {

            while (queue.size() == 0) {
                // 掛起當(dāng)前線程,并釋放通過同步塊獲取的queue上的鎖硼讽,讓消費(fèi)者線程可以獲取該鎖巢价,然后獲取隊(duì)列里面的元素。
                try {
                    queue.wait();
                    System.out.println("-----等待生產(chǎn)-----");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            // 消費(fèi)元素, 并通知喚醒生產(chǎn)者
            System.out.println("消費(fèi)成功:" + queue.poll());
            queue.notifyAll();
        }
    }

    public static void main(String[] args) {


        ObjectMethodTest objectMethodTest = new ObjectMethodTest();

        // 10 個(gè)生產(chǎn)線程
        for (int i = 0; i < 10; i++) {
            new Thread(objectMethodTest::produce).start();
        }

        // 10 個(gè)消費(fèi)線程
        for (int i = 0; i < 10; i++) {
            new Thread(objectMethodTest::consume).start();
        }
    }
}
image-20200111212730839

在如上代碼中固阁,假如生產(chǎn)者線程A首先通過synchronized獲取到了queue上的鎖蹄溉,那么后續(xù)所有企圖生產(chǎn)的線程和消費(fèi)的線程 都將會(huì)在獲取該監(jiān)視器鎖的地方被阻塞掛起。線程A獲取鎖后發(fā)現(xiàn)隊(duì)列已滿會(huì)調(diào)用wait()方法阻塞掛起自己您炉,然后就會(huì)釋放掉獲取到的queue上的鎖柒爵,防止發(fā)生死鎖。

另外需要注意的是赚爵,當(dāng)前線程調(diào)用共享變量的wait()方法后指揮釋放當(dāng)前共享變量上的鎖棉胀,如果當(dāng)前線程還持有其他共享變量的鎖,則這些鎖是不會(huì)被釋放的冀膝,接下來看例子唁奢。

public class WaitTest {
    private static volatile Object resourceA = new Object();
    private static volatile Object resourceB = new Object();

    public static void main(String[] args) throws InterruptedException {

        // 創(chuàng)建線程
        Thread threadA = new Thread(() -> {

            try {
                synchronized (resourceA) {
                    System.out.println("threadA 獲取到resourceA的鎖");

                    synchronized (resourceB){
                        System.out.println("threadA 獲取到 resourceB的鎖");

                        System.out.println("threadA 釋放掉 resourceA的鎖");
                        resourceA.wait();
                    }
                }
            }catch (Exception ex) {
                ex.printStackTrace();
            }
        });


        // 創(chuàng)建線程
        Thread threadB = new Thread(() -> {

            try {
                Thread.sleep(1000);

                synchronized (resourceA) {
                    System.out.println("threadB 獲取到resourceA的鎖");

                    System.out.println("threadB 嘗試獲取resourceB的鎖*****");
                    synchronized (resourceB){
                        System.out.println("threadB 獲取到 resourceB的鎖");

                        System.out.println("threadB 釋放掉 resourceA的鎖");
                        resourceA.wait();
                    }
                }
            }catch (Exception ex) {
                ex.printStackTrace();
            }
        });

        threadA.start();
        threadB.start();

        threadA.join();
        threadB.join();
    }
}

image-20200111215813652

如上代碼在main方法里 啟動(dòng)了 A,B兩個(gè)線程,為了讓A先獲取到鎖窝剖,這里讓線程B休眠了1s麻掸,線程A先后獲取到了共享變量resourceA和resourceB上的鎖,然后調(diào)用了resourceA的wait()方法阻塞掛起自己赐纱,阻塞自己后線程A釋放掉了獲取到的resourceA上的鎖脊奋。

線程B休眠后結(jié)束后會(huì)先嘗試獲取resourceA上的鎖,如果當(dāng)前線程A里邊還沒有調(diào)用resourceA的wait()方法阻塞掛起釋放掉該鎖疙描,那么線程B就會(huì)被阻塞诚隙,如果線程A釋放了resourceA的鎖后,線程B就會(huì)獲取到resourceA上的鎖起胰,然后嘗試獲取resourceB上的鎖久又。由于線程A中沒有釋放鎖,所以導(dǎo)致線程B嘗試獲取resourceB上的鎖時(shí)會(huì)被阻塞。

以上就證明了當(dāng)前線程調(diào)用共享變量對(duì)象的wait()方法時(shí)地消,當(dāng)前線程只會(huì)釋放當(dāng)前共享對(duì)象的鎖炉峰,當(dāng)前線程持有其他共享對(duì)象的監(jiān)視器鎖并不會(huì)被釋放。

這里再舉個(gè)例子說明當(dāng)一個(gè)線程調(diào)用共享對(duì)象的wait()方法被阻塞掛起后脉执,如果其他線程中斷了該線程疼阔,則該線程會(huì)拋出InterruptedException異常返回。

public class WaitNotifyInterrupt {
    static Object obj = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread testThread = new Thread(() -> {
            try {
                System.out.println("----begin----");

                // 阻塞當(dāng)前線程
                synchronized (obj) {
                    obj.wait();
                }

                System.out.println("---end---");
            } catch (Exception ex) {
                ex.printStackTrace();
            }
        });

        testThread.start();

        Thread.sleep(1000);


        System.out.println("開始阻斷 testThread");
        testThread.interrupt();
        System.out.println("阻塞 testThread 完畢");
    }
}
image-20200112161109221

如上代碼适瓦,testThread調(diào)用了共享變量obj的wait()方法后阻塞掛起了自己竿开,然后主線程休眠1s后中斷了testThread線程,中斷后testThread再obj.wait()處拋出了java.lang.InterruptedException 異常而返回并終止玻熙。

2.wait(long timeout)方法

該方法相比于wait()方法多了一個(gè)超時(shí)參數(shù)否彩,它的不同之處在于,如果一個(gè)線程調(diào)用共享變量的該方法掛起后嗦随,沒有再指定的timeout ms時(shí)間內(nèi)被其他線程調(diào)用該共享變量的notify()或者notifyAll()方法喚醒列荔,那么該函數(shù)還是因?yàn)槌瑫r(shí)而返回。如果將timeoout設(shè)置為0那么則和wait()方法效果一樣枚尼,因?yàn)閣ait()方法內(nèi)部就是調(diào)用了wait(0)贴浙,需要注意的是,如果在調(diào)用該方法時(shí)署恍,傳遞了一個(gè)負(fù)的timeout則會(huì)拋出IllegalArgumentException異常崎溃。

3.wait(long timeout, int nanos)方法

在內(nèi)部調(diào)用的是wait(long timeout)函數(shù),如下代碼只用nanos>0時(shí)才使timeout參數(shù)遞增1盯质。

    public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
    }

4.notify()方法

一個(gè)線程調(diào)用共享對(duì)象的notify方法后袁串,會(huì)喚醒一個(gè)在該共享變量上調(diào)用wait系列方法后被阻塞掛起的線程。一個(gè)共享變量上可能會(huì)有多個(gè)線程在等待呼巷,具體喚醒哪個(gè)等待的線程是隨機(jī)的囱修。

此外,被喚醒的線程不能馬上從wait方法返回并繼續(xù)執(zhí)行王悍,它必須在獲取了共享對(duì)象的監(jiān)視器鎖后才可以返回破镰,也就是喚醒它的線程釋放了共享變量上的監(jiān)視器鎖后,被喚醒的線程也不一定會(huì)獲取到共享變量的監(jiān)視器鎖压储,這是因?yàn)樵摼€程還需要和其他線程一起競爭該鎖鲜漩,只有該線程競爭到了共享變量的監(jiān)視器鎖后才能繼續(xù)執(zhí)行。

類似wait()系列方法渠脉,只有當(dāng)前線程獲取到了共享變量的監(jiān)視器鎖后宇整,才可以調(diào)用共享變量的notify()方法,否則會(huì)拋出 IllegalMonitorStateException異常芋膘。

5.notifyAll()方法

不同于在共享變量上調(diào)用notify(),會(huì)喚醒被阻塞到該共享變量上的一個(gè)線程,notifyAll()方法則會(huì)喚醒所有在該共享變量由于調(diào)用wait()系列方法而被掛起的線程为朋。

下面舉個(gè)例子來說明notify()和notifyAll()方法具體含義以及一些需要注意的地方臂拓。

public class NotifyTest {

    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {

        // 創(chuàng)建線程
        Thread threadA = new Thread(() -> {
            // 獲取resourceA的共享資源鎖
            synchronized (resourceA) {
                System.out.println("threadA 獲取到 resourceA 的鎖");

                try {
                    System.out.println("threadA 開始調(diào)用resourceA的wait()方法進(jìn)行阻塞掛起");
                    resourceA.wait();
                    System.out.println("threadA 結(jié)束等待");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        // 創(chuàng)建線程
        Thread threadB = new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("threadB 獲取到 resourceA 的鎖");

                try {
                    System.out.println("threadB 開始調(diào)用resourceA的wait()方法進(jìn)行阻塞掛起");
                    resourceA.wait();
                    System.out.println("threadB 結(jié)束等待");
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });

        // 創(chuàng)建線程
        Thread threadC = new Thread(() -> {
            synchronized (resourceA) {
                System.out.println("threadC 開始調(diào)用 resourceA的notify()方法");
                resourceA.notify();
            }
        });

        // 啟動(dòng)線程
        threadA.start();
        threadB.start();

        Thread.sleep(1000);

        threadC.start();

        // 等待線程執(zhí)行結(jié)束
        threadA.join();
        threadB.join();
        threadC.join();

        System.out.println("end---------------------");

    }
}
image-20200112170749487

如上代碼開啟了三個(gè)線程,其中A,B線程分別調(diào)用了resourceA的wait()方法习寸,線程C在主線程休眠1s后調(diào)用了notify()方法胶惰。主線程休息1s是為了保證讓線程A,B全部執(zhí)行完wait()方法后再調(diào)用線程C的notify()方法。

這個(gè)例子試圖再線程A和線程B都因調(diào)用共享資源resourceA的wait()方法而被阻塞后霞溪,讓線程C調(diào)用resourceA的notify()方法孵滞,從而喚醒線程A,B。但是從執(zhí)行結(jié)果來看鸯匹,只有一個(gè)線程A被喚醒坊饶,線程B依然在阻塞掛起狀態(tài)。

從輸出結(jié)果可知線程調(diào)度器這次先調(diào)度了線程A占用Cpu來運(yùn)行殴蓬,線程A先獲取到resourceA的資源所匿级,然后調(diào)用wait()方法阻塞掛起,釋放鎖染厅,而后線程B獲取到資源鎖痘绎,調(diào)用resourceA的wait()阻塞掛起。然后線程C調(diào)用notify()方法肖粮,嘗試喚醒線程孤页,這回激活resourceA的阻塞集合里面的一個(gè)線程,這里激活了線程A涩馆,所以線程A方法執(zhí)行完畢并返回了行施。線程B則繼續(xù)在阻塞等待中。如果把notify()方法換成notifyAll()結(jié)果會(huì)這樣凌净。

image-20200112171809381

換成notifyAll()方法后悲龟,可以看到都得到了喚醒。因?yàn)樯线呉舱f過了notifyAll()方法會(huì)喚醒共享變量內(nèi)所有的等待線程冰寻。這里就是喚醒了resourceA的等待集合里所有線程须教。只是線程B先搶到了resourceA上的鎖,然后返回斩芭。然后線程A搶到也進(jìn)行了返回轻腺。

嘗試把主線程里面的休眠1s去掉,看一下執(zhí)行結(jié)果划乖。

image-20200112172207861

線程B沒有正常被喚醒贬养。

這是因?yàn)榫€程C可能比線程B先執(zhí)行了。如果調(diào)用notifyAll()方法后一個(gè)線程調(diào)用了該共享變量的wait()方法而被放到阻塞集合琴庵,則該線程不會(huì)被喚醒的误算,指揮喚醒執(zhí)行notifyAll()方法前阻塞集合里的所有線程仰美。

對(duì)wait(),notify(),notifyAll()的說明結(jié)束了

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市儿礼,隨后出現(xiàn)的幾起案子咖杂,更是在濱河造成了極大的恐慌,老刑警劉巖蚊夫,帶你破解...
    沈念sama閱讀 211,265評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件诉字,死亡現(xiàn)場離奇詭異,居然都是意外死亡知纷,警方通過查閱死者的電腦和手機(jī)壤圃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,078評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來琅轧,“玉大人伍绳,你說我怎么就攤上這事∮コ浚” “怎么了墨叛?”我有些...
    開封第一講書人閱讀 156,852評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵,是天一觀的道長模蜡。 經(jīng)常有香客問我漠趁,道長,這世上最難降的妖魔是什么忍疾? 我笑而不...
    開封第一講書人閱讀 56,408評(píng)論 1 283
  • 正文 為了忘掉前任闯传,我火速辦了婚禮,結(jié)果婚禮上卤妒,老公的妹妹穿的比我還像新娘甥绿。我一直安慰自己,他們只是感情好则披,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,445評(píng)論 5 384
  • 文/花漫 我一把揭開白布共缕。 她就那樣靜靜地躺著,像睡著了一般士复。 火紅的嫁衣襯著肌膚如雪图谷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,772評(píng)論 1 290
  • 那天阱洪,我揣著相機(jī)與錄音便贵,去河邊找鬼。 笑死冗荸,一個(gè)胖子當(dāng)著我的面吹牛承璃,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播蚌本,決...
    沈念sama閱讀 38,921評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開眼盔粹,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼隘梨!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起玻佩,我...
    開封第一講書人閱讀 37,688評(píng)論 0 266
  • 序言:老撾萬榮一對(duì)情侶失蹤出嘹,失蹤者是張志新(化名)和其女友劉穎席楚,沒想到半個(gè)月后咬崔,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,130評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡烦秩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,467評(píng)論 2 325
  • 正文 我和宋清朗相戀三年垮斯,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片只祠。...
    茶點(diǎn)故事閱讀 38,617評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡兜蠕,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出抛寝,到底是詐尸還是另有隱情熊杨,我是刑警寧澤,帶...
    沈念sama閱讀 34,276評(píng)論 4 329
  • 正文 年R本政府宣布盗舰,位于F島的核電站晶府,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏钻趋。R本人自食惡果不足惜川陆,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,882評(píng)論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望蛮位。 院中可真熱鬧较沪,春花似錦、人聲如沸失仁。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,740評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽萄焦。三九已至控轿,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間楷扬,已是汗流浹背解幽。 一陣腳步聲響...
    開封第一講書人閱讀 31,967評(píng)論 1 265
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留烘苹,地道東北人躲株。 一個(gè)月前我還...
    沈念sama閱讀 46,315評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像镣衡,于是被迫代替她去往敵國和親霜定。 傳聞我的和親對(duì)象是個(gè)殘疾皇子档悠,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,486評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容