寫在前面
AQS
也是在來來回回看了源碼好多遍,纔有所理解。原本打算就這一篇寫完,點到即止,但不知覺的又深入了,無法抗拒的代碼魅力啊!所以分爲兩篇,一篇共享式,一篇獨佔式,美滋滋呢。
什麼是 AQS
AQS
全稱 AbstractQueuedSynchronizer
,從類名可以知道,它是一個抽象類,並且可能維護了隊列,最主要的作用是作爲同步器。
在 JUC
包下,很多同步工具類使用了它,使用的方式並不是直接繼承該類,而是使用內部類的方式;
有關同步工具類,可以參考後續的推薦博文。
這樣看來,還是“半知半解”,且看下文。
如何理解 AQS
我所理解同步的本質持有鎖,訪問共享資源,釋放鎖;共享資源的訪問是由調用方決定的,所以,只有在持有鎖和釋放鎖上面做文章。這裏先拋出問題,然後 AQS
來解決問題。
從面向對象的角度來看,AQS
是針對同步問題的一種抽象,它並不代表某種具體的同步工具,但將同步工具中某些共性給抽取了出來,以方便編寫同步工具類。
這裏的共性可以理解爲一些實現細節,列如:
-
當前線程到底如何才能掛起?
LockSupport.park
方法; -
如何表示持有鎖?
AQS
的 state 屬性值,通過該值可以判斷是否持有鎖; -
如何表示釋放鎖
仍然是通過 state 值判斷;
這些實現細節是比較複雜的,但我覺得聰明之處在於,把持有鎖或者釋放鎖的請求,交由子類去實現,典型的模板方法模式。
共享鎖的鎖獲取
先寫下共享鎖獲取,如下面的:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
該方法是用於共享模式下的鎖獲取,其中 tryAcquireShared
方法由子類實現,如果鎖獲取失敗,線程將排隊,則由 doAcquireSharedInterruptibly
方法實現,子類無需關心;子類無需關心線程如何排隊,它僅僅需要關心鎖是否獲取成功,而對於鎖的獲取成功與否,是和 AQS
的 state
屬性有關,針對該屬性,僅僅提供了以下幾種方法查看或者修改其值:
protected final int getState() {
return state;
}
protected final void setState(int newState) {
state = newState;
}
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
用流程圖描述大概如下:
上面這種流程不完全準確,不準確的原因僅僅在於真實情況更加複雜,但仍然具有參考意義;比如上面的幾個點:
- CLH 隊列:這是需要搞懂的幾個點,這個隊列是鏈表實現的,存放的數據元素是線程,以及一個
waitStatus
,根據該值可以決定後繼線程的狀態,所以,會有一個哨兵節點,該節點並未持有線程,但擁有等waitStatus
值。 - park 和 unpark:節點持有了線程,那麼在合適的情況下,就可以通過
LockSupport.park
和LockSupport.unpark
方法控制線程的執行了。
上面的兩個知識點比較重要的,CLH
隊列是一種理論的實現,還有 CAS
,這是需要底層的支持的,它解決的是原子問題,即比較值和設置值是一個原子操作,而 CAS
自旋能夠在不加鎖的情況下,安全地對共享變量進行寫操作,它的本質:比較和交換,也就是說 a 線程準備爲變量設置值時,針對變量,它會有一個預期值,比如說 a 線程認爲變量此刻值是 1,那麼在 CAS
執行時,如果未有線程修改過該變量值,那麼 CAS
執行成功;如果中途 b 線程修改了該變量值,那麼 CAS
執行失敗,此時,開始重新循環,利用新的值參與運算,重複以上過程,這就是自旋的意思;CAS
自旋有一個 ABA 問題,也就是說先修改了變量爲 2,又重新改爲 1,這時候對於 a 線程是無法感知的,這是一種特殊情況,如果處理邏輯能夠容忍這種情況,那麼也是沒有問題的;
共享鎖的釋放
共享鎖的釋放代碼如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
這也是模板方法模式,tryReleaseShared
表示鎖的釋放成與否,如果鎖成功被釋放,那麼需要喚醒隊列中的其它線程,所以 doReleaseShared
方法做的就是這件事;
交互
關於釋放了共享鎖如何換醒其它線程,我就放在這裏了,這應該纔是重點吧!
兩個最主要的方法:doReleaseShared
和 doAcquireSharedInterruptibly
方法;
doReleaseShared
能夠喚醒頭結點的後繼節點, 而 doAcquireSharedInterruptibly
方法中,後繼節點被喚醒後,會重新進入循環,那時候又會調用 doReleaseShared
方法,直到喚醒完所有節點,這就是共享鎖的交互過程;
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
/**
* 創建節點,放置隊列末尾,如果頭節點爲 null,會初始化話一個空的 node 節點,作爲頭節點,然後該 * 節點將作爲頭結點的後繼節點
**/
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 注意這裏的循環!!!
for (;;) {
// 找到上個節點
final Node p = node.predecessor();
if (p == head) {
// 嘗試獲取鎖
int r = tryAcquireShared(arg);
// 如果成功了的話,就可以不用 park 了
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/**
* 第一個方法判斷在鎖請求失敗之後,是否應該 park,
* 第二個方法則 park 當前線程;
**/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
共享鎖請求失敗後,進入上面這個方法,創建節點,入隊列;在節點創建成功,入隊列之後,線程被 park 之前,需要判斷是否能夠成功獲取鎖,這樣的話,就不用 park 了;爲什麼要用 p == head
作爲獲取鎖的 if 條件呢?想想看,隊列是有序的,如果上一個節點都還在 park 狀態,那麼當前節點是不是不能搶在它之前 ,提前結束掉!這也是用於當前線程在 park 結束後,重新喚醒其後繼節點的線程的;
shouldParkAfterFailedAcquire
這個方法修改節點的狀態並優化隊列:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
修改上一節點的 waitStatus
爲 Node.SIGNAL
,所以當前線程在進入doAcquireSharedInterruptibly
中的 for 循環時,會在這裏執行失敗,無法 park,直到下一次循環,判斷 waitStatus
的 值爲 Node.SIGNAL
才返回 true
,纔會執行使當前線程 park 的 parkAndCheckInterrupt
方法。
現在隊列是什麼狀態?兩個節點,一個空的頭節點,waitStatus
爲 Node.SIGNAL
,一個線程被 park 的節點,waitStatus
爲 初始值 0,如果再進來一個線程,新建了一個節點,那麼隊列變爲 “ 頭節點不變,前一個線程被 park 的節點的 waitStatus
值變爲 1,新來的線程這個節點鏈接在其後,waitStatus
爲 0 ”;
下面看看釋放操作 doReleaseShared
方法:
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
// 注意循環
for (;;) {
Node h = head;
// 從頭節點開始
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 第一次執行成功,修改 waitStatus 爲 0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 喚醒後繼節點線程
unparkSuccessor(h);
}
// 跳過
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 結束,如果頭節點引用未被改變
if (h == head) // loop if head changed
break;
}
}
從頭節點開始,喚醒後繼節點,這裏主要看看 unparkSuccessor
方法:
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
// 如果後繼節點不存在,或者等待狀態值大於0,則倒過來開始從尾節點開始找
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
// 節點線程被 unpark
if (s != null)
LockSupport.unpark(s.thread);
}
節點線程被 unpark
,doAcquireSharedInterruptibly
方法中的 for 循環會重新進入:
// 省略了其它代碼
// 注意這裏的循環!!!
for (;;) {
// 找到上個節點
final Node p = node.predecessor();
if (p == head) {
// 嘗試獲取鎖
int r = tryAcquireShared(arg);
// 如果成功了的話,就可以不用 park 了
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/**
* 第一個方法判斷在鎖請求失敗之後,是否應該 park,
* 第二個方法則 park 當前線程;
**/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
這個時候,tryAcquireShared
會返回 1,一個大於 0 的數,所以重要的 setHeadAndPropagate
方法來了,先看下現在的隊列情況:空的頭節點,ws=0
,第一個線程節點,ws = Node.SIGNAL
,線程已經 unpark,第二個線程節點,ws=0
,線程還處在 park;
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
這個時候將第一個線程節點設置爲 頭節點,thread 爲 null,propagate
值 會大於1,下一個線程將會被喚醒,通過重新調用 doReleaseShared()
,因爲這也會將上面整個流程重新走一遍。
還記得 doReleaseShared
方法中的 for 循環結束條件吧!
for (;;) {
// 省略其它代碼
if (h == head) // loop if head changed
break;
}
在執行到第一個線程被修改爲 頭節點時,這裏的循環就有可能結束不掉了,所以 doReleaseShared
方法會接着執行,這和 setHeadAndPropagate
方法中的該方法執行一樣!真的好巧妙啊!
總結
寫了兩個多小時,流程真的很複雜,算是有交互了,不過也算理清楚了,搞明白幾個基礎理論,幾個關鍵方法,也能略知一二啦!
推薦博文
參考博文
我與風來
認認真真學習,做思想的產出者,而不是文字的搬運工
錯誤之處,還望指出