併發包裏的管程 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() | 喚醒所有的等待線程 |