併發編程實戰學習筆記(十)-構建自定義的同步工具

併發編程實戰學習筆記(十)-構建自定義的同步工具

核心概念【狀態依賴】

程序在做某一個操作之前,需要依賴另一個操作的完成或者狀態的就緒,這樣的一種關係就叫做“狀態依賴”。

狀態依賴的實現類,就是併發工具的原語。

例如FutureTask、Semaphore和BlockingQueue等。在這些類的一些操作中有着基於狀態的前提條件,例如,不能從一個空隊列刪除元素,或者獲取一個尚未結束的任務的計算結果,在這些操作可以執行之前,必須等待隊列進入“非空”狀態,或者任務進入“已完成”狀態。

實現了狀態依賴的底層類有:內置的條件隊列、顯示的Condition對象以及AbstractQueuedSynchronizer框架。

併發機制原語,包含有兩個

  • 原子操作,最終的操作才能保證線程安全性。

鎖機制或者直接使用原子類都能實現應用程序中的原子操作,但原子操作的原語則不能依賴鎖機制,必須依賴於操作系統本身才行,否則會陷入先有雞還是先有蛋的悖論,因爲鎖機制原語本身是有依賴原子操作的。

  • 阻塞線程,並能在依賴狀態滿足時即時喚醒。

阻塞與喚醒、自旋都是實現狀態依賴的思路。

條件隊列

概念

它合得一組線程(稱之爲等待線程集合)能夠通過某種方式來等待特定的條件變爲真。傳統的隊列是一個個數據,而與之不同的是,條件隊列中的元素是一個個正在等待相關條件的線程。

要點

  • Object中的wait、notify和notifyAll方法構成了內部條件隊列的API。

  • 對象的內置鎖與內部條件是相互關聯的,要調用對象X中的條件隊列的任何一個方法,必須持有對象X上的鎖。

這是因爲“等待由狀態構成的條件”與“維護狀態一致性”這兩種機制必須被緊密地綁定在一起。只有能對狀態進行檢查時,才能在某個條件上等待,並能只有能修改狀態時,才能從條件等待中釋放另一個線程。

  • Object.wait會自動釋放鎖,並請求操作系統掛起當前線程,從而使其它線程能夠獲得這個鎖並修改對象的狀態。當被掛起的線路醒來時,它將在返回之前重新獲取鎖。(需要重新競爭,並沒有優先獲取權)

條件謂詞

概念

條件謂詞是使某個操作成爲狀態依賴操作的前提條件。在有界緩存中,只有當緩存不爲空時,take方法才能執行,否則必須等待。對take方法來說,它的條件謂詞就是“緩存不爲空”,take方法在執行之前必須首先測試該條件謂詞。

條件謂詞與條件隊列的關係

每一次wait調用都會隱式地與特定的條件謂詞關聯起來。當調用某個特定條件謂詞的wait時,調用者必須已經持有與條件隊列相關的鎖,並且這個鎖必須保護着構成條件謂詞的狀態變量。

當使用條件等待時要滿足的條件(Object.wait或Condition.wait)

  • 通常都有一個條件謂詞——包括一些對象狀態的測試,線程在執行前必須首先通過這些測試。
  • 在調用wait之前測試條件謂詞,並且從wait中返回時再次進行測試。
  • 在一個循環中調用wait。
  • 確保使用與條件隊列相關的鎖來保護構成條件謂詞的各個狀態變量。
  • 當調用wait/notify/notifyAll等方法時,一定要持有與條件隊列相關的鎖。
  • 在檢查條件謂詞之後以及開始執行相應的操作之前,不要釋放鎖。

丟失的信號

notify或者notifyAll操作發生在wait之前,就會造成通知信號的丟失,最終wait永遠都得不到恢復或者不得不等待下一次重新通知而延遲了恢復時間。

通知

注意事項:發出通知的線程應該儘快地釋放鎖,從而確保正在等待的線程儘可能快地解除阻塞。如果這些等待中線程此時不能重新獲得鎖,那麼無法從wait返回。

優先選擇notifyAll而不是單個的notify的原因

由於多個線程可以基於不同的條件謂詞在同一個條件隊列上等待,因此如果使用notify而不是notifyAll,那麼將是一個危險的操作,因爲單一的通知很容易導致類似信號丟失的問題。爲什麼說是類似,因爲導致的問題是相同的:線程正在等待一個已經(或者本應該)發生過的信號。

使用單一的notify而不是notifyAll的條件

  • 所有等待線程的類型都相同。只有一個條件謂詞與條件隊列相關,並且每個線程在從wait返回後將執行相同的操作。
  • 單進單出。在條件變量上的每次通知,最多隻能喚醒一個線程來執行。

子類的安全問題

對於狀態依賴的類,要麼將其等待和通知等協議完全向子類公開(並且寫入正式文檔),要麼完全阻止子類參與到等待與通知等過程中。

這是對“要麼圍繞着繼承來設計和文檔化,要麼禁止使用繼承”這條規則的一種擴展【EJ ITEM 15】

顯示Condition對象

內置條件隊列的侷限性

每個內置鎖都只能有一個相關聯的條件隊列,因而在像BoundBuffer這種類中,多個線程可能在同一個條件隊列上等待不同的條件謂詞,並且在最常見的加鎖模式下公開條件隊列對象。

顯示條件隊列的優勢

  • 可以編寫一個帶有多個條件謂詞的併發對象,或者獲得除了條件隊列可見性之外的更多控制權,這是一種靈活的選擇。
  • 對於每個Lock,可以有任意數量的Condition對象。Condition對象繼承了相關的Lock對象的公平性,對於公平的鎖,線程會依照FIFO順序從Condition await中釋放。

特別注意:Condition對象中,三個與條件隊列相關的API是:await,signal,signalAll。不要使用錯了。

AbstractQueuedSynchronizer

  • 無論是獲取和釋放鎖、信號量等,它們的實現都可以抽象爲狀態依賴的“獲取”和“釋放”的操作。這也是AQS實現的理論基礎。
  • JDK中大部分的併發工具類,都是基於AQS來實現的。使用AQS來構造工具類是使用複合而不是繼承的方式,這樣可以保護AQS的脆弱實現。

關於AQS底層原理的研究

AQS中對於併發狀態操作最核心的原語,如線程掛起喚醒、原子操作等,都是通過LockSupport(最終由sun.misc.Unsafe中的native方法)提供。

Unsafe中提供對內存直接操作、序列化、併發原語、CAS操作、線程掛起與喚醒等,不建議在我們自己的代碼中使用。

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