當前位置:編程學習大全網 - 編程語言 - 14

14

Java sdk 並發包內容很多,保羅萬象,但是最核心的還是對管程的實現,理論上妳可以利用管程實現所有並發包裏的工具。並發編程的兩大核心問題:壹個是互斥,同壹時刻只允許壹個線程訪問***享資源,另壹個是同步,即線程之間的同步、協作。這兩大問題,管程都是可以解決的。 Java SDK 並發包通過 Lock 和 Condition 兩個接口來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題

妳也許曾經聽到過很多這方面的傳說,例如在 Java 的 1.5 版本中,synchronized 性能不如 SDK 裏面的 Lock,但 1.6 版本之後,synchronized 做了很多優化,將性能追了上來,所以 1.6 之後的版本又有人推薦使用 synchronized 了。那性能是否可以成為“重復造輪子”的理由呢?顯然不能。因為性能問題優化壹下就可以了,完全沒必要“重復造輪子”。

到這裏,關於這個問題,妳是否能夠想出壹條理由來呢?如果妳細心的話,也許能想到壹點。那就是我們前面在介紹死鎖問題的時候,提出了壹個破壞不可搶占條件方案,但是這個方案 synchronized 沒有辦法解決。原因是 synchronized 申請資源的時候,如果申請不到,線程直接進入阻塞狀態了,而線程進入阻塞狀態,啥都幹不了,也釋放不了線程已經占有的資源。但我們希望的是:

如果我們重新設計壹把互斥鎖去解決這個問題,那該怎麽設計呢?我覺得有三種方案。

1、能夠響應中斷。synchronized 的問題是,持有鎖 A 後,如果嘗試獲取鎖 B 失敗,那麽線程就進入阻塞狀態,壹旦發生死鎖,就沒有任何機會來喚醒阻塞的線程。但如果阻塞狀態的線程能夠響應中斷信號,也就是說當我們給阻塞的線程發送中斷信號的時候,能夠喚醒它,那它就有機會釋放曾經持有的鎖 A。這樣就破壞了不可搶占條件了。

2、支持超時。如果線程在壹段時間之內沒有獲取到鎖,不是進入阻塞狀態,而是返回壹個錯誤,那這個線程也有機會釋放曾經持有的鎖。這樣也能破壞不可搶占條件。

3、非阻塞地獲取鎖。如果嘗試獲取鎖失敗,並不進入阻塞狀態,而是直接返回,那這個線程也有機會釋放曾經持有的鎖。這樣也能破壞不可搶占條件。

這三種方案可以全面彌補 synchronized 的問題。到這裏相信妳應該也能理解了,這三個方案就是“重復造輪子”的主要原因,體現在 API 上,就是 Lock 接口的三個方法。詳情如下:

那 Java SDK 裏面 Lock 靠什麽保證可見性呢?例如在下面的代碼中,線程 T1 對 value 進行了 +=1 操作,那後續的線程 T2 能夠看到 value 的正確結果嗎?

答案必須是肯定的,Java sdk 裏面鎖的實現非常復雜。它是利用了 volatile 相關的 Happens-Before 規則。Java SDK 裏面的 ReentrantLock,內部持有壹個 volatile 的成員變量 state,獲取鎖的時候,會讀寫 state 的值;解鎖的時候,也會讀寫 state 的值(簡化後的代碼如下面所示)。也就是說,在執行 value+=1 之前,程序先讀寫了壹次 volatile 變量 state,在執行 value+=1 之後,又讀寫了壹次 volatile 變量 state。根據相關的 Happens-Before 規則:

順序性規則:對於線程 T1,value+=1 Happens-Before 釋放鎖的操作 unlock();

volatile 變量規則:由於 state = 1 會先讀取 state,所以線程 T1 的 unlock() 操作 Happens-Before 線程 T2 的 lock() 操作;

傳遞性規則:線程 T1 的 value+=1 Happens-Before 線程 T2 的 lock() 操作。

所以說,後續線程 T2 能夠看到 value 的正確結果。如果妳覺得理解起來還有點困難,建議妳重溫壹下前面我們講過的《02 | Java 內存模型:看 Java 如何解決可見性和有序性問題》裏面的相關內容。

細心觀察,我們創建鎖的Juin類名,ReentrantLock,翻譯可重入鎖,所謂可重入鎖就是指線程可以重復獲取同壹把鎖。當線程 T1 執行到 ① 處時,已經獲取到了鎖 rtl ,當在 ① 處調用 get() 方法時,會在 ② 再次對鎖 rtl 執行加鎖操作。此時,如果鎖 rtl 是可重入的,那麽線程 T1 可以再次加鎖成功;如果鎖 rtl 是不可重入的,那麽線程 T1 此時會被阻塞。

除了可重入鎖,可能妳還聽說過可重入函數,可重入函數怎麽理解呢?指的是線程可以重復調用?顯然不是,所謂可重入函數,指的是多個線程可以同時調用該函數,每個線程都能得到正確結果;同時在壹個線程內支持線程切換,無論被切換多少次,結果都是正確的。多線程可以同時執行,還支持線程切換,這意味著什麽呢?線程安全啊。所以,可重入函數是線程安全的。

前面介紹過入口等待隊列,鎖都對應著壹個等待隊列,如果壹個線程沒有獲得鎖,就會進入等待隊列,當有線程釋放的時候,就需要從等待隊列中獲取壹個等待的線程,如果是公平鎖,那麽就是誰等待的時間長就喚醒誰,如果是非公平鎖,則不提供這個保證。有可能等待時間短的線程被喚起。

妳已經知道,用鎖雖然能解決很多並發問題,但是風險也是很高,可能會導致死鎖,也可能影響性能,是否有最佳的實踐呢?但是我覺得最值得推薦的是並發大師 Doug Lea《Java 並發編程:設計原則與模式》壹書中,推薦的三個用鎖的最佳實踐,它們分別是:

這三條規則,最後壹條可能過於嚴苛了,但是還是要傾向於去遵守,因為調用其他對象的方法,實在不安全了。也許其他方法裏面有線程sleep()的調用,也可能有奇慢無比的io操作,這些都會嚴重影響性能,更可怕的是其他的類的方法也可能加鎖,然後雙重鎖可能導致死鎖,所以壹定要讓妳的代碼安全,簡單,哪怕哪裏出現問題都要努力避免。

Java sdk 並發包裏 lock接口裏面的每個方法,都是經過深思熟慮的,除了支持synchronized隱式加鎖的lock方法外,還支持超時、阻塞、可中斷的方式獲取,這三種方式為我們編寫更加安全、健壯的程序提供了很大便利,以後使用鎖的時候壹定要仔細斟酌。

Java sdk 裏面 lock 的使用,有壹個

  • 上一篇: Lock和Condition(上):隱藏在並發包中的管程
  • 下一篇:C#到現在壹***經歷了幾個版本?
  • copyright 2024編程學習大全網