並發學習筆記13-執行緒基礎(下)
- 2020 年 1 月 22 日
- 筆記
執行緒間的通訊
vloatile和synchronized關鍵字
Volatile可以修飾欄位(成員變數),就是告知程式任何對該變數的訪問均需從共享記憶體中獲取,而對它的改變必須同步刷新回共享記憶體,它能保證所有執行緒對變數訪問的可見性。但是過多的使用volatile是不必要的,因為會降低程式執行的效率。
關鍵字synchronized可以修飾方法或以同步塊的形式來使用,它主要確保多個執行緒在同一時刻,只能有一個執行緒處於方法或同步塊中, 保證了執行緒對變數訪問的可見性和排他性。
同步塊的實現使用了monitorenter和monitorexit指令,而同步方法則依靠方法修飾符上的ACC_SYNCHRONIZED來完成。無論採用哪種方式,本質是對一個對象的監視器(monitor)進行獲取,而這個獲取過程是排他的,即同一個時刻只能有一個執行緒獲取到由synchronized所保護對象的監視器。
每個對象都有自己的監視器,任意執行緒對由synchronized保護的Object的訪問,首先要獲取Objcet的監視器。若獲取失敗,執行緒進入同步隊列執行緒狀態變為BLOCKED。當訪問Objcet的前驅(獲取了鎖的執行緒)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的執行緒,使其重新嘗試對監視器的獲取。
等待/通知機制
一個執行緒修改了一個對象的值,而另一個執行緒感知到了變化,然後進行相應的操作,整個過程開始於一個執行緒,最終執行的是另一個執行緒。前者是生產者,後者是消費者。這種模式隔離了「做什麼」和「怎麼做」,在功能上實現了解耦,體系結構上具備了良好的伸縮性。
Java實現中簡單的方法時讓消費者執行緒不斷地循環檢查變數是否符合預期,如:
while(value != desire) { Thread.sleep(1000); } ......上述偽程式碼在不符合條件時就睡眠一段時間,這樣做的目的是防止過快的「無效」嘗試,這種方式看似能實現所需的功能,但存在如下問題:
- 難以確保及時性。在睡眠時,基本不消耗處理器資源,但如果睡得過久,就不能及時發現條件已經改變。
- 難以降低開銷。若降低睡眠時間,消費者能更快發現條件變化,但會消耗更多的處理器資源,造成無端浪費,
上面兩個問題看似矛盾,但Java通過內置的等待/通知機制能夠很好地解決這個矛盾並實現所需功能。
等待/通知的相關方法是任意對象都具備的,因為其定義在Object上,相關描述如下:
方法名稱 | 描述 |
|---|---|
notify() | 通知一個在對象上等待的執行緒,使其從wait()方法返回,而返回的前提是該執行緒獲取到了對象的鎖。 |
notifyAll() | 通知所有等待在該對象上的執行緒。 |
wait() | 調用該方法的執行緒進入WAITING狀態,只有等待另外的執行緒的通知或被中斷才會返回,需要注意,調用wait()方法後會釋放對象的鎖。 |
wait(long) | 超時等待一段時間,這裡的參數時間時毫秒。等待n毫秒若沒通知就超時返回。 |
wait(long,int) | 對於超時時間更細膩的控制,可達到納秒。 |
等待/通知機制是指一個執行緒A調用了對象O的wait()方法進入等待狀態,而另一個執行緒B調用了對象O的notify()或notifyAll()方法,執行緒A收到通知後從對象O的wait()方法返回,進而執行後續操作。
調用wait、notify、notifyAll需要注意一些細節:
- 使用wait()、notify()、notifyAll()時需要先對調用對象加鎖。
- 調用wait()方法後,執行緒狀態由RUNNING變為WAITING,並將當前執行緒放置到對象的等待隊列。
- notify()或notifyAll()方法調用後,等待執行緒依舊不會從wait()方法返回,需要調用notify()或notifyAll()的執行緒釋放鎖之後,才有機會從wait()返回。
- notify()方法將等待隊列中的一個等待執行緒中等待隊列中移到同步隊列,被移動的執行緒狀態由WAITING變為BLOCKED。
- 從wait()返回的前提是獲取了調用對象的鎖。
從上述可看到,等待/通知機制依託於同步機制,其目的就是確保等待執行緒從wait()方法返回時能夠感知到通知執行緒對變數做出的修改。
等待/通知的經典範式
該範式分為兩部分,分別針對等待方(消費者)和通知方(生產者)。
等待方遵守如下規則:
- 獲取對象的鎖。
- 如果條件不滿足,那麼調用對象的wait()方法,被通知後仍要檢查條件。
- 條件滿足則執行對應的邏輯。
通知方遵守如下原則:
- 獲取對象的鎖。
- 改變條件。
- 通知所有等待在對象上的執行緒。
管道輸入/輸出流
管道輸入/輸出流和普通文件的輸入/輸出流或網路輸入/輸出流不同之處在於,它主要用於執行緒之間的數據傳輸,而傳輸的媒介為記憶體。
管道輸入/輸出流主要包括了如下4種具體實現:PipedOutputStrem、PipedInputStrem、PipedReader和PipedWriter,前兩種面向位元組,後兩種面向字元。
對於Piped類型的流,必須先要進行綁定,也就是調用connect()方法,如果沒有將輸入/輸出流綁定起來,對於該流的訪問將會拋出異常。
Thread.join()的使用
如果一個執行緒A執行了thread.join()語句,其含義是:當前執行緒A等待thread執行緒終止之後才從thread.join()返回。執行緒還提供了join(long millis)和join(long millis, int naons)兩個具有超時特性的方法。其表示若執行緒thread在超時時間裡沒有返回,將會從該超時方法中返回默認結果。
每個執行緒擁有前一個執行緒的引用,需等待前一個執行緒終止,才能從等待中返回。每個執行緒終止的前提是前驅執行緒的終止,等待終止後,才從join()方法返回。
當執行緒終止時,會調用執行緒自身的notifyAll()方法,會通知所有等待執行緒在該執行緒對象上的執行緒。join()方法的邏輯結構與等待/通知經典範式一直,即加鎖、循環、處理邏輯3個步驟。
ThreadLocal的使用
ThreadLocal即是執行緒變數,是一個以ThreadLocal對象為鍵,任意對象為值的存儲結構。該結構被附帶在執行緒上,即一個執行緒可以根據一個ThreadLocal對象查詢到綁定在這個執行緒上的一個值。
可以通過set(T)方法來設置一個值,在當前執行緒調用get()方法獲取到原先設置的值。
執行緒應用實例
等待超時模式
超時等待模式就是在等待/通知範式基礎上增加了超時控制,這使得該模式相比原因範式更具有靈活性,因為即使方法執行時間長,也不會「永久」阻塞調用者,而是會按照調用者的要求「按時」返回。
可以用該模式來構建一個簡單的資料庫連接池。
執行緒池技術預先創建了若干數量的執行緒,並且不能由用戶直接對執行緒的創建進行控制,在這個前提下重複使用固定或較為固定數目的執行緒來完成任務的執行。這樣做的好處是:
- 一方面,消除了頻繁創建和消亡執行緒的系統資源開銷。
- 另一方面,面對過量任務的提交能夠平緩的劣化。
執行緒池的本質是:
- 使用了一個執行緒安全的工作隊列連接工作者執行緒和客戶端執行緒。
- 客戶端執行緒將任務放入工作隊列後便返回,而工作者執行緒則不斷地從工作隊列上取出工作並執行。
- 當工作隊列為空時,所有的工作者執行緒均等待在工作隊列上,當有客戶端提交了一個任務之後會通知任意一個工作者執行緒。
- 隨著大量的任務被提交,更多的工作者執行緒會被喚醒。
執行緒池中執行緒的具體數量需要評估每個任務的處理時間,以及當前電腦的處理器能力和數量。過少無法發揮處理器的性能,過多會增加系統的無故開銷,從而起到反作用。

