零:序言
使用過ReentrantLock
的盆友應該也知道Condition
的存在。先講解下它存在的意義:就是仿照實現Object
類的wait
signal
signallAll
等函式功能的。
這裡引申一個面試常問到的問題:wait
會釋放鎖,sleep
不會。
Condition
的通常使用場景是這樣的:
生產者消費者模型,假設生產者只有在生產佇列為空時才進行生產,則程式碼類似如下:
Condition emptyCondition = ReentrantLock.newCondition();
Runnable consumer = new Runnable() {
public void run() {
if(queue.isEmpty()) {
emptyCondition.signal(); // emptyObj.notify();
} else {
consumer.consume();
}
}
}
Runnable provider = new Runnable() {
public void run() {
emptyCondition.wait(); // emptyObj.wait();
providerInstance.produce();
}
}
複製程式碼
所以我們可以知道Condition
設計的意義了。下面我們來講解下其實現原理。
一:實現概況
還記得在AQS:JAVA經典之鎖實現演算法(一)提到的鎖實現的Sync Queue
嗎?
Condition
的實現是類似的原理:
每個AQS
裡有x(視你newCondition
幾次)個Condition Queue
,它的結點類也是AQS
內部類Node
。Node
裡有一個nextWaiter
,指向下一個在同一Condition Queue
裡的Node
。
結構如下圖:
- 首先明確下是,
condition.wait
一定是在成功lock
的執行緒裡呼叫才有效,不然不符合邏輯,同時也會丟擲IlleagleMornitorException
。 - 獲取鎖的執行緒處於
Sync Queue
的隊首,當呼叫condition.wait
時,該執行緒會釋放鎖(即將AQS
的state
置為0),同時喚醒後繼結點,後繼結點在acquire
的迴圈裡會成功獲取鎖,然後將自己所在結點置為隊首,然後開始自己執行緒自己的業務程式碼。
這個過程看下圖:
- 當waiter_1收到相應
condition
的signal
後,在Condition Queue
中的Node
會從Condition Queue
中出隊,進入Sync Queue
佇列,開始它的鎖競爭的過程。
過程看下圖:
所以,這裡可以看出來,即使是被signal
了,被signal
的執行緒也不是直接就開始跑,而是再次進入Sync Queue
開始競爭鎖而已。這裡的這個邏輯,跟Object.wait Object.signal
也是完全一樣的。
二:程式碼實現原理
我們先看一段運用到condition
的程式碼案例:
假設生成者在生產佇列queue
為空時emptyCondition.signal
才進行生產操作
ReentrantLock locker = new ReentrantLock();
Condition emptyCondition = locker.newCondition();
Runnable consumer = new Runnable() {
public void run() {
locker.lock();
if (queue.isEmpty()) {
emptyCondition.signal();
} else {
...
}
locker.unlock();
}
};
Runnable producer = new Runnable() {
public void run() {
locker.lock();
emptyCondition.wait();
// 開始生產
...
locker.unlock();
}
}
複製程式碼
我們從消費者一步一步走,擬定如下這樣一套執行緒切換邏輯:
producer#lock
consumer#lock
producer#await
consumer#signal
consumer#unlock
producer#unlock
(先從Sync Queue Condition Queue
圖解講一遍,然後對應圖解,對著程式碼擼一遍)
-
producer#lock
生產者直接獲取鎖成功,入隊Sync Queue
,位隊首
consumer#lock
消費者競爭鎖失敗,進入Sync Queue
等待獲取鎖
-
producer#await
生產者進入等待,釋放鎖,出Sync Queue
,進入Condition Queue
,等待emptyCondition
來喚醒。
-
consumer#signal
消費者喚起生產者,生產者consumer
的node
自Condition Queue
轉移到Sync Queue
開始競爭鎖。
-
consumer.unlock
consumer
釋放鎖後,consumer
的node
從Sync Queue
出隊,釋放state
,喚醒後繼結點provider#node
,provider
搶佔到鎖。
-
provider#unlock
這裡就沒有啥好說的了。
當然,我為了講解過程,像在鎖被第一次成功獲取的時候,邏輯上雖然並不是直接進入
Sync Queue
我也給講解成直接進入Sync Queue
了,這是為了縮減邊邊角角的小邏輯,講清楚主線邏輯。大家看明白主邏輯,然後再自己去擼一遍,就融會貫通了。
三:程式碼擼一把
-
provider.lock
final void lock() {
// 這就直接獲取鎖成功了,沒有else的邏輯了
if (compareAndSetState(0, 1))
// 這個方法是AQS類用來設定擁有鎖的執行緒例項
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
複製程式碼
-
consumer#lock
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
// consumer.lock就要走這裡了,因為上面的compareAndSetState
// 返回false
else
acquire(1);
}
複製程式碼
protected final boolean compareAndSetState(int expect, int update) {
// 樓下這個是CAS原理進行值修改,CAS就對比樂觀鎖來,
// 這裡想要修改this這個物件的state欄位,如果state是expect
// 則修改至update,返回true;否則false。我們知道provider.lock
// 已經將state 改為非0值了,所以這裡肯定失敗啦
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
複製程式碼
-
provider#await
先簡單看下Condition
類物件結構
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
/** First node of condition queue. */
private transient Node firstWaiter;
/** Last node of condition queue. */
private transient Node lastWaiter;
}
...
複製程式碼
一個Condition
物件就是一條鏈隊,頭尾結點在Condition
的內部欄位指定firstWaiter lastWaiter
。
看await
方法
public final void await() throws InterruptedException {
// 因為await是響應中斷的等待,這裡就是檢驗下,
// 通常而言,凡是throws InterruptedException的,
// 開頭基本都是這句
if (Thread.interrupted())
throw new InterruptedException();
// 這裡是向condition queue中插入一個node,並返回之,
// 插入了這個node,就代表當前執行緒在condition queue
// 中開始等待了
Node node = addConditionWaiter();
// 這個是AQS釋放鎖方法,加個fully,就是用來將多次
// 獲取鎖一次性都釋放掉,然後將鎖獲取次數返回,
// 留著後面signal後成功獲取鎖的時候,還要加鎖同樣的
// 次數。
// !!!同時注意,這裡喚醒了後繼結點!後集結點就繼續開始
// 競爭鎖,就是在acquire那個自旋方法裡,記得嗎
// 不記得去看看文章(一)
int savedState = fullyRelease(node);
// 記錄當前執行緒中斷的標記
int interruptMode = 0;
// 判斷當前的node是否已經轉移到sync queue裡了。
// 轉移了,說明這個node已經開始競爭鎖了,不用再等待
// 喚醒了,沒轉,繼續自旋
while (!isOnSyncQueue(node)) {
// 這裡把當前執行緒給掛起了
LockSupport.park(this);
// 這裡的方法checkxxx就是用來檢查waiting自旋期間,執行緒有沒有
// interrupt掉。因為await方法是響應執行緒中斷的。
// 若interrupt了,則在checkxxx方法裡,會將node轉移到
// sync Queue中,去競爭,不要擔心,因為同時
// 會設定interruptMode,在最後會根據其值拋Interrupted
// 異常。。
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 那什麼時候就結束上面的自旋呢?一個是當前的執行緒被
// signal了,那node就被transfer到sync queue了,while
// 就不滿足了。再一個就是執行緒中斷了,在while迴圈體裡給break掉了
}
// 跳出來後,緊接著去競爭鎖,知道成功為止。&& 後面這個THROW_IE,標識
// 要丟擲異常,不是的話,就是REINTERRPUT,代表保證執行緒的中斷標記不被
// 重置即可。
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 這兒是在condition queue裡有多個waiter的時候才起作用,主要用來將
// CANCEL的結點從鏈隊中剔除掉
// 具體大家自己看吧。現在忽略這
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 這兒就是處理interruptMode中斷標記欄位的邏輯
// 在reportxxx中,interruptMode為THROW_IE,則丟擲
// 異常,不是,則保證執行緒的中斷field不被重置為“未中斷”即可
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
複製程式碼
-
consumer#signal
consumer
在呼叫emptyCondition.signal
的時候,會影響到emptyCondition
的condition queue
中的等待執行緒,這裡
具體指上面的provider#await方法。
public final void signal() {
// 先判斷下,lock鎖是不是在呼叫signal方法的當前執行緒手裡
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 取到condition queue裡的第一個waiter node,這裡也就是
// consumer,因為它第一個await進入condition queue了
Node first = firstWaiter;
// 這裡去進行了具體的signal操作,具體會做先把waiter node的waitStatus
// 從CONDITION狀態改為入Sync Queue的正常狀態值0
// 然後修改Sync Queue 的Head Tail等,讓其入隊成功
// 最後再從其前驅結點的狀態值上確保當前結點能夠被喚起即可。
// 這裡是因為這個waitStatus值對後繼結點的行為是有影響的,像SIGNAL指
// 的是在結點釋放後,要去喚醒後繼結點
//
if (first != null)
doSignal(first);
}
複製程式碼
-
consumer#unlock
unlock
具體呼叫的 AQS
的release()
方法
public void unlock() {
sync.release(1);
}
// AQS.release
public final boolean release(int arg) {
// tryRelease,這裡由NonFairSync實現,具體就是通過
// CAS去修改state值,並判斷是否成功釋放鎖
if (tryRelease(arg)) {
// 成功釋放了,則在waitStatus 不是初始狀態時,去喚醒後繼,
// 這個 != 0 來做判斷的原因,就要綜合所有情況,
// 像FailSync NonFairSync Exclusive Share
// 等所有情況來看這裡的waitSTatus都會處於什麼狀態。
// 全擼一遍的話,會發現這裡的 != 0能夠涵蓋以上所有情況。
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
複製程式碼
-
provider#unlock
這裡就同理上面了。
總結
總體來看兩個 queue
的轉換還是挺清楚的。只要記住,不管什麼情況(中斷與否),都是要從condition queue
轉移到sync queue
的。具體大家還是要自己去想一種執行緒切換場景,去走走看。
行文匆匆, 歡迎指正。