Java SDK併發包的鎖------Lock

併發包裏的管程 Lock

我們提到過在併發編程領域,有兩大核心問題:一個是互斥,即同一時刻只允許一個線程訪問共享資源;另一個是同步,即線程之間如何通信、協作。這兩大問題,管程都是能夠解決的。Java SDK 併發包通過 Lock 和 Condition 兩個接口來實現管程,其中 Lock 用於解決互斥問題,Condition 用於解決同步問題

什麼是死鎖

現實世界裏的死等,就是編程領域的死鎖了。死鎖的一個比較專業的定義是:一組互相競爭資源的線程因互相等待,導致“永久”阻塞的現象

如何預防死鎖

要避免死鎖就需要分析死鎖發生的條件,有個叫 Coffman 的牛人早就總結過了,只有以下這四個條件都發生時纔會出現死鎖:

1、互斥,共享資源 X 和 Y 只能被一個線程佔用;

2、佔有且等待,線程 T1 已經取得共享資源 X,在等待共享資源 Y 的時候,不釋放共享資源 X;

3、不可搶佔,其他線程不能強行搶佔線程 T1 佔有的資源;

4、 循環等待,線程 T1 等待線程 T2 佔有的資源,線程 T2 等待線程 T1 佔有的資源,就是循環等待。

反過來分析,也就是說只要我們破壞其中一個,就可以成功避免死鎖的發生。

其中,互斥這個條件我們沒有辦法破壞,因爲我們用鎖爲的就是互斥。不過其他三個條件都是有辦法破壞掉的,到底如何做呢?

佔用且等待

我們可以一次性申請所有的資源,這樣就不存在等待了。

不可搶佔

佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。

循環等待

對於這個條件,可以靠按序申請資源來預防。所謂按序申請,是指資源是有線性順序的,申請的時候可以先申請資源序號小的,再申請資源序號大的,這樣線性化後自然就不存在循環了。

併發包解決死鎖方案

前面提出了一個破壞不可搶佔條件方案,但是這個方案 synchronized 沒有辦法解決。原因是 synchronized 申請資源的時候,如果申請不到,線程直接進入阻塞狀態了,而線程進入阻塞狀態,啥都幹不了,也釋放不了線程已經佔有的資源。但我們希望的是:

對於“不可搶佔”這個條件,佔用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它佔有的資源,這樣不可搶佔這個條件就破壞掉了。

Lock如何解決死鎖問題

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

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

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

體現在 API 上,就是 Lock 接口的三個方法。詳情如下:

// 支持中斷的 API
void lockInterruptibly() 
  throws InterruptedException;
// 支持超時的 API
boolean tryLock(long time, TimeUnit unit) 
  throws InterruptedException;
// 支持非阻塞獲取鎖的 API
boolean tryLock();

Lock如何保證可見性

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;
  }
}

Lock的API(interface)

方法名稱 描述
void lock() 獲取鎖,調用該方法該線程會獲取鎖,當鎖獲得後,從該方法返回
void lockInterruptibly() throws InterruptedException 可中斷地獲取鎖,該方法會響應中斷,當獲取鎖的線程被中斷時,中斷異常會拋出,同時鎖會被釋放
boolean tryLock() 嘗試非阻塞的獲取鎖,調用該方法時會立即返回,如果能夠獲取則返回true,否則返回false
boolean tryLock(long time, TimeUnit unit) throws InterruptedException 超時的獲取鎖,當前線程會在一下3中情況下返回:1、當前線程在超時時間內獲取到鎖;2、當前線程在超時時間內中斷;3、當前線程超時時間結束,返回false
void unlock() 釋放鎖
Condition newCondition() 獲取等待通知組件,該組件和當前的鎖是綁定關係,當前線程只有獲得鎖,才能調用該組件的wait()方法,而調用該方法以後,當前線程則釋放鎖

Condition相關API(interface)

方法名稱 描述
void await() throws InterruptedException 當前線程處於等待狀態,直到收到喚醒的信號或者線程中斷
void awaitUninterruptibly() 當前線程處於等待狀態,直到收到喚醒信號或者中斷,或者指定的等待時間已過
long awaitNanos(long nanosTimeout) throws InterruptedException 當前線程處於等待狀態,直到收到喚醒信號或者中斷,或者指定的等待時間已過
boolean await(long time, TimeUnit unit) throws InterruptedException 當前線程處於等待狀態,直到收到喚醒信號或者中斷,或者指定的等待時間已過,類似於awaitNanos(long nanosTimeout)
boolean awaitUntil(Date deadline) throws InterruptedException 當前線程處於等待狀態,直到收到喚醒信號或者中斷,或者指定的截至時間已過
void signal() 喚醒一個等待的線程
void signalAll() 喚醒所有的等待線程
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章