前言
本文是 ReentrantLock 原始碼的第二篇,第一篇主要介紹了公平鎖非公平鎖正常的加鎖解鎖流程,雖然表達能力有限不知道有沒有講清楚,本著不太監的原則,本文填補下第一篇中挖的坑。
原始碼分析
感知中斷鎖
如果我們希望檢測到中斷後能立刻丟擲異常就用 lockInterruptibly 方法去加鎖,還是建議用 lock 方法,自定義中斷處理,更靈活一點。
- ReentrantLock#lockInterruptibly
我們只需要把 ReentrantLock#lock 改成 ReentrantLock#lockInterruptibly 方法就可以獲得內部檢測中斷的鎖了
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
- AbstractQueuedSynchronizer#acquireInterruptibly
主要流程和前文介紹的類似
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 一上來就檢查下中斷,中斷直接異常,就沒必要搶鎖排隊了
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
- AbstractQueuedSynchronizer#doAcquireInterruptibly
和正常加鎖唯一區別就是這個方法,但是定睛一看是不是似曾相識?最大區別就是把中斷標識給去掉了,檢測到中斷直接拋異常
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
// 大神也偷懶了,因為這個方法,只有獨佔鎖且檢查中斷這一個應用場景,把節點入隊的步驟也揉了進來
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 當執行緒拿到鎖甦醒過來,發現自己掛起過程被中斷了,直接丟擲異常
throw new InterruptedException();
}
} finally {
// 只要發生了中斷異常,就會進取消加鎖方法
if (failed)
cancelAcquire(node);
}
}
- AbstractQueuedSynchronizer#cancelAcquire
此方法很有東西,只保證該節點失效,然後延遲移出等待佇列
private void cancelAcquire(Node node) {
if (node == null)
return;
// 把節點裡登記等待的執行緒去掉,完成這一步此節點已經沒有作用了
node.thread = null;
// 下面的三步其實可以放到一個CAS中,直接設定 CANCELLED 狀態 ,拿前一個節點,predNext 也必然是自己,但是吞吐量就下來了
// 這裡大神,沒有這樣做也是出於了效能考慮,因為我們已經把等待執行緒設定成 null 了,所以此節點已經沒有任何意義,沒有必要去保證節點第一時間被釋放,只要設定好 CANCELLED 狀態
// 就算後面 CAS 調整等待佇列失敗了,下次取消操作也會幫著回收。相應地程式碼複雜度提高了。
/* ----------------------------------------- */
// 找到自己前面第一個沒取消的節點,
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 主要是為了下面把連結串列接上
Node predNext = pred.next;
// 這裡邏輯上把當前節點的狀態設定成取消,便於檢測釋放
node.waitStatus = Node.CANCELLED;
/* ----------------------------------------- */
// 如果當前節點是尾節點,就把前一個沒取消的節點設成新尾巴
if (node == tail && compareAndSetTail(node, pred)) {
// 把新尾巴的 next 設定成空
compareAndSetNext(pred, predNext, null);
} else {
// 進到這裡說明當前節點肯定不是尾節點了
int ws;
// 條件1: 如果前一個非取消節點不是頭,也就是還需要排隊
// 條件2: 如果前一個節點為 SIGNAL,也就是說後面肯定還有執行緒等待被喚醒
// 條件3: 如果前一個節點也取消了,說明前一個節點也取消了,還沒來得及設定狀態
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
// 當前節點後一個沒取消的話,就接到前一個正常的節點後面
compareAndSetNext(pred, predNext, next);
} else {
// 前一篇文章解鎖部分講過,會把下一個節點中的執行緒恢復,然後把後繼節點接上
unparkSuccessor(node);
}
// 有點花裡胡哨,直接 = null不行麼,
node.next = node; // help GC
}
}
來張圖說明下,假如我們目前等待佇列裡有7個執行緒:
等待條件鎖
上篇文章看原始碼過程中,AQS中有個 CONDITION 狀態沒有研究
static final int CONDITION = -2;
ReentrantLock 中的 newCondition 等 Condition 相關方法正是基於 AQS 中的實現的,讓我們先大致瞭解一波作用和用法
Condition簡介
Condition 類似於 Object 中的 wait 和 notify ,主要用於執行緒間通訊,最大的優勢是 Object 的 wait 是把執行緒放到當前物件的等待池中,也就是說一個物件只能有一個等待條件,而 Condition 可以支援多個等待條件,舉個例子,商品要等至少三個人預定了才開始發售,第一個預定的減500,第二三兩個減100。正式發售之後恢復原價。
public class ReentrantLockConditionDemo {
private final ReentrantLock reentrantLock = new ReentrantLock();
private final Condition wait1 = reentrantLock.newCondition();
private final Condition wait2 = reentrantLock.newCondition();
private int wait1Count = 0;
private int wait2Count = 0;
public void buy() {
int price = 999;
reentrantLock.lock();
try {
while (wait1Count++ < 1) {
System.out.println(Thread.currentThread().getName() + "減500");
wait1.await();
price -= 500;
}
wait1.signal();
while (wait2Count++ < 2) {
System.out.println(Thread.currentThread().getName() + "減100");
wait2.await();
price -= 100;
}
wait2.signal();
System.out.println(Thread.currentThread().getName() + "到手價" + price);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
reentrantLock.unlock();
}
}
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
ReentrantLockConditionDemo reentrantLockConditionDemo = new ReentrantLockConditionDemo();
IntStream.rangeClosed(0, 4)
.forEach(num -> executorService
.execute(reentrantLockConditionDemo::buy)
);
}
/**
* 輸出:
*
* pool-1-thread-1減500
* pool-1-thread-2減100
* pool-1-thread-3減100
* pool-1-thread-4到手價999
* pool-1-thread-5到手價999
* pool-1-thread-1到手價499
* pool-1-thread-2到手價899
* pool-1-thread-3到手價899
*/
}
- ReentrantLock#newCondition
先來看條件的建立,需要基於鎖物件使用 newCondition 去建立
public Condition newCondition() {
return sync.newCondition();
}
final ConditionObject newCondition() {
// ConditionObject 是 AQS 中對 Condition 的實現
return new ConditionObject();
}
ConditionObject結構
上一篇文章中介紹了 Node 結構,這裡條件也使用了這個節點定義了一個單連結串列,統稱為條件佇列,上一篇介紹統稱同步佇列。條件佇列結構相當簡單就不單獨畫圖了。
// 條件佇列頭
private transient Node firstWaiter;
// 條件佇列尾
private transient Node lastWaiter;
// 因為預設感知中斷,需要考慮如何處理
// 退出條件佇列時重新設定中斷位
private static final int REINTERRUPT = 1;
// 退出條件佇列時直接拋異常
private static final int THROW_IE = -1;
條件佇列入隊
- AbstractQueuedSynchronizer.ConditionObject#await
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 到條件佇列中排隊,下文詳解
Node node = addConditionWaiter();
// 此方法比較簡單,就是呼叫前一篇講過的 release 方法釋放鎖(呼叫 await 時必定是鎖的持有者)
// savedState 是進入條件佇列前,持有鎖的數量
// 失敗會直接丟擲異常,並且最終把節點狀態設定為 CANCELLED
int savedState = fullyRelease(node);
int interruptMode = 0;
// 判斷在不在同步佇列(當呼叫signal之後會從條件佇列移到同步佇列),此判斷很簡單:節點狀態是 CONDITION 肯定 false,否則就到同步佇列中去找
while (!isOnSyncQueue(node)) {
// 掛起
LockSupport.park(this);
// 檢查是不是因為中斷被喚醒的,下文詳解
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 上一篇介紹過acquireQueued自旋搶鎖,如果搶到鎖了,並且中斷模式不是 -1(預設0),就記錄中斷模式為1,表示需要重新設定中斷
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清除條件佇列中取消的節點
if (node.nextWaiter != null)
// 下文詳解,在addConditionWaiter方法中也有用到
unlinkCancelledWaiters();
// 處理中斷
if (interruptMode != 0)
// 1:再次中斷 -1:丟擲異常
reportInterruptAfterWait(interruptMode);
}
- AbstractQueuedSynchronizer.ConditionObject#addConditionWaiter
加入條件佇列
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果條件佇列最後一個節點取消了,就清理
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
// 新建一個 waitStatus = -2 的節點
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 下面是簡單的單連結串列操作,之前同步佇列入隊用的 CAS 操作,因為會有很多執行緒去搶鎖,而執行緒進入條件佇列一定是拿到鎖了,不滿足條件了,所以不存在併發問題
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
- AbstractQueuedSynchronizer.ConditionObject#unlinkCancelledWaiters
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
// 輔助變數,用於接尾巴,trail始終等於迴圈中當前節點t的上一個不是取消狀態的節點
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 判斷當前節點有沒有取消
if (t.waitStatus != Node.CONDITION) {
// 斷當前節點鏈
t.nextWaiter = null;
// trail == null 說明目前條件佇列裡面全取消了
if (trail == null)
// 頭節點指向第一個沒取消的節點
firstWaiter = next;
else
// trail 是 t 的前一個節點,也就是踢出了 t
trail.nextWaiter = next;
// 如果最後一個節點取消了,那需要改一下尾指標
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
- AbstractQueuedSynchronizer.ConditionObject#checkInterruptWhileWaiting
上文 await 方法中,執行緒一旦喚醒會先檢查中斷
private int checkInterruptWhileWaiting(Node node) {
// 沒中斷,返回0,中斷了需要放回同步佇列
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
- AbstractQueuedSynchronizer#transferAfterCancelledWait
// 如果
final boolean transferAfterCancelledWait(Node node) {
// 把因為中斷醒來的節點,設定狀態為全新的節點,從條件佇列放入同步佇列
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node);
return true;
}
// 上面改狀態為什麼要 CAS ? 如果中斷喚醒的同時被 signal 喚醒了,在 signal 入隊成功之前讓出cpu,但是不釋放鎖
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
條件佇列出隊
單個喚醒和喚醒所以掉的方法類似,看一個單個喚醒流程就可
- AbstractQueuedSynchronizer.ConditionObject#signal
public final void signal() {
// 如果持有鎖的執行緒不是當前執行緒就拋異常,也就是隻有獲得鎖的執行緒可以執行喚醒操作
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 通知條件佇列中的第一個節點,也就是等的最久的節點
if (first != null)
doSignal(first);
}
- AbstractQueuedSynchronizer.ConditionObject#doSignal
private void doSignal(Node first) {
do {
// 把 first 斷鏈
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
first.nextWaiter = null;
// 如果轉移到同步佇列失敗了,並且還有條件佇列不為空就喚醒下一個
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
- AbstractQueuedSynchronizer#transferForSignal
final boolean transferForSignal(Node node) {
// 如果節點取消了,轉移失敗
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 這裡的 p 是 node 在同步佇列裡的前驅節點
Node p = enq(node);
int ws = p.waitStatus;
// 看過上一篇文章應該有映像,只要是進同步佇列,都需要把前一個節點狀態設為 -1
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果取消了,或者狀態設定失敗,喚醒後繼續掛起
LockSupport.unpark(node.thread);
return true;
}
最後按照慣例結合上面的案例,畫張圖總結下: