線程間通信

線程和線程之間不是獨立的個體,它們彼此之間可以互相通信和協作。

線程通信就是在線程之間傳遞信息,保證他們能夠協同工作。在線程間進行通信後,系統之間的交互性會更強大,在大大提高CPU利用率的同時還會使程序員對各線程任務的處理過程進行有效的把控與監督。

1.等待/通知機制

(1)不使用等待/通知實現線程間通信

如果不使用線程間的通信機制,兩個線程想要根據同一個數據實現通信的話,就必須使用while( )輪詢的方式獲取共享數據的值。 

比如:兩個線程A和B,共享同一個int型數據i,i的初始值是0,線程A對i執行循環加一的操作,線程B在i等於5的時候執行一些操作,那麼線程B怎麼才能獲取到i的值呢?

線程B就必須不停的通過while( )循環語句輪詢的來檢測i的值。這樣雖然兩個線程間實現了通信,但是會浪費CPU資源。

如果輪詢的時間間隔很小,更浪費CPU資源;如果輪詢的時間間隔很大,有可能會取不到想要得到的數據。就需要有一種機制來減少CPU的資源浪費,而且還可以實現在多個線程間通信,滿足這些條件的就是“wait/notify”機制。

(2)等待/通知機制

等待/通知機制中將線程分爲兩種:等待線程和通知線程,等待線程的執行需要依靠對應的通知線程把相應的方法執行完畢,返回對應值給等待線程時等待線程才能執行,以此進行線程之間的通信和合作,在這個機制中一個線程的執行需要依靠其他線程。

等待/通知機制使用wait( )/notify( )方法實現。wait( )和notify( )都是Object類的方法,在調用wait( )和notify( )方法之前,線程必須獲得該對象的對象級別鎖,即只能在同步方法和同步塊中調用wait( )和notify( )方法,

wait( )是使當前執行代碼的進程等待,wait( )方法是Object類的方法,該方法用來將當前線程置入“預執行隊列”,並在wait( )所在的代碼行停止執行,直到接到通知或被中斷爲止。在執行wait( )方法後,當前線程釋放鎖。

notify( )方法用來通知那些可能等待該對象對應的對象鎖的其他線程,如果有多個線程等待,則由線程規劃器隨機挑選出其中一個呈wait狀態的線程,對其發出通知notify,並使它等待獲取該對象的對象鎖。notify( )方法僅通知一個線程。

notifyAll( )方法可以使所有在阻塞狀態等待某個對象鎖的線程從阻塞狀態退出,進入就緒狀態。但這些線程有時候是隨機執行,有時候就是優先級最高的那個線程最先執行,取決於JVM虛擬機的實現。

需要說明的是,在執行notify( )方法後,當前線程不會馬上釋放該對象鎖,呈wait狀態的線程也並不能馬上獲取該對象鎖,要等到執行notify( )方法的線程將程序執行完,也就是退出synchronized代碼塊後,當前線程才能釋放鎖,而呈wait狀態所在的線程纔可以獲取該對象鎖。

用一句話來總結wait和notify:wait使線程停止運行,而notify使停止的線程繼續運行。

調用wait( )方法的線程釋放對象鎖,然後從運行狀態退出,進入阻塞狀態;調用notify( )方法喚醒因調用wait( )方法而處於阻塞狀態的線程,進入就緒狀態。如果發出notify操作時沒有處於阻塞狀態中的線程,就是沒有調用wait( )方法的線程,那麼該命令會被忽略。

wait( )方法和notify( )方法一起使用,他們釋放和獲得的對象鎖也要對應。一個持有對象鎖的notify( )方法只能釋放執行wait( )方法等待該對象鎖的線程,而不能釋放執行wait( )方法等待其他對象鎖的線程。

在之前有介紹過和線程狀態有關的方法,這些方法可以改變線程對象的狀態,線程狀態和他們之間的轉換關係和對應的線程狀態轉換方法如圖:

在這張圖中,運行包括運行態和就緒態, 暫停就是阻塞態。

每個鎖對象都有兩個隊列,一個就緒隊列,一個阻塞隊列。就緒隊列存儲了將要獲得對象鎖的線程,阻塞隊列存儲了被阻塞的線程。一個線程被喚醒後,纔會進入就緒隊列,等待CPU的調度;反之,一個線程被wait( )後,就會進入阻塞隊列,等待下一次被喚醒。

(3)方法wait鎖釋放與notify鎖不釋放

當方法wait( )執行後,鎖被自動釋放。但執行完notify( )方法,鎖卻不自動釋放,必須執行完notify( )方法所在的同步synchronized代碼塊後才釋放鎖。

(4)當線程呈wait狀態時,調用對象線程的interrupt( )方法會出現InterruptedException異常。

在執行同步代碼塊的過程中,遇到異常而導致線程終止,鎖也會被釋放。

(5)如果有多個線程執行wait( )方法,調用notify( )方法一次只隨機通知一個線程進行喚醒。

(6)調用notifyAll( )方法喚醒所有線程。

如果有多個線程執行wait( )方法,可以多次使用notify( )方法喚醒這些線程,但是可能存在有的線程沒被喚醒,要想喚醒所有的線程,調用notifyAll( )方法喚醒所有等待的線程。

(7)使用wait(long)方法自動喚醒線程

帶一個參數wait(long)方法的功能是等待指定的時間,在某一時間內看是否有線程對鎖進行喚醒,如果超過這個時間還沒有被喚醒,則不需要使用notity( )方法來喚醒而是自動喚醒。也可以由notify( )方法喚醒,如果執行notify( )方法喚醒執行wait(long)方法的線程執行的時間比執行wait(long)方法的線程自動喚醒本身花的時間要短,則執行wait(long)方法的線程就不用等待參數中規定的那麼長時間而是notify( )方法執行完之後就可以被喚醒。

(8)生產者/消費者模式實現

爲了更好的解決多個交互線程之間的運行速度匹配問題(即同步問題),Java引入了多線程同步模型的概念。等待/通知模式是多線程同步模型的一種。等待/通知模式最經典的案例就是“生產者/消費者模式”,原理基於wait/notify。

當一個生產者和一個消費者共享一個對象鎖的時候他們能很好的交替運行。

但是當出現一個生產者多個消費者或多個生產者多個消費者的情況時,就會可能會出現程序的“假死”,“假死”就是死鎖。程序“假死”現象就是所有的線程都處於WAITING的狀態,程序的執行無法向前推進的一種狀態。

出現這種情況的原因是多個生產者和多個消費者共享一個對象鎖,那麼當有線程調用notify( )方法喚醒一個線程時,喚醒的線程不僅有可能是異類,還有可能是同類,比如生產者調用notify( )方法不僅可以喚醒等待中的消費者,還可以喚醒生產者,消費者調用notify( )方法不僅可以喚醒等待中的生產者,還可以喚醒消費者,因爲他們持有的對象鎖都是一樣的。如果喚醒同類的情況越來越多,就會有越來越多的同類被喚醒後又被阻塞,異類也沒有被喚醒的機會,結果就是導致被阻塞的線程越來越多,最終可能導致所有的線程都處於等待狀態而造成“假死”。

解決的方法就是在每個線程釋放正在等待該對象鎖的其他線程時使用notifyAll( )方法取代notify( )方法,這樣每次都能喚醒所有線程而不會出現阻塞。

(9)通過管道進行線程間通信:字節流

在Java語言中提供了各種各樣的輸入/輸出流Stream,可以使我們很方便的能夠對數據進行操作,其中管道流是一種特殊的流,用於在不同線程之間直接傳遞數據。一個線程發送數據到輸出管道,另一個線程從輸入管道中讀數據。通過使用管道,實現不同線程間的通信。

在Java的JDK中提供了4個類來使線程間可以進行通信:

<1>PipedInputStreamPipedOutputStream,這是基於字節流。

<2>PipedReaderPipedWriter,這是基於字符流。

(10)通過管道進行線程間通信:字符流

在管道中可以傳遞字節流,也可以傳遞字符流。可以在兩個線程中通過管道流進行字符數據的傳輸。

(11)方法join( )的使用

方法join( )在和線程狀態有關的方法有講過它的基本使用。它可以使調用該方法的線程先執行。一個可以使用的場景就是主線程想等待子線程執行完之後再執行,比如子線程處理一個數據,主線程要取得這個數據中的值,就可以使用join( )方法。join( )方法的作用是當前線程等待調用該方法的線程結束後,再排隊等待CPU資源。

方法join( )具有使線程排隊運行的效果,有些類似同步的運行效果。join和synchronized的區別是:join在內部使用wait( )方法進行等待,而synchronized關鍵字使用的是“對象監視器”原理作爲同步。

方法join(long) 和sleep(long)的區別

方法join(long)的功能是在內部使用wait(long)方法實現的,所以join(long)具有釋放鎖的特點。

在執行wait(long)方法後,當前線程的鎖被釋放,那麼其他線程就可以調用此線程中的同步方法了。而Thread.sleep(long)方法卻具有不釋放鎖的特點。

參考:《Java多線程編程核心技術》 --- 高洪巖(Chapter3 3.1 3.2)

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