<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          【69期】面試官:對并發(fā)熟悉嗎?談?wù)劸€程間的協(xié)作(wait/notify/sleep/yield/join)

          共 4266字,需瀏覽 9分鐘

           ·

          2020-10-25 10:22

          程序員的成長之路
          互聯(lián)網(wǎng)/程序員/技術(shù)/資料共享?
          關(guān)注


          閱讀本文大概需要 8.5 分鐘。

          來自:www.cnblogs.com/paddix/p/5381958.html

          一、線程的狀態(tài)

          Java中線程中狀態(tài)可分為五種:New(新建狀態(tài)),Runnable(就緒狀態(tài)),Running(運行狀態(tài)),Blocked(阻塞狀態(tài)),Dead(死亡狀態(tài))。
          • New:新建狀態(tài),當(dāng)線程創(chuàng)建完成時為新建狀態(tài),即new Thread(…),還沒有調(diào)用start方法時,線程處于新建狀態(tài)。

          • Runnable:就緒狀態(tài),當(dāng)調(diào)用線程的的start方法后,線程進入就緒狀態(tài),等待CPU資源。處于就緒狀態(tài)的線程由Java運行時系統(tǒng)的線程調(diào)度程序(thread scheduler)來調(diào)度。

          • Running:運行狀態(tài),就緒狀態(tài)的線程獲取到CPU執(zhí)行權(quán)以后進入運行狀態(tài),開始執(zhí)行run方法。

          • Blocked:阻塞狀態(tài),線程沒有執(zhí)行完,由于某種原因(如,I/O操作等)讓出CPU執(zhí)行權(quán),自身進入阻塞狀態(tài)。

          • Dead:死亡狀態(tài),線程執(zhí)行完成或者執(zhí)行過程中出現(xiàn)異常,線程就會進入死亡狀態(tài)。

          這五種狀態(tài)之間的轉(zhuǎn)換關(guān)系如下圖所示:
          有了對這五種狀態(tài)的基本了解,現(xiàn)在我們來看看Java中是如何實現(xiàn)這幾種狀態(tài)的轉(zhuǎn)換的。 

          二、wait/notify/notifyAll方法的使用

          1、wait方法:

          JDK中一共提供了這三個版本的方法,
          • wait()方法的作用是將當(dāng)前運行的線程掛起(即讓其進入阻塞狀態(tài)),直到notify或notifyAll方法來喚醒線程.

          • wait(long timeout),該方法與wait()方法類似,唯一的區(qū)別就是在指定時間內(nèi),如果沒有notify或notifAll方法的喚醒,也會自動喚醒。

          • 至于wait(long timeout,long nanos),本意在于更精確的控制調(diào)度時間,不過從目前版本來看,該方法貌似沒有完整的實現(xiàn)該功能,其源碼(JDK1.8)如下:

          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?>=?500000?||?(nanos?!=?0?&&?timeout?==?0))?{
          ????????????timeout++;
          ????????}

          ????????wait(timeout);
          ????}
          從源碼來看,JDK8中對納秒的處理,只做了四舍五入,所以還是按照毫秒來處理的,可能在未來的某個時間點會用到納秒級別的精度。雖然JDK提供了這三個版本,其實最后都是調(diào)用wait(long timeout)方法來實現(xiàn)的,wait()方法與wait(0)等效,而wait(long timeout,int nanos)從上面的源碼可以看到也是通過wait(long timeout)來完成的。
          下面我們通過一個簡單的例子來演示wait()方法的使用:
          package?com.paddx.test.concurrent;

          public?class?WaitTest?{

          ????public?void?testWait(){
          ????????System.out.println("Start-----");
          ????????try?{
          ????????????wait(1000);
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????System.out.println("End-------");
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????final?WaitTest?test?=?new?WaitTest();
          ????????new?Thread(new?Runnable()?{
          ????????????@Override
          ????????????public?void?run()?{
          ????????????????test.testWait();
          ????????????}
          ????????}).start();
          ????}
          }
          這段代碼的意圖很簡單,就是程序執(zhí)行以后,讓其暫停一秒,然后再執(zhí)行。運行上述代碼,查看結(jié)果:
          Start-----
          Exception?in?thread?"Thread-0"?java.lang.IllegalMonitorStateException
          ????at?java.lang.Object.wait(Native?Method)
          ????at?com.paddx.test.concurrent.WaitTest.testWait(WaitTest.java:8)
          ????at?com.paddx.test.concurrent.WaitTest$1.run(WaitTest.java:20)
          ????at?java.lang.Thread.run(Thread.java:745)
          這段程序并沒有按我們的預(yù)期輸出相應(yīng)結(jié)果,而是拋出了一個異常。大家可能會覺得奇怪為什么會拋出異常?而拋出的IllegalMonitorStateException異常又是什么?我們可以看一下JDK中對IllegalMonitorStateException的描述:
          Thrown to indicate that a thread has attempted to wait on an object's monitor or to notify other threads waiting on an object's monitor without owning the specified monitor.
          這句話的意思大概就是:線程試圖等待對象的監(jiān)視器或者試圖通知其他正在等待對象監(jiān)視器的線程,但本身沒有對應(yīng)的監(jiān)視器的所有權(quán)。
          其實這個問題在《【68期】面試官:對并發(fā)熟悉嗎?說說Synchronized及實現(xiàn)原理》一文中有提到過,wait方法是一個本地方法,其底層是通過一個叫做監(jiān)視器鎖的對象來完成的。所以上面之所以會拋出異常,是因為在調(diào)用wait方式時沒有獲取到monitor對象的所有權(quán),那如何獲取monitor對象所有權(quán)?
          Java中只能通過Synchronized關(guān)鍵字來完成,修改上述代碼,增加Synchronized關(guān)鍵字:
          package?com.paddx.test.concurrent;

          public?class?WaitTest?{

          ????public?synchronized?void?testWait(){//增加Synchronized關(guān)鍵字
          ????????System.out.println("Start-----");
          ????????try?{
          ????????????wait(1000);
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????System.out.println("End-------");
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????final?WaitTest?test?=?new?WaitTest();
          ????????new?Thread(new?Runnable()?{
          ????????????@Override
          ????????????public?void?run()?{
          ????????????????test.testWait();
          ????????????}
          ????????}).start();
          ????}
          }
          現(xiàn)在再運行上述代碼,就能看到預(yù)期的效果了:
          Start-----
          End-------
          所以,通過這個例子,大家應(yīng)該很清楚,wait方法的使用必須在同步的范圍內(nèi),否則就會拋出IllegalMonitorStateException異常,wait方法的作用就是阻塞當(dāng)前線程等待notify/notifyAll方法的喚醒,或等待超時后自動喚醒。

          2、notify/notifyAll方法

          有了對wait方法原理的理解,notify方法和notifyAll方法就很容易理解了。既然wait方式是通過對象的monitor對象來實現(xiàn)的,所以只要在同一對象上去調(diào)用notify/notifyAll方法,就可以喚醒對應(yīng)對象monitor上等待的線程了。
          notify和notifyAll的區(qū)別在于前者只能喚醒monitor上的一個線程,對其他線程沒有影響,而notifyAll則喚醒所有的線程,看下面的例子很容易理解這兩者的差別:
          package?com.paddx.test.concurrent;

          public?class?NotifyTest?{
          ????public?synchronized?void?testWait(){
          ????????System.out.println(Thread.currentThread().getName()?+"?Start-----");
          ????????try?{
          ????????????wait(0);
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????System.out.println(Thread.currentThread().getName()?+"?End-------");
          ????}

          ????public?static?void?main(String[]?args)?throws?InterruptedException?{
          ????????final?NotifyTest?test?=?new?NotifyTest();
          ????????for(int?i=0;i<5;i++)?{
          ????????????new?Thread(new?Runnable()?{
          ????????????????@Override
          ????????????????public?void?run()?{
          ????????????????????test.testWait();
          ????????????????}
          ????????????}).start();
          ????????}

          ????????synchronized?(test)?{
          ????????????test.notify();
          ????????}
          ????????Thread.sleep(3000);
          ????????System.out.println("-----------分割線-------------");

          ????????synchronized?(test)?{
          ????????????test.notifyAll();
          ????????}
          ????}
          }
          輸出結(jié)果如下:
          Thread-0?Start-----
          Thread-1?Start-----
          Thread-2?Start-----
          Thread-3?Start-----
          Thread-4?Start-----
          Thread-0?End-------
          -----------分割線-------------
          Thread-4?End-------
          Thread-3?End-------
          Thread-2?End-------
          Thread-1?End-------
          從結(jié)果可以看出:調(diào)用notify方法時只有線程Thread-0被喚醒,但是調(diào)用notifyAll時,所有的線程都被喚醒了。
          最后,有兩點需要注意:
          1.調(diào)用wait方法后,線程是會釋放對monitor對象的所有權(quán)的。
          2.一個通過wait方法阻塞的線程,必須同時滿足以下兩個條件才能被真正執(zhí)行:
          • 線程需要被喚醒(超時喚醒或調(diào)用notify/notifyll)。

          • 線程喚醒后需要競爭到鎖(monitor)。

          三、sleep/yield/join方法解析

          上面我們已經(jīng)清楚了wait和notify方法的使用和原理,現(xiàn)在我們再來看另外一組線程間協(xié)作的方法。這組方法跟上面方法的最明顯區(qū)別是:這幾個方法都位于Thread類中,而上面三個方法都位于Object類中。至于為什么,大家可以先思考一下。現(xiàn)在我們逐個分析sleep/yield/join方法:

          1、sleep

          sleep方法的作用是讓當(dāng)前線程暫停指定的時間(毫秒),sleep方法是最簡單的方法,在上述的例子中也用到過,比較容易理解。唯一需要注意的是其與wait方法的區(qū)別。最簡單的區(qū)別是,wait方法依賴于同步,而sleep方法可以直接調(diào)用。而更深層次的區(qū)別在于sleep方法只是暫時讓出CPU的執(zhí)行權(quán),并不釋放鎖。而wait方法則需要釋放鎖。
          package?com.paddx.test.concurrent;

          public?class?SleepTest?{
          ????public?synchronized?void?sleepMethod(){
          ????????System.out.println("Sleep?start-----");
          ????????try?{
          ????????????Thread.sleep(1000);
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????System.out.println("Sleep?end-----");
          ????}

          ????public?synchronized?void?waitMethod(){
          ????????System.out.println("Wait?start-----");
          ????????synchronized?(this){
          ????????????try?{
          ????????????????wait(1000);
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????}
          ????????System.out.println("Wait?end-----");
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????final?SleepTest?test1?=?new?SleepTest();

          ????????for(int?i?=?0;i<3;i++){
          ????????????new?Thread(new?Runnable()?{
          ????????????????@Override
          ????????????????public?void?run()?{
          ????????????????????test1.sleepMethod();
          ????????????????}
          ????????????}).start();
          ????????}


          ????????try?{
          ????????????Thread.sleep(10000);//暫停十秒,等上面程序執(zhí)行完成
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????System.out.println("-----分割線-----");

          ????????final?SleepTest?test2?=?new?SleepTest();

          ????????for(int?i?=?0;i<3;i++){
          ????????????new?Thread(new?Runnable()?{
          ????????????????@Override
          ????????????????public?void?run()?{
          ????????????????????test2.waitMethod();
          ????????????????}
          ????????????}).start();
          ????????}

          ????}
          }
          執(zhí)行結(jié)果:
          Sleep?start-----
          Sleep?end-----
          Sleep?start-----
          Sleep?end-----
          Sleep?start-----
          Sleep?end-----
          -----分割線-----
          Wait?start-----
          Wait?start-----
          Wait?start-----
          Wait?end-----
          Wait?end-----
          Wait?end-----
          這個結(jié)果的區(qū)別很明顯,通過sleep方法實現(xiàn)的暫停,程序是順序進入同步塊的,只有當(dāng)上一個線程執(zhí)行完成的時候,下一個線程才能進入同步方法,sleep暫停期間一直持有monitor對象鎖,其他線程是不能進入的。而wait方法則不同,當(dāng)調(diào)用wait方法后,當(dāng)前線程會釋放持有的monitor對象鎖,因此,其他線程還可以進入到同步方法,線程被喚醒后,需要競爭鎖,獲取到鎖之后再繼續(xù)執(zhí)行。

          2、yield方法

          yield方法的作用是暫停當(dāng)前線程,以便其他線程有機會執(zhí)行,不過不能指定暫停的時間,并且也不能保證當(dāng)前線程馬上停止。yield方法只是將Running狀態(tài)轉(zhuǎn)變?yōu)镽unnable狀態(tài)。我們還是通過一個例子來演示其使用:
          package?com.paddx.test.concurrent;

          public?class?YieldTest?implements?Runnable?{
          ????@Override
          ????public?void?run()?{
          ????????try?{
          ????????????Thread.sleep(100);
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????????for(int?i=0;i<5;i++){
          ????????????System.out.println(Thread.currentThread().getName()?+?":?"?+?i);
          ????????????Thread.yield();
          ????????}
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????YieldTest?runn?=?new?YieldTest();
          ????????Thread?t1?=?new?Thread(runn,"FirstThread");
          ????????Thread?t2?=?new?Thread(runn,"SecondThread");

          ????????t1.start();
          ????????t2.start();

          ????}
          }
          運行結(jié)果如下:
          FirstThread:?0
          SecondThread:?0
          FirstThread:?1
          SecondThread:?1
          FirstThread:?2
          SecondThread:?2
          FirstThread:?3
          SecondThread:?3
          FirstThread:?4
          SecondThread:?4
          這個例子就是通過yield方法來實現(xiàn)兩個線程的交替執(zhí)行。不過請注意:這種交替并不一定能得到保證,源碼中也對這個問題進行說明:
          /**
          ?????*?A?hint?to?the?scheduler?that?the?current?thread?is?willing?to?yield
          ?????*?its?current?use?of?a?processor.?The?scheduler?is?free?to?ignore?this
          ?????*?hint.
          ?????*
          ?????*?

          ?Yield?is?a?heuristic?attempt?to?improve?relative?progression
          ?????*?between?threads?that?would?otherwise?over-utilise?a?CPU.?Its?use
          ?????*?should?be?combined?with?detailed?profiling?and?benchmarking?to
          ?????*?ensure?that?it?actually?has?the?desired?effect.
          ?????*
          ?????*?

          ?It?is?rarely?appropriate?to?use?this?method.?It?may?be?useful
          ?????*?for?debugging?or?testing?purposes,?where?it?may?help?to?reproduce
          ?????*?bugs?due?to?race?conditions.?It?may?also?be?useful?when?designing
          ?????*?concurrency?control?constructs?such?as?the?ones?in?the
          ?????*?{@link?java.util.concurrent.locks}?package.
          */

          這段話主要說明了三個問題:
          • 調(diào)度器可能會忽略該方法。

          • 使用的時候要仔細分析和測試,確保能達到預(yù)期的效果。

          • 很少有場景要用到該方法,主要使用的地方是調(diào)試和測試。  

          3、join方法

          join方法的作用是父線程等待子線程執(zhí)行完成后再執(zhí)行,換句話說就是將異步執(zhí)行的線程合并為同步的線程。JDK中提供三個版本的join方法,其實現(xiàn)與wait方法類似,join()方法實際上執(zhí)行的join(0),而join(long millis, int nanos)也與wait(long millis, int nanos)的實現(xiàn)方式一致,暫時對納秒的支持也是不完整的。我們可以看下join方法的源碼,這樣更容易理解:
          public?final?void?join()?throws?InterruptedException?{
          ????????join(0);
          ????}

          ?public?final?synchronized?void?join(long?millis)
          ????throws?InterruptedException?
          {
          ????????long?base?=?System.currentTimeMillis();
          ????????long?now?=?0;

          ????????if?(millis?0)?{
          ????????????throw?new?IllegalArgumentException("timeout?value?is?negative");
          ????????}

          ????????if?(millis?==?0)?{
          ????????????while?(isAlive())?{
          ????????????????wait(0);
          ????????????}
          ????????}?else?{
          ????????????while?(isAlive())?{
          ????????????????long?delay?=?millis?-?now;
          ????????????????if?(delay?<=?0)?{
          ????????????????????break;
          ????????????????}
          ????????????????wait(delay);
          ????????????????now?=?System.currentTimeMillis()?-?base;
          ????????????}
          ????????}
          ????}

          public?final?synchronized?void?join(long?millis,?int?nanos)
          ????throws?InterruptedException?
          {

          ????????if?(millis?0)?{
          ????????????throw?new?IllegalArgumentException("timeout?value?is?negative");
          ????????}

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

          ????????if?(nanos?>=?500000?||?(nanos?!=?0?&&?millis?==?0))?{
          ????????????millis++;
          ????????}

          ????????join(millis);
          ????}
          大家重點關(guān)注一下join(long millis)方法的實現(xiàn),可以看出join方法就是通過wait方法來將線程的阻塞,如果join的線程還在執(zhí)行,則將當(dāng)前線程阻塞起來,直到j(luò)oin的線程執(zhí)行完成,當(dāng)前線程才能執(zhí)行。
          不過有一點需要注意,這里的join只調(diào)用了wait方法,卻沒有對應(yīng)的notify方法,原因是Thread的start方法中做了相應(yīng)的處理,所以當(dāng)join的線程執(zhí)行完成以后,會自動喚醒主線程繼續(xù)往下執(zhí)行。下面我們通過一個例子來演示join方法的作用:
          (1)不使用join方法:
          package?com.paddx.test.concurrent;

          public?class?JoinTest?implements?Runnable{
          ????@Override
          ????public?void?run()?{

          ????????try?{
          ????????????System.out.println(Thread.currentThread().getName()?+?"?start-----");
          ????????????Thread.sleep(1000);
          ????????????System.out.println(Thread.currentThread().getName()?+?"?end------");
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????for?(int?i=0;i<5;i++)?{
          ????????????Thread?test?=?new?Thread(new?JoinTest());
          ????????????test.start();
          ????????}

          ????????System.out.println("Finished~~~");
          ????}
          }
          執(zhí)行結(jié)果如下:
          Thread-0?start-----
          Thread-1?start-----
          Thread-2?start-----
          Thread-3?start-----
          Finished~~~
          Thread-4?start-----
          Thread-2?end------
          Thread-4?end------
          Thread-1?end------
          Thread-0?end------
          Thread-3?end------
          (2)使用join方法:
          package?com.paddx.test.concurrent;

          public?class?JoinTest?implements?Runnable{
          ????@Override
          ????public?void?run()?
          {

          ????????try?{
          ????????????System.out.println(Thread.currentThread().getName()?+?"?start-----");
          ????????????Thread.sleep(1000);
          ????????????System.out.println(Thread.currentThread().getName()?+?"?end------");
          ????????}?catch?(InterruptedException?e)?{
          ????????????e.printStackTrace();
          ????????}
          ????}

          ????public?static?void?main(String[]?args)?{
          ????????for?(int?i=0;i<5;i++)?{
          ????????????Thread?test?=?new?Thread(new?JoinTest());
          ????????????test.start();
          ????????????try?{
          ????????????????test.join();?//調(diào)用join方法
          ????????????}?catch?(InterruptedException?e)?{
          ????????????????e.printStackTrace();
          ????????????}
          ????????}

          ????????System.out.println("Finished~~~");
          ????}
          }
          執(zhí)行結(jié)果如下:
          Thread-0?start-----
          Thread-0?end------
          Thread-1?start-----
          Thread-1?end------
          Thread-2?start-----
          Thread-2?end------
          Thread-3?start-----
          Thread-3?end------
          Thread-4?start-----
          Thread-4?end------
          Finished~~~
          對比兩段代碼的執(zhí)行結(jié)果很容易發(fā)現(xiàn),在沒有使用join方法之間,線程是并發(fā)執(zhí)行的,而使用join方法后,所有線程是順序執(zhí)行的。

          四、總結(jié)

          本文主要詳細講解了wait/notify/notifyAll和sleep/yield/join方法。最后回答一下上面提出的問題:wait/notify/notifyAll方法的作用是實現(xiàn)線程間的協(xié)作,那為什么這三個方法不是位于Thread類中,而是位于Object類中?位于Object中,也就相當(dāng)于所有類都包含這三個方法(因為Java中所有的類都繼承自O(shè)bject類)。
          要回答這個問題,還是得回過來看wait方法的實現(xiàn)原理,大家需要明白的是,wait等待的到底是什么東西?如果對上面內(nèi)容理解的比較好的話,我相信大家應(yīng)該很容易知道wait等待其實是對象monitor,由于Java中的每一個對象都有一個內(nèi)置的monitor對象,自然所有的類都理應(yīng)有wait/notify方法。

          推薦閱讀:

          【68期】面試官:對并發(fā)熟悉嗎?說說Synchronized及實現(xiàn)原理

          【67期】談?wù)凜oncurrentHashMap是如何保證線程安全的?

          【65期】Spring的IOC是啥?有什么好處?

          5T技術(shù)資源大放送!包括但不限于:C/C++,Linux,Python,Java,PHP,人工智能,單片機,樹莓派,等等。在公眾號內(nèi)回復(fù)「2048」,即可免費獲取!!

          微信掃描二維碼,關(guān)注我的公眾號

          朕已閱?

          瀏覽 16
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  一区二区三区四区精品在线 | 成人无码区免费A片在线软件 | 国产传媒天美果冻精品亚洲一区二区三区直播 | 国产精品视频导航 | 日韩精品淫秽视频 |