Java SDK 併發包內容很豐富。可是最核心的仍是其對管程的實現。由於理論上利用管程,你幾乎能夠實現併發包裏全部的工具類。在前面咱們提到過在併發編程領域,有兩大核心問題:一個是互斥:即同一時刻只容許一個線程訪問共享資源;
另外一個是 同步:即線程之間如何通訊、協做。
編程
這兩大問題,管程都是可以解決的。Java SDK 併發包經過 Lock 和 Condition 兩個接口來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題。安全
今天咱們重點介紹 Lock 的使用,在介紹 Lock 的使用以前,有個問題須要你首先思考一下:Java 語言自己提供的 synchronized 也是管程的一種實現,既然 Java 從語言層面已經實現了管程了,那爲何還要在 SDK 裏提供另一種實現呢?很顯然它們之間是有巨大區別的。那區別在哪裏呢?多線程
讓咱們回顧下在以前的死鎖
問題中。提出一個破壞不可搶佔條件
的方案。
可是這個方案 synchronized 沒有辦法解決。緣由是 synchronized 申請資源的時候,若是申請不到,線程直接進入阻塞狀態了,而線程進入阻塞狀態,啥都幹不了,也釋放不了線程已經佔有的資源。
但咱們但願的是:併發
對於「不可搶佔」這個條件,佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。
若是咱們從新設計一把互斥鎖去解決這個問題,那該怎麼設計呢?我以爲有三種方案。app
synchronized 的問題是,持有鎖 A 後,若是嘗試獲取鎖 B 失敗,那麼線程就進入阻塞狀態,一旦發生死鎖,就沒有任何機會來喚醒阻塞的線程。但若是阻塞狀態的線程可以響應中斷信號,也就是說當咱們給阻塞的線程發送中斷信號的時候,可以喚醒它,那它就有機會釋放曾經持有的鎖 A。這樣就破壞了不可搶佔條件了。函數
若是線程在一段時間以內沒有獲取到鎖,不是進入阻塞狀態,而是返回一個錯誤,那這個線程也有機會釋放曾經持有的鎖。這樣也能破壞不可搶佔條件。工具
若是嘗試獲取鎖失敗,並不進入阻塞狀態,而是直接返回,那這個線程也有機會釋放曾經持有的鎖。這樣也能破壞不可搶佔條件。性能
這三種方案能夠全面彌補 synchronized 的問題。這三個方案就是「重複造輪子」的主要緣由,體如今 API 上,就是 Lock 接口的三個方法。詳情以下:線程
// 支持中斷的 API void lockInterruptibly() throws InterruptedException; // 支持超時的 API boolean tryLock(long time, TimeUnit unit) throws InterruptedException; // 支持非阻塞獲取鎖的 API boolean tryLock();
Java SDK 裏面 Lock 的使用,有一個經典的範例,就是try{}finally{}
。須要重點關注的是在 finally 裏面釋放鎖。這個範例無需多解釋。可是有一點須要解釋一下,那就是可見性是怎麼保證的。你已經知道 Java 裏多線程的可見性是經過 Happens-Before 規則保證的,而 synchronized 之因此可以保證可見性,也是由於有一條 synchronized 相關的規則:synchronized 的解鎖 Happens-Before 於後續對這個鎖的加鎖。
那 Java SDK 裏面 Lock 靠什麼保證可見性呢?例如在下面的代碼中,線程 T1 對 value 進行了 +=1 操做,那後續的線程 T2 可以看到 value 的正確結果嗎?翻譯
class X { private final Lock rtl = new ReentrantLock(); int value; public void addOne() { // 獲取鎖 rtl.lock(); try { value+=1; } finally { // 保證鎖能釋放 rtl.unlock(); } } }
咱們來比較理論的討論下這段代碼的可見性是怎麼保證的。
這裏說下雖然 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() 操做。class SampleLock { volatile int state; // 加鎖 lock() { // 省略代碼無數 state = 1; } // 解鎖 unlock() { // 省略代碼無數 state = 0; } }
若是你細心觀察,會發現咱們建立的鎖的具體類名是ReentrantLock
,這個翻譯過來叫可重入鎖
,這個概念前面咱們一直沒有介紹過。所謂可重入鎖,顧名思義,指的是線程能夠重複獲取同一把鎖。
例以下面代碼中,當線程 T1 執行到 ① 處時,已經獲取到了鎖 rtl ,當在 ① 處調用 get() 方法時,會在 ② 再次對鎖 rtl 執行加鎖操做。此時,若是鎖 rtl 是可重入的,那麼線程 T1 能夠再次加鎖成功;若是鎖 rtl 是不可重入的,那麼線程 T1 此時會被阻塞。
class X { private final Lock rtl = new ReentrantLock(); int value; public int get() { // 獲取鎖 rtl.lock(); ② try { return value; } finally { // 保證鎖能釋放 rtl.unlock(); } } public void addOne() { // 獲取鎖 rtl.lock(); try { value = 1 + get(); ① } finally { // 保證鎖能釋放 rtl.unlock(); } } }
在使用ReentrantLock
的時候,你會發現ReentrantLock
這個類有兩個構造函數,一個是無參構造函數,一個是傳入 fair 參數的構造函數。fair 參數表明的是鎖的公平策略,若是傳入 true 就表示須要構造一個公平鎖,反之則表示要構造一個非公平鎖。
// 無參構造函數:默認非公平鎖 public ReentrantLock() { sync = new NonfairSync(); } // 根據公平策略參數建立鎖 public ReentrantLock(boolean fair){ sync = fair ? new FairSync() : new NonfairSync(); }
在以前咱們介紹過入口等待隊列,鎖都對應着一個等待隊列,若是一個線程沒有得到鎖,就會進入等待隊列,當有線程釋放鎖的時候,就須要從等待隊列中喚醒一個等待的線程。若是是公平鎖,喚醒的策略就是誰等待的時間長,就喚醒誰,很公平;若是是非公平鎖,則不提供這個公平保證,有可能等待時間短的線程反而先被喚醒。
你已經知道,用鎖雖然能解決不少併發問題,可是風險也是挺高的。雖然有不少最佳實踐,可是我以爲最值得推薦的是併發大師 Doug Lea《Java 併發編程:設計原則與模式》一書中,推薦的三個用鎖的最佳實踐,它們分別是:
最後一條你可能會以爲過於嚴苛。可是我仍是傾向於你去遵照,由於調用其餘對象的方法,實在是太不安全了,也許「其餘」方法裏面有線程 sleep() 的調用,也可能會有奇慢無比的 I/O 操做,這些都會嚴重影響性能。更可怕的是,「其餘」類的方法可能也會加鎖,而後雙重加鎖就可能致使死鎖。
Java SDK 併發包裏的 Lock 接口裏面的每一個方法,你能夠感覺到,都是通過深思熟慮的。除了支持相似 synchronized 隱式加鎖的 lock() 方法外,還支持超時、非阻塞、可中斷的方式獲取鎖,這三種方式爲咱們編寫更加安全、健壯的併發程序提供了很大的便利。
還有一些其餘實踐諸如:減小鎖的持有時間、減少鎖的粒度等業界廣爲人知的規則,其實本質上它們都是相通的,不過是在該加鎖的地方加鎖而已。你能夠本身體會,本身總結,最終總結出本身的一套最佳實踐來。