[Java併發-8]Lock和Condition(上) 隱藏在併發包中的管程

Java SDK 併發包內容很豐富。可是最核心的仍是其對管程的實現。由於理論上利用管程,你幾乎能夠實現併發包裏全部的工具類。在前面咱們提到過在併發編程領域,有兩大核心問題:一個是
互斥:即同一時刻只容許一個線程訪問共享資源;另外一個是
同步:即線程之間如何通訊、協做。編程

這兩大問題,管程都是可以解決的。Java SDK 併發包經過 Lock 和 Condition 兩個接口來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題。安全

今天咱們重點介紹 Lock 的使用,在介紹 Lock 的使用以前,有個問題須要你首先思考一下:Java 語言自己提供的 synchronized 也是管程的一種實現,既然 Java 從語言層面已經實現了管程了,那爲何還要在 SDK 裏提供另一種實現呢?很顯然它們之間是有巨大區別的。那區別在哪裏呢?多線程

再造管程的理由

讓咱們回顧下在以前的死鎖問題中。提出一個破壞不可搶佔條件的方案。
可是這個方案 synchronized 沒有辦法解決。緣由是 synchronized 申請資源的時候,若是申請不到,線程直接進入阻塞狀態了,而線程進入阻塞狀態,啥都幹不了,也釋放不了線程已經佔有的資源。
但咱們但願的是:併發

對於「不可搶佔」這個條件,佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。

若是咱們從新設計一把互斥鎖去解決這個問題,那該怎麼設計呢?我以爲有三種方案。app

1. 可以響應中斷

synchronized 的問題是,持有鎖 A 後,若是嘗試獲取鎖 B 失敗,那麼線程就進入阻塞狀態,一旦發生死鎖,就沒有任何機會來喚醒阻塞的線程。但若是阻塞狀態的線程可以響應中斷信號,也就是說當咱們給阻塞的線程發送中斷信號的時候,可以喚醒它,那它就有機會釋放曾經持有的鎖 A。這樣就破壞了不可搶佔條件了。函數

2. 可以支持超時

若是線程在一段時間以內沒有獲取到鎖,不是進入阻塞狀態,而是返回一個錯誤,那這個線程也有機會釋放曾經持有的鎖。這樣也能破壞不可搶佔條件。工具

3. 非阻塞地獲取鎖

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

這三種方案能夠全面彌補 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 規則:

  1. 順序規則:對於線程 T1,value+=1 Happens-Before 釋放鎖的操做 unlock();
  2. volatile 變量規則:因爲 state = 1 會先讀取 state,因此線程 T1 的 unlock() 操做 Happens-Before 線程 T2 的 lock() 操做;
  3. 傳遞性規則:線程 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 併發編程:設計原則與模式》一書中,推薦的三個用鎖的最佳實踐,它們分別是:

  1. 永遠只在更新對象的成員變量時加鎖
  2. 永遠只在訪問可變的成員變量時加鎖
  3. 永遠不在調用其餘對象的方法時加鎖

最後一條你可能會以爲過於嚴苛。可是我仍是傾向於你去遵照,由於調用其餘對象的方法,實在是太不安全了,也許「其餘」方法裏面有線程 sleep() 的調用,也可能會有奇慢無比的 I/O 操做,這些都會嚴重影響性能。更可怕的是,「其餘」類的方法可能也會加鎖,而後雙重加鎖就可能致使死鎖。

小結

Java SDK 併發包裏的 Lock 接口裏面的每一個方法,你能夠感覺到,都是通過深思熟慮的。除了支持相似 synchronized 隱式加鎖的 lock() 方法外,還支持超時、非阻塞、可中斷的方式獲取鎖,這三種方式爲咱們編寫更加安全、健壯的併發程序提供了很大的便利。

還有一些其餘實踐諸如:減小鎖的持有時間、減少鎖的粒度等業界廣爲人知的規則,其實本質上它們都是相通的,不過是在該加鎖的地方加鎖而已。你能夠本身體會,本身總結,最終總結出本身的一套最佳實踐來。

相關文章
相關標籤/搜索