原文鏈接:https://javadoop.com/post/AbstractQueuedSynchronizer
在分析 Java 併發包 java.util.concurrent 源碼的時候,少不了需要了解 AbstractQueuedSynchronizer(以下簡寫AQS)這個抽象類,因爲它是 Java 併發包的基礎工具類,是實現 ReentrantLock、CountDownLatch、Semaphore、FutureTask 等類的基礎。
Google 一下 AbstractQueuedSynchronizer,我們可以找到很多關於 AQS 的介紹,但是很多都沒有介紹清楚,因爲大部分文章沒有把其中的一些關鍵的細節說清楚。
本文將從 ReentrantLock 的公平鎖源碼出發,分析下 AbstractQueuedSynchronizer 這個類是怎麼工作的,希望能給大家提供一些簡單的幫助。
申明以下幾點:
- 本文有點長,但是很簡單很簡單很簡單,主要面向讀者對象爲併發編程的初學者,或者想要閱讀java併發包源碼的開發者。
- 建議在電腦上閱讀,如果你想好好地理解所有的細節,而且你從來沒看過相關的分析,你可能至少需要 20 分鐘仔細看所有的描述,本文後面的 1/3 以上很簡單,前面的 1/4 更簡單,中間的部分要好好看。
- 如果你不知道爲什麼要看這個,我想告訴你,即使你看懂了所有的細節,你可能也不能把你的業務代碼寫得更好
- 源碼環境 JDK1.7,看到不懂或有疑惑的部分,最好能自己打開源碼看看。Doug Lea 大神的代碼寫得真心不錯。
- 有很多英文註釋我沒有刪除,這樣讀者可以參考着英文說的來,萬一被我忽悠了呢
- 本文不分析共享模式,這樣可以給讀者減少很多負擔,只要把獨佔模式看懂,共享模式讀者應該就可以順着代碼看懂了。而且也不分析 condition 部分,所以應該說很容易就可以看懂了。
- 本文大量使用我們平時用得最多的 ReentrantLock 的概念,本質上來說是不正確的,讀者應該清楚,AQS 不僅僅用來實現鎖,只是希望讀者可以用鎖來聯想 AQS 的使用場景,降低讀者的閱讀壓力
- ReentrantLock 的公平鎖和非公平鎖只有一點點區別,沒有任何閱讀壓力
- 你需要提前知道什麼是 CAS(CompareAndSet)
廢話結束,開始。
AQS 結構
先來看看 AQS 有哪些屬性,搞清楚這些基本就知道 AQS 是什麼套路了,畢竟可以猜嘛!
// 頭結點,你直接把它當做 當前持有鎖的線程 可能是最好理解的
private transient volatile Node head;
// 阻塞的尾節點,每個新的節點進來,都插入到最後,也就形成了一個隱視的鏈表
private transient volatile Node tail;
// 這個是最重要的,不過也是最簡單的,代表當前鎖的狀態,0代表沒有被佔用,大於0代表有線程持有當前鎖
// 之所以說大於0,而不是等於1,是因爲鎖可以重入嘛,每次重入都加上1
private volatile int state;
// 代表當前持有獨佔鎖的線程,舉個最重要的使用例子,因爲鎖可以重入
// reentrantLock.lock()可以嵌套調用多次,所以每次用這個來判斷當前線程是否已經擁有了鎖
// if (currentThread == getExclusiveOwnerThread()) {state++}
private transient Thread exclusiveOwnerThread; //繼承自AbstractOwnableSynchronizer
怎麼樣,看樣子應該是很簡單的吧,畢竟也就四個屬性啊。
AbstractQueuedSynchronizer 的等待隊列示意如下所示,注意了,之後分析過程中所說的 queue,也就是阻塞隊列不包含 head,不包含 head,不包含 head。
等待隊列中每個線程被包裝成一個 node,數據結構是鏈表,一起看看源碼吧:
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
// 標識節點當前在共享模式下
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
// 標識節點當前在獨佔模式下
static final Node EXCLUSIVE = null;
// ======== 下面的幾個int常量是給waitStatus用的 ===========
/** waitStatus value to indicate thread has cancelled */
// 代碼此線程取消了爭搶這個鎖
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
// 官方的描述是,其表示當前node的後繼節點對應的線程需要被喚醒
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
// 本文不分析condition,所以略過吧,下一篇文章會介紹這個
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
// 同樣的不分析,略過吧
static final int PROPAGATE = -3;
// =====================================================
// 取值爲上面的1、-1、-2、-3,或者0(以後會講到)
// 這麼理解,暫時只需要知道如果這個值 大於0 代表此線程取消了等待,
// 也許就是說半天搶不到鎖,不搶了,ReentrantLock是可以指定timeouot的。。。
volatile int waitStatus;
// 前驅節點的引用
volatile Node prev;
// 後繼節點的引用
volatile Node next;
// 這個就是線程本尊
volatile Thread thread;
}
Node 的數據結構其實也挺簡單的,就是 thread + waitStatus + pre + next 四個屬性而已,大家先要有這個概念在心裏。
上面的是基礎知識,後面會多次用到,心裏要時刻記着它們,心裏想着這個結構圖就可以了。下面,我們開始說 ReentrantLock 的公平鎖。多嘴一下,我說的阻塞隊列不包含 head 節點。
首先,我們先看下 ReentrantLock 的使用方式。
// 我用個web開發中的service概念吧
public class OrderService {
// 使用static,這樣每個線程拿到的是同一把鎖,當然,spring mvc中service默認就是單例,別糾結這個
private static ReentrantLock reentrantLock = new ReentrantLock(true);
public void createOrder() {
// 比如我們同一時間,只允許一個線程創建訂單
reentrantLock.lock();
// 通常,lock 之後緊跟着 try 語句
try {
// 這塊代碼同一時間只能有一個線程進來(獲取到鎖的線程),
// 其他的線程在lock()方法上阻塞,等待獲取到鎖,再進來
// 執行代碼...
// 執行代碼...
// 執行代碼...
} finally {
// 釋放鎖
reentrantLock.unlock();
}
}
}
ReentrantLock 在內部用了內部類 Sync 來管理鎖,所以真正的獲取鎖和釋放鎖是由 Sync 的實現類來控制的。
abstract static class Sync extends AbstractQueuedSynchronizer {
}
Sync 有兩個實現,分別爲 NonfairSync(非公平鎖)和 FairSync(公平鎖),我們看 FairSync 部分。
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
線程搶鎖
很多人肯定開始嫌棄上面廢話太多了,下面跟着代碼走,我就不廢話了。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
// 爭鎖
final void lock() {
acquire(1);
}
// 來自父類AQS,我直接貼過來這邊,下面分析的時候同樣會這樣做,不會給讀者帶來閱讀壓力
// 我們看到,這個方法,如果tryAcquire(arg) 返回true, 也就結束了。
// 否則,acquireQueued方法會將線程壓到隊列中
public final void acquire(int arg) { // 此時 arg == 1
// 首先調用tryAcquire(1)一下,名字上就知道,這個只是試一試
// 因爲有可能直接就成功了呢,也就不需要進隊列排隊了,
// 對於公平鎖的語義就是:本來就沒人持有鎖,根本沒必要進隊列等待(又是掛起,又是等待被喚醒的)
if (!tryAcquire(arg) &&
// tryAcquire(arg)沒有成功,這個時候需要把當前線程掛起,放到阻塞隊列中。
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
selfInterrupt();
}
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 嘗試直接獲取鎖,返回值是boolean,代表是否獲取到鎖
// 返回true:1.沒有線程在等待鎖;2.重入鎖,線程本來就持有鎖,也就可以理所當然可以直接獲取
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// state == 0 此時此刻沒有線程持有鎖
if (c == 0) {
// 雖然此時此刻鎖是可以用的,但是這是公平鎖,既然是公平,就得講究先來後到,
// 看看有沒有別人在隊列中等了半天了
if (!hasQueuedPredecessors() &&
// 如果沒有線程在等待,那就用CAS嘗試一下,成功了就獲取到鎖了,
// 不成功的話,只能說明一個問題,就在剛剛幾乎同一時刻有個線程搶先了 =_=
// 因爲剛剛還沒人的,我判斷過了???
compareAndSetState(0, acquires)) {
// 到這裏就是獲取到鎖了,標記一下,告訴大家,現在是我佔用了鎖
setExclusiveOwnerThread(current);
return true;
}
}
// 會進入這個else if分支,說明是重入了,需要操作:state=state+1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
// 如果到這裏,說明前面的if和else if都沒有返回true,說明沒有獲取到鎖
// 回到上面一個外層調用方法繼續看:
// if (!tryAcquire(arg)
// && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
return false;
}
// 假設tryAcquire(arg) 返回false,那麼代碼將執行:
// acquireQueued(addWaiter(Node.EXCLUSIVE), arg),
// 這個方法,首先需要執行:addWaiter(Node.EXCLUSIVE)
/**
* Creates and enqueues node for current thread and given mode.
*
* @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
* @return the new node
*/
// 此方法的作用是把線程包裝成node,同時進入到隊列中
// 參數mode此時是Node.EXCLUSIVE,代表獨佔模式
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 以下幾行代碼想把當前node加到鏈表的最後面去,也就是進到阻塞隊列的最後
Node pred = tail;
// tail!=null => 隊列不爲空(tail==head的時候,其實隊列是空的,不過不管這個吧)
if (pred != null) {
// 設置自己的前驅 爲當前的隊尾節點
node.prev = pred;
// 用CAS把自己設置爲隊尾, 如果成功後,tail == node了
if (compareAndSetTail(pred, node)) {
// 進到這裏說明設置成功,當前node==tail, 將自己與之前的隊尾相連,
// 上面已經有 node.prev = pred
// 加上下面這句,也就實現了和之前的尾節點雙向連接了
pred.next = node;
// 線程入隊了,可以返回了
return node;
}
}
// 仔細看看上面的代碼,如果會到這裏,
// 說明 pred==null(隊列是空的) 或者 CAS失敗(有線程在競爭入隊)
// 讀者一定要跟上思路,如果沒有跟上,建議先不要往下讀了,往回仔細看,否則會浪費時間的
enq(node);
return node;
}
/**
* Inserts node into queue, initializing if necessary. See picture above.
* @param node the node to insert
* @return node's predecessor
*/
// 採用自旋的方式入隊
// 之前說過,到這個方法只有兩種可能:等待隊列爲空,或者有線程競爭入隊,
// 自旋在這邊的語義是:CAS設置tail過程中,競爭一次競爭不到,我就多次競爭,總會排到的
private Node enq(final Node node) {
for (;;) {
Node t = tail;
// 之前說過,隊列爲空也會進來這裏
if (t == null) { // Must initialize
// 初始化head節點
// 細心的讀者會知道原來head和tail初始化的時候都是null,反正我不細心
// 還是一步CAS,你懂的,現在可能是很多線程同時進來呢
if (compareAndSetHead(new Node()))
// 給後面用:這個時候head節點的waitStatus==0, 看new Node()構造方法就知道了
// 這個時候有了head,但是tail還是null,設置一下,
// 把tail指向head,放心,馬上就有線程要來了,到時候tail就要被搶了
// 注意:這裏只是設置了tail=head,這裏可沒return哦,沒有return,沒有return
// 所以,設置完了以後,繼續for循環,下次就到下面的else分支了
tail = head;
} else {
// 下面幾行,和上一個方法 addWaiter 是一樣的,
// 只是這個套在無限循環裏,反正就是將當前線程排到隊尾,有線程競爭的話排不上重複排
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
// 現在,又回到這段代碼了
// if (!tryAcquire(arg)
// && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// selfInterrupt();
// 下面這個方法,參數node,經過addWaiter(Node.EXCLUSIVE),此時已經進入阻塞隊列
// 注意一下:如果acquireQueued(addWaiter(Node.EXCLUSIVE), arg))返回true的話,
// 意味着上面這段代碼將進入selfInterrupt(),所以正常情況下,下面應該返回false
// 這個方法非常重要,應該說真正的線程掛起,然後被喚醒後去獲取鎖,都在這個方法裏了
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// p == head 說明當前節點雖然進到了阻塞隊列,但是是阻塞隊列的第一個,因爲它的前驅是head
// 注意,阻塞隊列不包含head節點,head一般指的是佔有鎖的線程,head後面的才稱爲阻塞隊列
// 所以當前節點可以去試搶一下鎖
// 這裏我們說一下,爲什麼可以去試試:
// 首先,它是隊頭,這個是第一個條件,其次,當前的head有可能是剛剛初始化的node,
// enq(node) 方法裏面有提到,head是延時初始化的,而且new Node()的時候沒有設置任何線程
// 也就是說,當前的head不屬於任何一個線程,所以作爲隊頭,可以去試一試,
// tryAcquire已經分析過了, 忘記了請往前看一下,就是簡單用CAS試操作一下state
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 到這裏,說明上面的if分支沒有成功,要麼當前node本來就不是隊頭,
// 要麼就是tryAcquire(arg)沒有搶贏別人,繼續往下看
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
/**
* Checks and updates status for a node that failed to acquire.
* Returns true if thread should block. This is the main signal
* control in all acquire loops. Requires that pred == node.prev
*
* @param pred node's predecessor holding status
* @param node the node
* @return {@code true} if thread should block
*/
// 剛剛說過,會到這裏就是沒有搶到鎖唄,這個方法說的是:"當前線程沒有搶到鎖,是否需要掛起當前線程?"
// 第一個參數是前驅節點,第二個參數纔是代表當前線程的節點
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
// 前驅節點的 waitStatus == -1 ,說明前驅節點狀態正常,當前線程需要掛起,直接可以返回true
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
// 前驅節點 waitStatus大於0 ,之前說過,大於0 說明前驅節點取消了排隊。這裏需要知道這點:
// 進入阻塞隊列排隊的線程會被掛起,而喚醒的操作是由前驅節點完成的。
// 所以下面這塊代碼說的是將當前節點的prev指向waitStatus<=0的節點,
// 簡單說,就是爲了找個好爹,因爲你還得依賴它來喚醒呢,如果前驅節點取消了排隊,
// 找前驅節點的前驅節點做爹,往前循環總能找到一個好爹的
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.
*/
// 仔細想想,如果進入到這個分支意味着什麼
// 前驅節點的waitStatus不等於-1和1,那也就是隻可能是0,-2,-3
// 在我們前面的源碼中,都沒有看到有設置waitStatus的,所以每個新的node入隊時,waitStatu都是0
// 用CAS將前驅節點的waitStatus設置爲Node.SIGNAL(也就是-1)
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
// private static boolean shouldParkAfterFailedAcquire(Node pred, Node node)
// 這個方法結束根據返回值我們簡單分析下:
// 如果返回true, 說明前驅節點的waitStatus==-1,是正常情況,那麼當前線程需要被掛起,等待以後被喚醒
// 我們也說過,以後是被前驅節點喚醒,就等着前驅節點拿到鎖,然後釋放鎖的時候叫你好了
// 如果返回false, 說明當前不需要被掛起,爲什麼呢?往後看
// 跳回到前面是這個方法
// if (shouldParkAfterFailedAcquire(p, node) &&
// parkAndCheckInterrupt())
// interrupted = true;
// 1. 如果shouldParkAfterFailedAcquire(p, node)返回true,
// 那麼需要執行parkAndCheckInterrupt():
// 這個方法很簡單,因爲前面返回true,所以需要掛起線程,這個方法就是負責掛起線程的
// 這裏用了LockSupport.park(this)來掛起線程,然後就停在這裏了,等待被喚醒=======
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// 2. 接下來說說如果shouldParkAfterFailedAcquire(p, node)返回false的情況
// 仔細看shouldParkAfterFailedAcquire(p, node),我們可以發現,其實第一次進來的時候,一般都不會返回true的,原因很簡單,前驅節點的waitStatus=-1是依賴於後繼節點設置的。也就是說,我都還沒給前驅設置-1呢,怎麼可能是true呢,但是要看到,這個方法是套在循環裏的,所以第二次進來的時候狀態就是-1了。
// 解釋下爲什麼shouldParkAfterFailedAcquire(p, node)返回false的時候不直接掛起線程:
// => 是爲了應對在經過這個方法後,node已經是head的直接後繼節點了。剩下的讀者自己想想吧。
}
說到這裏,也就明白了,多看幾遍 final boolean acquireQueued(final Node node, int arg)
這個方法吧。自己推演下各個分支怎麼走,哪種情況下會發生什麼,走到哪裏。
解鎖操作
最後,就是還需要介紹下喚醒的動作了。我們知道,正常情況下,如果線程沒獲取到鎖,線程會被 LockSupport.park(this);
掛起停止,等待被喚醒。
// 喚醒的代碼還是比較簡單的,你如果上面加鎖的都看懂了,下面都不需要看就知道怎麼回事了
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
// 往後看吧
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 回到ReentrantLock看tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
// 是否完全釋放鎖
boolean free = false;
// 其實就是重入的問題,如果c==0,也就是說沒有嵌套鎖了,可以釋放了,否則還不能釋放掉
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
// 喚醒後繼節點
// 從上面調用處知道,參數node是head頭結點
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;
// 如果head節點當前waitStatus<0, 將其修改爲0
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.
*/
// 下面的代碼就是喚醒後繼節點,但是有可能後繼節點取消了等待(waitStatus==1)
// 從隊尾往前找,找到waitStatus<=0的所有節點中排在最前面的
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 從後往前找,仔細看代碼,不必擔心中間有節點取消(waitStatus==1)的情況
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 喚醒線程
LockSupport.unpark(s.thread);
}
喚醒線程以後,被喚醒的線程將從以下代碼中繼續往前走:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 剛剛線程被掛起在這裏了
return Thread.interrupted();
}
// 又回到這個方法了:acquireQueued(final Node node, int arg),這個時候,node的前驅是head了
好了,後面就不分析源碼了,剩下的還有問題自己去仔細看看代碼吧。
總結
總結一下吧。
在併發環境下,加鎖和解鎖需要以下三個部件的協調:
- 鎖狀態。我們要知道鎖是不是被別的線程佔有了,這個就是 state 的作用,它爲 0 的時候代表沒有線程佔有鎖,可以去爭搶這個鎖,用 CAS 將 state 設爲 1,如果 CAS 成功,說明搶到了鎖,這樣其他線程就搶不到了,如果鎖重入的話,state進行+1 就可以,解鎖就是減 1,直到 state 又變爲 0,代表釋放鎖,所以 lock() 和 unlock() 必須要配對啊。然後喚醒等待隊列中的第一個線程,讓其來佔有鎖。
- 線程的阻塞和解除阻塞。AQS 中採用了 LockSupport.park(thread) 來掛起線程,用 unpark 來喚醒線程。
- 阻塞隊列。因爲爭搶鎖的線程可能很多,但是只能有一個線程拿到鎖,其他的線程都必須等待,這個時候就需要一個 queue 來管理這些線程,AQS 用的是一個 FIFO 的隊列,就是一個鏈表,每個 node 都持有後繼節點的引用。AQS 採用了 CLH 鎖的變體來實現,感興趣的讀者可以參考這篇文章關於CLH的介紹,寫得簡單明瞭。
示例圖解析
下面屬於回顧環節,用簡單的示例來說一遍,如果上面的有些東西沒看懂,這裏還有一次幫助你理解的機會。
首先,第一個線程調用 reentrantLock.lock(),翻到最前面可以發現,tryAcquire(1) 直接就返回 true 了,結束。只是設置了 state=1,連 head 都沒有初始化,更談不上什麼阻塞隊列了。要是線程 1 調用 unlock() 了,纔有線程 2 來,那世界就太太太平了,完全沒有交集嘛,那我還要 AQS 幹嘛。
如果線程 1 沒有調用 unlock() 之前,線程 2 調用了 lock(), 想想會發生什麼?
線程 2 會初始化 head【new Node()】,同時線程 2 也會插入到阻塞隊列並掛起 (注意看這裏是一個 for 循環,而且設置 head 和 tail 的部分是不 return 的,只有入隊成功纔會跳出循環)
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
首先,是線程 2 初始化 head 節點,此時 head==tail, waitStatus==0
然後線程 2 入隊:
同時我們也要看此時節點的 waitStatus,我們知道 head 節點是線程 2 初始化的,此時的 waitStatus 沒有設置, java 默認會設置爲 0,但是到 shouldParkAfterFailedAcquire 這個方法的時候,線程 2 會把前驅節點,也就是 head 的waitStatus設置爲-1。
那線程 2 節點此時的 waitStatus 是多少呢,由於沒有設置,所以是 0;
如果線程3此時再進來,直接插到線程2的後面就可以了,此時線程 3 的 waitStatus 是 0,到 shouldParkAfterFailedAcquire 方法的時候把前驅節點線程 2 的 waitStatus 設置爲 -1。
這裏可以簡單說下 waitStatus 中 SIGNAL(-1) 狀態的意思,Doug Lea 註釋的是:代表後繼節點需要被喚醒。也就是說這個 waitStatus 其實代表的不是自己的狀態,而是後繼節點的狀態,我們知道,每個 node 在入隊的時候,都會把前驅節點的狀態改爲 SIGNAL,然後阻塞,等待被前驅喚醒。這裏涉及的是兩個問題:有線程取消了排隊、喚醒操作。其實本質是一樣的,讀者也可以順着 “waitStatus代表後繼節點的狀態” 這種思路去看一遍源碼。
(全文完)
評論區
大神,這裏的理解是不是有點問題。 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } 這裏保留head爲剛開始的new Node()不好嗎?爲什麼要重新設置一下head呢?
HongJie2017-11-16 10:04
我稍微美化一下:
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
這裏保留head爲剛開始的new Node()不好嗎?爲什麼要重新設置一下head呢?
假設不調用 setHead(node),我們假設此時 A 持有這個鎖,head 是 new Node() 那個空節點。
A 持有的鎖用完了,釋放鎖,喚醒後繼節點 B。後繼節點 B 從 parkAndCheckInterrupt() 這個方法返回,注意這裏的 for 循環。然後調用 final Node p = node.predecessor(); 這個方法,那麼這個時候,p == head 就不成立了,也就進不到 tryAcquire(arg) 這裏去獲取鎖。
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 進到這裏,說明獲取到鎖了
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
HongJie2017-11-16 10:12
另一處。既然當前線程獲取到了鎖,它就不應該再是阻塞隊列的一員。如果沒有 setHead 這一步,下面這個方法就不準確了:
public final Collection<Thread> getQueuedThreads() {
ArrayList<Thread> list = new ArrayList<Thread>();
for (Node p = tail; p != null; p = p.prev) {
Thread t = p.thread;
if (t != null) // 注意這裏
list.add(t);
}
return list;
}
xuke2018-11-09 15:01
因爲每次喚醒(unPark)的都是阻塞隊列裏面的第一個被阻塞掛起(park)的線程head->next; 如果該線程被喚醒了不把head往後移動那麼後面有很多線程等待被喚醒怎麼辦(每次都喚醒第一個)?
徐濤2017-11-16 11:24
謝謝! 另外我感覺AQS裏面的waitStatus不是很好理解,有時候-1代表後續節點是不是需要unpark,1又代表自己是被cancel掉。有時候代表別人的狀態,有時候代表自己的狀態。 都用waitStatus表示自己的狀態不好嗎?
spjich2017-11-30 17:19
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
第一個問題:這個finally裏的cancelAcquire 似乎永遠都不會被執行吧 第二個問題:爲什麼都是要從tail往前找第一個狀態是非CANCEL的節點呢,如果從head往後找第一個狀態是非CANCEL的話效率會不會高一點
HongJie2017-12-07 14:40
非常抱歉這麼久纔回復你。你看文章看得很仔細?。
第一個問題:這裏的 failed 確實永遠都不會爲 true,cancelAcquire() 永遠不會得到執行。那爲什麼要這麼寫呢,如果你看了後面的兩篇,可能會有些體會,這部分其實是用於響應中斷或超時的,你可以參考 doAcquireNanos(int arg, long nanosTimeout)
或 doAcquireInterruptibly(int arg)
。在這個方法中確實是沒用的,這更像是模板代碼吧。
randy2018-12-12 14:36
private void doAcquireSharedInterruptibly(int arg) throws InterruptedException { final Node node = addWaiter(Node.SHARED); boolean failed = true; try { for (;;) { final Node p = node.predecessor(); if (p == head) { int r = tryAcquireShared(arg); if (r >= 0) { setHeadAndPropagate(node, r); p.next = null; // help GC failed = false; return; } } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) throw new InterruptedException(); } } finally { if (failed) cancelAcquire(node); } } jdk1.8裏是跑了個終端異常出來 感覺這樣就可以走到finally執行語句裏了
HongJie2018-12-12 14:39
也有其他讀者已經指出了問題,這段代碼的異常可能發生在 tryAcquire(arg)
這裏,因爲這是依賴於子類來實現的。
HongJie2017-12-07 14:40
第二個問題:你應該說的是 unparkSuccessor(Node node)
這個方法吧。
Node s = node.next;
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;
}
if (s != null)
LockSupport.unpark(s.thread);
首先,第一行代碼先檢測 head 的後繼節點,只有當此時的後繼節點不存在或者這個後繼節點取消了纔開始從後往前找,所以大部分情況下,其實不會發生從後往前遍歷整個隊列的情況。(後繼節點取消很正常,但是某節點在入隊的時候,如果發現前驅是取消狀態,前驅節點是會被請出隊列的)
你說的這個問題的答案在 addWaiter(Node mode)
方法中,看下面的代碼:
Node pred = tail;
if (pred != null) {
node.prev = pred;
// 1. 先設置的 tail
if (compareAndSetTail(pred, node)) {
// 2. 設置前驅節點的後繼節點爲當前節點
pred.next = node;
return node;
}
}
所以,這裏存在併發問題:從前往後尋找不一定能找到剛剛加入隊列的後繼節點。
spjich2017-12-07 15:13
恩是的,確實如此,源碼作者如此設計,巧妙的解決了併發問題。因爲先node.prev = pred; 再compareAndSetTail(pred, node) 再 pred.next = node; 這種操作步奏 是安全的,以此爲前提,如果從前往後找的話 compareAndSetTail(pred, node) 如果執行完成而pred.next = node; 還未來的及執行的話 新加的tail是無法被遍歷到的。多謝作者指點
李曉東2018-02-23 18:03
把addWaiter(mode)的返回值誤認爲就是enq(node)得返回值,看得我懷疑人生。老闆你寫的這麼風趣、細緻、還那麼貼心實在是太nice了,非常感謝
xupeng.zhang2018-02-23 20:18
看了一遍又一遍,一邊看文章,一邊回過頭看代碼,一邊推演整個流程的模型。總算看懂了。
hello23332018-02-26 12:31
感覺Java的monitor和AQS的目的和實現思路很相似,爲什麼還要再實現一遍AQS呢?
HongJie2018-02-26 14:11
monitor 功能太單一了,就是獲取獨佔鎖,
AQS 相比 monitor,功能要豐富很多,比如我們可以設置超時時間,可以用線程中斷進行退出,可以選擇公平/非公平模式等,你可以先看看後面關於 AQS 介紹的第二篇和第三篇,然後就會發現,採用 AQS 可以實現很多靈活的場景
馮健2018-03-03 22:12
大神,有一個地方還是沒搞懂。 acquireQueued方法裏面,第一次調用shouldParkAfterFailedAcquire(p, node)的時候,把前驅節點waitStatus從0改爲-1,然後返回false,回到acquireQueued方法,再嘗試拿一次鎖,然後第二次調用shouldParkAfterFailedAcquire返回true,調用parkAndCheckInterrupt()掛起線程。 那麼,如果在某線程B還沒有掛起之前,前驅節點的線程A發現自己waitStatus爲-1直接unpark,然後剛剛的線程B才掛起。那不就沒人能喚醒它了嗎?它是怎麼保證被喚醒的?
HongJie2018-03-05 10:05
非常抱歉,這次的回覆很不及時~
你應該已經把流程摸清楚了,我就說一點就可以了,你的疑問其實在 unpark() 方法上。
1、如果一個線程 park 了,那麼調用 unpark(thread) 這個線程會被喚醒;
2、如果一個線程先被調用了 unpark,那麼下一個 park(thread) 操作不會掛起線程。
馮健2018-03-05 10:51
原來是這樣,那就沒有問題了。 十分感謝,回覆非常快。
hello23332018-03-06 19:07
假設t1時刻A線程執行addWaiter,在隊尾加入了一個新的nodeA,執行結束; t2時刻,B線程執行unparkSuccessor,在for循環中突然被線程C打斷; 線程C程執行addWaiter,在隊尾加入了一個新的nodeC,執行結束; 再回到線程B,此時線程B仍然無法訪問到節點nodeC是吧?
HongJie2018-03-06 19:14
我表示沒有理解你的意思?
第二句話稍微再具體一點點
你猜2018-03-09 11:49
你是廖紅傑嗎
你猜2018-03-09 11:50
打錯字了,廖鴻傑
HongJie2018-03-09 12:51
你猜?
你猜2018-12-14 11:25
直營那邊的吧
餘謙2018-03-14 16:20
公平鎖對於已經在隊列裏面的線程其實是不公平的,已經在隊列中的線程只能順序獲得鎖,是嗎?
HongJie2018-03-14 16:24
你說的這個問題很簡單,建議你看下下一篇文章,開頭第一小節解釋了公平鎖和非公平鎖的區別。
L2018-03-23 19:50
public final boolean hasQueuedPredecessors() { // The correctness of this depends on head being initialized // before tail and on head.next being accurate if the current // thread is first in queue. Node t = tail; // Read fields in reverse initialization order Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); } 請教大神,爲什麼要先讀tail,再讀head,我猜是爲了增加tail爲null的可能性,可是增加這種可能性的目的呢?
HongJie2018-03-26 18:19
非常抱歉,週末的留言一般我很難及時回覆,然後就只能等工作日想起來的時候回覆了?
你這個問題可真是細啊,細心的程序員~~~
我把評論限制了 1024 個字符,所以分兩條了。。。
看下面這段代碼,如果是第一個 node 進隊列的情況:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// 1. 設置 head
if (compareAndSetHead(new Node()))
// 2. 設置 tail
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
也就是說,先有 head 然後纔有 tail。
HongJie2018-03-26 18:19
回到 hasQueuedPredecessors:
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
有可能的情況就是 t 爲 null,h 不爲 null 對吧,這個時候返回值取決於 h.next。
如果調換一下 Node t = tail;
和 Node h = head;
那麼可能出現 h 爲 null,t 不爲 null,這個方法會返回 false。
但是其實不對的,很可能這個間隙是有節點 enq 成功的。
袋鼠2018-04-01 12:38
看了評論區,關於unparkSuccessor(Node node)方法中從後往前找的描述,我瞭解了從後往前找的原因,但是還有個疑問想請教。
我們不應該從前往後找嗎?這樣,先入列的,等待之後,先獲得鎖嗎?
袋鼠2018-04-01 23:01
我問的這個問題,我再看看,自己再思考一下!!! 先謝過。
HongJie2018-04-02 11:07
非常抱歉哈,一般週末留言的問題我確實經常不能很及時回覆。
Node s = node.next;
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;
}
if (s != null)
LockSupport.unpark(s.thread);
這段代碼不是說就是從後往前找,而是當 s.next “不正常” 的時候纔是從後往前找,大概率情況下,還是不需要遍歷的。
袋鼠2018-04-04 22:52
感謝回覆,又看了幾遍理解了,謝謝!
袋鼠2018-04-01 12:40
自己也是在一步步研究AQS,有些頭大
sky2018-04-09 11:55
大神,爲什麼所說的阻塞隊列不包含 head 節點
HongJie2018-04-09 12:40
因爲 head 節點是當前獨佔鎖的持有者。
sky2018-04-09 12:53
感謝,獲益匪淺
JCM2018-12-12 17:45
Head節點是個空節點吧,之所以有個head節點是滿足FIFO隊列的先進先出,所有的獲取鎖或釋放鎖都是從head指向的節點開始。假設沒有head節點則每次都要從隊尾往前找,所以我認爲這個節點主要是爲了滿足FIFO的特性而存在。
夏天2018-04-09 20:39
我想問一下博主,爲什麼共享鎖的node節點是new了一個節點,獨佔是null呢
HongJie2018-04-09 20:47
其實我也不知道是爲什麼???
大神 Doug Lea 的想法應該是(我怎麼知道他到底怎麼想的):nextWaiter
這個屬性在這個時候是沒用的,因爲它用來實現 Condition,那麼不用白不用,雖然不好理解,可是充分利用資源呀,不然還得自己額外定義一個用來標識模式的屬性。
JCM2018-12-12 17:47
純粹是爲了標識共享鎖和獨佔鎖,沒有其他任何意義吧
sky2018-04-09 22:19
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
博主您好,請問這個裏如果同步狀態tryAcquire(arg)獲取失敗,就構造一個同步節點通過addWaiter(Node.EXCLUSIVE)將節點添加到尾部,如果條件成立執行 selfInterrupt()會中斷當前線程嗎 selfInterrupt()的用途不是太明白,有的書籍上說acquire(int arg)方法對中斷不敏感,不會將獲取同步狀態失敗的線程從同步隊列中移除。煩請博主大大給點指點下哈
HongJie2018-04-09 22:27
你說的這個很簡單,acquireQueued 返回值代表的是:是否被中斷過。但是,不管是否被中斷過,acquireQueued 出來以後,線程的中斷狀態一定是 false,所以如果發生過中斷,要重新設置中斷狀態。
雖然 acquire(int arg) 確實是不關心中斷的,但是它會保持這個狀態,如果客戶端想要知道是否發生過中斷的話,還是可以知道的。因爲中斷情況下,中斷狀態雖然中間丟過,但是 selfInterrupt() 會設置回去。
會實際受到中斷影響的是另一個方法 acquireInterruptibly(int arg),這個方法會通過拋出異常的方式告訴客戶端。
sky2018-04-09 22:40
final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; }
// p == head 說明當前節點雖然進到了阻塞隊列,但是是阻塞隊列的第一個,因爲它的前驅是head // 注意,阻塞隊列不包含head節點,head一般指的是佔有鎖的線程,head後面的才稱爲阻塞隊列 // 所以當前節點可以去試搶一下鎖 // 這裏我們說一下,爲什麼可以去試試: // 首先,它是隊頭,這個是第一個條件,其次,當前的head有可能是剛剛初始化的node, // enq(node) 方法裏面有提到,head是延時初始化的,而且new Node()的時候沒有設置任何線程 // 也就是說,當前的head不屬於任何一個線程,所以作爲隊頭,可以去試一試,
博主,如果是在enq(node)中new head的話,那說明只有head這一個節點把,也就沒有前驅節點了,那麼 Node p = prev; p就爲null了把,就會直接拋異常了把
HongJie2018-04-09 22:47
下次評論的時候,記得要使用 markdown 語法,不然樣式很難看。
你先認真看看吧,自己推導推導,還是挺有意思的,而且整個併發包源碼很多地方都用了 AQS 框架。
sky2018-04-09 23:08
博主:if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; 這裏可以解釋下嗎?上面的沒有看懂。。。
HongJie2018-04-09 23:13
三言兩語還是比較難說清楚的。隨便說幾句不嚴謹的,具體的還是得仔細看每一行代碼。
shouldParkAfterFailedAcquire:判斷是否應該要掛起,如果需要,進到 parkAndCheckInterrupt 進行掛起,等待別人喚醒它。
。。。假設過了很久。。。被喚醒了,需要判斷是否是被中斷喚醒的還是前驅節點用完了鎖正常喚醒自己的。
sky2018-04-09 23:18
好的?
sky2018-04-11 07:29
大神,爲什麼cancelAcquire方法中,要設置node.next = node呢
石黑龍2018-04-12 23:58
不是很明白head 節點是當前獨佔鎖的持有者的意思(註釋也說到head一般指的是佔有鎖的線程),請問從何作出這個判斷? 看代碼感覺整個阻塞隊列(包括head節點)都沒有當前佔有鎖線程的信息。
HongJie2018-04-13 00:06
這是隱含的信息,ReentrantLock 具有排他性,lock() 方法要麼阻塞,要麼順利拿到鎖返回。
當 lock() 返回的時候,我們說當前線程持有了獨佔鎖,而此時的 head 就是當前線程。
(這裏說的情況不考慮連 head 都沒有初始化的場景)
石黑龍2018-04-13 09:45
這樣的說法很容易讓人混淆,應該是得分兩種情況考慮:
1、當前已有別的線程持有鎖的時候,head是指向(head.next)下次解鎖時即將能持有鎖的線程。
2、當持有鎖的線程unlock時, head 指向的就是當前持有鎖的線程 ,但這個時間非常短,因爲head馬上又會指向一下個即將能持有鎖的線程。
jacky2018-04-23 17:44
你好,謝謝你的博客,有一點我想問一下
private void cancelAcquire(Node node) {
...
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
/* 這裏爲什麼如果不滿足條件就喚醒下一位
* 第一個條件我是理解的 如果pred是head 那麼需要喚醒下一位
* 因爲如果前面是pred ==head 那麼這個cancel的線程定時醒來後
* 如果還沒有執行賦值,那麼此時持有鎖的線程正好開始釋放鎖,
* 那麼會喚醒第一個阻塞的線程,假設這個線程正好是上面的線程
* 那麼如果此時這個cancel的線程不傳遞這個喚醒,就會造成
* 其他線程醒不來, 但是爲什麼下面兩個條件的失敗也會喚醒後面的線程?
*/
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
...
}
HongJie2018-04-23 19:00
轉化一下,如果要進入到 else,需要滿足:
(ws = pred.waitStatus) != Node.SIGNAL
&&
ws > 0 || (ws <=0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))
再將其轉化爲下面兩種情況:
-
(ws = pred.waitStatus != Node.SIGNAL) && ws > 0
前驅節點處於 CANCELLED 狀態,顯然是需要喚醒後繼節點的,這條很簡單
-
(ws = pred.waitStatus != Node.SIGNAL)
&& (ws <= 0)
&& (!compareAndSetWaitStatus(pred, ws, Node.SIGNAL))
這種情況下,在將前驅設置爲 SIGNAL 的時候失敗了,我想到的一種情況是,在 CAS 之前前驅設置爲了 CANCELLED
tips: 老鳥的話就算了,新手的話,建議結合 acquireQueued 方法來看,假設裏面的 tryAcquire 拋出異常的場景。
HongJie2018-04-23 19:04
好像漏了一條: pred.thread == null
這個很簡單,head 是 new Node() 的“空節點”,要是不做喚醒後繼節點的話,那。。。你懂的
jacky2018-04-23 21:52
謝謝你的回覆, (ws = pred.waitStatus != Node.SIGNAL) && ws > 0 前驅節點處於 CANCELLED 狀態,顯然是需要喚醒後繼節點的,這條很簡單 如果前驅是cancelled,爲什麼需要喚醒下一位? 我的理解是,要激活下一位的原因,是怕把真正的激活用在了激活了cancel節點,造成後面的線程醒不了,所以需要激活下一位, 因爲需要喚醒下一位,是怕下一位醒不來,如果前驅是cancelled,因爲有第一條的限制,pred != head, 說明在上面Skip cancelled predecessors的時候,pred還不是cancel,如果在執行完這個之後,pred變爲了cancel也沒有關係,因爲pred的醒來晚於當前,所以當前線程不可能浪費一次激活的機會,這個浪費跟我的第一條裏面的方式一樣(釋放鎖激活了一個cancel節點,如果激活不傳遞會有問題),那麼說明當前線程不可能是pred激活的,那麼說明當前線程的喚醒根本不可能是pred喚醒的,也就是不會存在釋放鎖的線程激活第一個cancel節點,cancel節點激活當前這個節點
jacky2018-04-23 21:59
但是如果是這種情況,那麼就會直接走pred == head 這個條件了,而不是走pred == null 這個條件, 所以如果走了這個pred.thread == null 這個條件,說明pred != head, 那麼head 是 new Node() 的“空節點”就不成立了,不知是不是理解有誤
jacky2018-04-23 22:37
我想說的是,這兩個條件執行的前提也是head!=null 如果cancelAcquire一開始過濾cancel節點的時候,pred不是cancel,那麼在底下的時候,如果pred的狀態變爲了cancel,那麼已經說明當前線程不可能是pred喚醒的,因爲pred醒來的時候,當前線程早已經醒了,所以不會是pred,還有會存在喚醒的可能是,正常釋放鎖的時候,會喚醒頭節點,那麼頭節點如果是cancel,然後cancel有可能繼續在接下來cancel節點醒來的時候,去喚醒他一次,但是如果是這種情況,那麼第二個節點在過濾cancel的時候,會過濾掉第一個節點,結果pred變爲了head,這樣又是不滿足第一個條件 pred == head,而不是底下的判斷條件
HongJie2018-04-26 21:14
這兩天實在是忙得很。。。
這裏我應該是犯了低級錯誤,此時的情況不可能是 new Node() 的節點,我想應該是前驅調用了 cancelAcquire 方法置 null 了。
上面一條的評論我就先不回覆你了,我也有些沒完全看懂,改天我看懂了再回復吧。
如果你看懂了,希望你能分享給我 ???
jacky2018-05-04 14:37
ok,一起交流,最近沒有深入下去,現在還是沒有完全get到這三個條件的點
lzf88882018-05-11 17:07
首先,文章寫得非常好,代碼真是非常的精妙。 JVM的monitor最終是使用了(如果升級爲重量鎖)mutex,操作系統級的支持;能否這樣理解,基於AQS的ReentrantLock不需要操作系統的鎖支持了,所以比較輕?而且也不會升級爲重量級鎖,本身只是個等待隊列而已。
Alan2018-05-17 16:03
do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node;這一塊會不會發生pred對象爲null呢????
kaka2018-05-22 11:13
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
( (s = h.next) == null || s.thread != Thread.currentThread() );
}
1隊列非空,2隊列第一個節點(非head)非當前線程 滿足兩個條件返回true, 那麼h != t && (s = h.next) == null 這樣一種場景怎麼理解
HongJie2018-05-26 00:48
不好意思哈,最近太忙了,都沒空回覆你們的問題,看來我真不是一個合格的博主。
你的問題需要到 enq 找答案:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
// 看這裏
t.next = node;
return t;
}
}
}
}
節點入隊時,先成爲 tail 然後才設置前驅的 next 屬性。
那麼對應於你的問題,h != t && h.next == null
對應的就是某個節點現在已經是 tail 了,但是 head.next 還是 null。
雖然文中沒有分析到這個分支,不過還是有些細心的讀者對這個很感興趣的,你也可以往前看下另一個讀者的問題,也是針對這個方法的。
HongJie2018-05-26 00:52
這裏肯定是不會爲 null 的,你可以把你認爲它可能會爲 null 的分析過程描述得詳細一些,這樣我們比較容易在一個頻道上。
PAF_J2018-06-07 10:50
else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } 請教一下,既然線程重入了,說明狀態c>0 ,而且acquires=1那麼,nextc會有<0的情況嗎?
kenneth2018-06-21 17:19
真的太感謝了。看了好多AQS相關的博客,老是看不懂。這篇文章看起來很順。感謝!!!
sharif2018-07-09 17:14
do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0);
pred = pred.prev 這個不是個死循環嘛? 這麼循環怎麼能成立的呀?麻煩大神給解釋一下
HongJie2018-07-09 17:22
這裏沒有死循環呀,一直在沿着隊列往前走,找到一個 waitStatus<=0 的節點
sharif2018-07-09 17:33
我代碼理解錯了,不好意思??
NJU_YZF2018-07-18 14:08
問:阻塞隊列的頭結點是什麼時候初始化的呢? 答: 1. 當前隊列爲空,2.當有線程阻塞的時候 問: 爲什麼不在鎖的構造器裏就先建一個阻塞隊列的頭結點呢? 答: 如果沒有線程競爭鎖的話就是浪費一個節點的空間,Doug Lea大師的註釋如下。可見大師的代碼一點一滴都體現水平。
* CLH queues need a dummy header node to get started. But
* we don't create them on construction, because it would be wasted
* effort if there is never contention. Instead, the node
* is constructed and head and tail pointers are set upon first
* contention.
問:阻塞隊列的頭結點什麼時候會被GC呢? 答:當隊列裏第一個Node節點得到鎖後,該節點會被設置成新的頭結點。那麼原來“老”的頭結點由於沒有任何引用指向它,就會被GC回收。
rainDon2018-07-27 17:52
寫的很牛逼的樣子,其實看起來相當費勁! 我建議作者以例子來說明,例如這篇文章,看了馬上就能懂,回頭看看作者的這篇博客就沒那麼難理解了 https://blog.csdn.net/zs064811/article/details/76996727
pangzhaojie2018-08-20 11:42
如果你不知道爲什麼要看這個,我想告訴你,即使你看懂了所有的細節,你可能也不能把你的業務代碼寫得更好
這句話是什麼意思,沒太明白
pangzhaojie2018-08-21 08:41
隊列中的線程節點node被喚醒後,node直接變爲head,指向原head的指針pred沒有被置null,原head節點沒法回收吧
HongJie2018-08-21 08:49
final boolean acquireQueued(final Node node, int arg) {
...
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 拿到鎖進來這裏
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
...
}
在 setHead(node)
方法裏面:
private void setHead(Node node) {
head = node;
node.thread = null;
// 這個
node.prev = null;
}
pangzhaojie2018-08-21 08:54
看到setHead(node),這個問題忽略吧
趙哲彬2018-08-21 22:13
大神你好。請問一下,head節點的屬性是不是state,這個state表示當前鎖已經被佔用幾次了。阻塞隊列裏的node的屬性是waitStatus,代表着阻塞隊列裏節點的狀態。這是兩個枚舉值完全不相關的屬性嗎?
zhu2018-09-03 15:47
public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非常感謝博主的文章,我這裏有一點不是很理解,就是剛初始化 fifo 的時候,h=new Node();
t = null; 這樣子 h != t && (s = h.next) == null 不是爲 true 嗎, h.next == null; 因爲是剛
初始化的節點,這樣子不是沒有 Predecessors 嘛。那這個明明沒有Predecessors 卻返回 true ;
不是錯了嗎 是我理解錯誤嗎
zhu2018-09-03 21:24
博主還沒來得及看,我自己想出來了,是我太菜了--,只要 head!=tail 就說明有新的節點進來到隊列的尾部了,如果 h.next == null, 說明正在初始化節點中,如果不是初始化中的話,只要 Head 的下一個節點不是 剛進來的 thread 的 的Node,如果是的話就說明沒有正在等待的節點,
對了先補充下這個方法的意思,查詢是否有任何線程等待獲取比當前線程更長的時間。
true如果有排隊線前面的當前線程,並 false如果當前線程在隊列或隊列的頭部是空的
十分感謝博主的文章,對我的幫助非常大。
zhu2018-09-03 22:18
博主對於 cancelAcquire() 的 unparkSuccessor(node); 會不會喚醒兩次呢。 如果 pred == head;然後又剛好 pred 線程也要出同步塊了 也調用了 unparkSuccessor(node); 那這個時候 node.next 是不是 會兩次 unpark() 呢,因爲 unpark 會抵消 park() ,所以 在第一次 unpark 將線程喚醒了,第二次 unpark 將許可證置爲可用的.那麼下次 condition 的 waiter 會不會 被抵消掉呢
HongJie2018-09-03 22:35
非常抱歉,前面的疑惑沒有及時給你回覆,我最近有些忙,文章也已經很久沒有更新了。
你的問題挺有趣的,我沒有仔細去推演每一步,不過我覺得其實這也不是什麼大問題。仔細想想,即使真的是兩次 unpark 了(假設真的如此),大不了就是後面會出現一次 park 直接返回。
對於 park 方法,我們本來就是要假設它有可能會無故返回的,被中斷或者系統的假喚醒,所以這些代碼往往都在循環體中。
zhu2018-09-04 15:36
博主您好,對於
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
setHead(node);
// head 不是一定不爲 null 的嗎,因爲到了這裏,前面的 addWriter() 一定已經入隊過了。Head 也肯定初始化過了
// 爲什麼 判斷一遍 h == null 不夠 還要判斷兩邊,覺得沒有必要判斷呀
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
求博主幫菜鳥解惑下 ☺
HongJie2018-09-04 15:59
我翻了一下,沒找到 ?應該是我們哪裏漏掉了,你找到記得告訴我哈
zhu2018-09-04 16:01
好吧 十分感謝博主
ykge2018-09-08 00:46
博主 :請教個問題 private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; if (compareAndSetTail(t, node)) { // 看這裏 t.next = node; return t; } } } } 看這段代碼 ,tail應該指向的還是head節點 ,但是示例圖解析 tail怎麼到阻塞隊列中了? 而且每插入一個節點 ,tail就跟着後移。很疑惑 ,請博主指點下,謝謝。
琴絃子2018-09-10 16:55
獲取鎖、釋放鎖,阻塞隊列的Node數量不會減少嗎?好像沒看到在哪裏減少阻塞隊列裏面Node的數量?
HongJie2018-09-10 18:04
因爲我們通常並不關心阻塞隊列中到底有多少 Node
jayzc2018-09-29 23:20
爲什麼博主的評論全不見了
Breeze2018-10-23 10:37
你好,有一個問題需要請教。問題比較長,分成了幾部分。
在unparkSuccessor()
裏,如果頭節點的後繼節點不符合條件,會從後往前找一個符合條件的:
// 喚醒後繼節點
// 從上面調用處知道,參數node是head頭結點
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
// 如果head節點當前waitStatus<0, 將其修改爲0
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 下面的代碼就是喚醒後繼節點,但是有可能後繼節點取消了等待(waitStatus==1)
// 從隊尾往前找,找到waitStatus<=0的所有節點中排在最前面的
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
// 從後往前找,仔細看代碼,不必擔心中間有節點取消(waitStatus==1)的情況
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
// 喚醒線程
LockSupport.unpark(s.thread);
}
Breeze2018-10-23 10:38
假設現在找到了等待隊列中間位置的一個節點是符合條件的,然後現在將其喚醒,喚醒後會繼續回到acquireQueued()
方法的循環體裏對吧?此時在for循環裏,先拿到當前被喚醒節點的前驅節點,但此時因爲當前被喚醒節點的前驅節點並不是頭節點,所以它是拿不到鎖的,應該又會在parkAndCheckInterrupt()
裏被掛起吧?
那如果此時這個節點也掛起了,等待隊列裏就沒有獲取到鎖的節點了?這樣一來,所有的節點好像都陷入了阻塞狀態啊。
Breeze2018-10-23 10:39
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
// 到這裏,說明上面的if分支沒有成功,要麼當前node本來就不是隊頭,
// 要麼就是tryAcquire(arg)沒有搶贏別人,繼續往下看
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
Breeze2018-10-23 11:30
我好像搞明白了,這裏會遍歷到最前面的那個合格的節點,然後在shouldParkAfterFailedAcquire裏會剔除掉被取消的節點,此時這個節點就會變成等待隊列的第一個節點,然後進入下次搶鎖,就能搶到了🤣
HongJie2018-10-23 15:29
😂😂😂可以!
JCM2018-12-12 17:54
因爲head節點的waitstatus永遠=-1,博主不知對不對,請指點!
小航2018-11-23 22:13
// => 是爲了應對在經過這個方法後,node已經是head的直接後繼節點了。剩下的讀者自己想想吧。 博主我想了半天。你這句話的意思是不是防止通知過早啊
stillywud5 小時前
CAS是 CompareAndSwap
本文關注以下幾點內容:
- 深入理解 ReentrantLock 公平鎖和非公平鎖的區別
- 深入分析 AbstractQueuedSynchronizer 中的 ConditionObject
- 深入理解 java 線程中斷和 InterruptedException 異常
基本上本文把以上幾點都說清楚了,我假設讀者看過上一篇文章中對 AbstractQueuedSynchronizer 的介紹,當然如果你已經熟悉 AQS 中的獨佔鎖了,那也可以直接看這篇。各小節之間基本上沒什麼關係,大家可以只關注自己感興趣的部分。
公平鎖和非公平鎖
ReentrantLock 默認採用非公平鎖,除非你在構造方法中傳入參數 true 。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
公平鎖的 lock 方法:
static final class FairSync extends Sync {
final void lock() {
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 1. 和非公平鎖相比,這裏多了一個判斷:是否有線程在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
非公平鎖的 lock 方法:
static final class NonfairSync extends Sync {
final void lock() {
// 2. 和公平鎖相比,這裏會直接先進行一次CAS,成功就返回了
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// AbstractQueuedSynchronizer.acquire(int arg)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
總結:公平鎖和非公平鎖只有兩處不同:
- 非公平鎖在調用 lock 後,首先就會調用 CAS 進行一次搶鎖,如果這個時候恰巧鎖沒有被佔用,那麼直接就獲取到鎖返回了。
- 非公平鎖在 CAS 失敗後,和公平鎖一樣都會進入到 tryAcquire 方法,在 tryAcquire 方法中,如果發現鎖這個時候被釋放了(state == 0),非公平鎖會直接 CAS 搶鎖,但是公平鎖會判斷等待隊列是否有線程處於等待狀態,如果有則不去搶鎖,乖乖排到後面。
公平鎖和非公平鎖就這兩點區別,如果這兩次 CAS 都不成功,那麼後面非公平鎖和公平鎖是一樣的,都要進入到阻塞隊列等待喚醒。
相對來說,非公平鎖會有更好的性能,因爲它的吞吐量比較大。當然,非公平鎖讓獲取鎖的時間變得更加不確定,可能會導致在阻塞隊列中的線程長期處於飢餓狀態。
Condition
Tips: 這裏重申一下,要看懂這個,必須要先看懂上一篇關於 AbstractQueuedSynchronizer 的介紹,或者你已經有相關的知識了,否則這節肯定是看不懂的。
我們先來看看 Condition 的使用場景,Condition 經常可以用在生產者-消費者的場景中,請看 Doug Lea 給出的這個例子:
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class BoundedBuffer {
final Lock lock = new ReentrantLock();
// condition 依賴於 lock 來產生
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100];
int putptr, takeptr, count;
// 生產
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 隊列已滿,等待,直到 not full 才能繼續生產
items[putptr] = x;
if (++putptr == items.length) putptr = 0;
++count;
notEmpty.signal(); // 生產成功,隊列已經 not empty 了,發個通知出去
} finally {
lock.unlock();
}
}
// 消費
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0)
notEmpty.await(); // 隊列爲空,等待,直到隊列 not empty,才能繼續消費
Object x = items[takeptr];
if (++takeptr == items.length) takeptr = 0;
--count;
notFull.signal(); // 被我消費掉一個,隊列 not full 了,發個通知出去
return x;
} finally {
lock.unlock();
}
}
}
(ArrayBlockingQueue 採用這種方式實現了生產者-消費者,所以請只把這個例子當做學習例子,實際生產中可以直接使用 ArrayBlockingQueue)
我們常用 obj.wait(),obj.notify() 或 obj.notifyAll() 來實現相似的功能,但是,它們是基於對象的監視器鎖的。需要深入瞭解這幾個方法的讀者,可以參考我的另一篇文章《深入分析 java 8 編程語言規範:Threads and Locks》。而這裏說的 Condition 是基於 ReentrantLock 實現的,而 ReentrantLock 是依賴於 AbstractQueuedSynchronizer 實現的。
在往下看之前,讀者心裏要有一個整體的概念。condition 是依賴於 ReentrantLock 的,不管是調用 await 進入等待還是 signal 喚醒,都必須獲取到鎖才能進行操作。
每個 ReentrantLock 實例可以通過調用多次 newCondition 產生多個 ConditionObject 的實例:
final ConditionObject newCondition() {
return new ConditionObject();
}
我們首先來看下我們關注的 Condition 的實現類 AbstractQueuedSynchronizer
類中的 ConditionObject
。
public class ConditionObject implements Condition, java.io.Serializable {
private static final long serialVersionUID = 1173984872572414699L;
// 條件隊列的第一個節點
// 不要管這裏的關鍵字 transient,是不參與序列化的意思
private transient Node firstWaiter;
// 條件隊列的最後一個節點
private transient Node lastWaiter;
......
在上一篇介紹 AQS 的時候,我們有一個阻塞隊列,用於保存等待獲取鎖的線程的隊列。這裏我們引入另一個概念,叫條件隊列(condition queue),我畫了一張簡單的圖用來說明這個。
這裏的阻塞隊列如果叫做同步隊列(sync queue)其實比較貼切,不過爲了和前篇呼應,我就繼續使用阻塞隊列了。記住這裏的兩個概念,阻塞隊列和條件隊列。
這裏,我們簡單回顧下 Node 的屬性:
volatile int waitStatus; // 可取值 0、CANCELLED(1)、SIGNAL(-1)、CONDITION(-2)、PROPAGATE(-3) volatile Node prev; volatile Node next; volatile Thread thread; Node nextWaiter;
prev 和 next 用於實現阻塞隊列的雙向鏈表,nextWaiter 用於實現條件隊列的單向鏈表
基本上,把這張圖看懂,你也就知道 condition 的處理流程了。所以,我先簡單解釋下這圖,然後再具體地解釋代碼實現。
- 我們知道一個 ReentrantLock 實例可以通過多次調用 newCondition() 來產生多個 Condition 實例,這裏對應 condition1 和 condition2。注意,ConditionObject 只有兩個屬性 firstWaiter 和 lastWaiter;
- 每個 condition 有一個關聯的條件隊列,如線程 1 調用 condition1.await() 方法即可將當前線程 1 包裝成 Node 後加入到條件隊列中,然後阻塞在這裏,不繼續往下執行,條件隊列是一個單向鏈表;
- 調用 condition1.signal() 會將condition1 對應的條件隊列的 firstWaiter 移到阻塞隊列的隊尾,等待獲取鎖,獲取鎖後 await 方法返回,繼續往下執行。
我這裏說的 1、2、3 是最簡單的流程,沒有考慮中斷、signalAll、還有帶有超時參數的 await 方法等,不過把這裏弄懂是這節的主要目的。
同時,從圖中也可以很直觀地看出,哪些操作是線程安全的,哪些操作是線程不安全的。
這個圖看懂後,下面的代碼分析就簡單了。
接下來,我們一步步按照流程來走代碼分析,我們先來看看 wait 方法:
// 首先,這個方法是可被中斷的,不可被中斷的是另一個方法 awaitUninterruptibly()
// 這個方法會阻塞,直到調用 signal 方法(指 signal() 和 signalAll(),下同),或被中斷
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加到 condition 的條件隊列中
Node node = addConditionWaiter();
// 釋放鎖,返回值是釋放鎖之前的 state 值
int savedState = fullyRelease(node);
int interruptMode = 0;
// 這裏退出循環有兩種情況,之後再仔細分析
// 1. isOnSyncQueue(node) 返回 true,即當前 node 已經轉移到阻塞隊列了
// 2. checkInterruptWhileWaiting(node) != 0 會到 break,然後退出循環,代表的是線程中斷
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 被喚醒後,將進入阻塞隊列,等待獲取鎖
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
其實,我大體上也把整個 await 過程說得十之八九了,下面我們分步把上面的幾個點用源碼說清楚。
1. 將節點加入到條件隊列
addConditionWaiter() 是將當前節點加入到條件隊列,看圖我們知道,這種條件隊列內的操作是線程安全的。
// 將當前線程對應的節點入隊,插入隊尾
private Node addConditionWaiter() {
Node t = lastWaiter;
// 如果條件隊列的最後一個節點取消了,將其清除出去
if (t != null && t.waitStatus != Node.CONDITION) {
// 這個方法會遍歷整個條件隊列,然後會將已取消的所有節點清除出隊列
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Thread.currentThread(), Node.CONDITION);
// 如果隊列爲空
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
在addWaiter 方法中,有一個 unlinkCancelledWaiters() 方法,該方法用於清除隊列中已經取消等待的節點。
當 await 的時候如果發生了取消操作(這點之後會說),或者是在節點入隊的時候,發現最後一個節點是被取消的,會調用一次這個方法。
// 等待隊列是一個單向鏈表,遍歷鏈表將已經取消等待的節點清除出去
// 純屬鏈表操作,很好理解,看不懂多看幾遍就可以了
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = null;
while (t != null) {
Node next = t.nextWaiter;
// 如果節點的狀態不是 Node.CONDITION 的話,這個節點就是被取消的
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
if (trail == null)
firstWaiter = next;
else
trail.nextWaiter = next;
if (next == null)
lastWaiter = trail;
}
else
trail = t;
t = next;
}
}
2. 完全釋放獨佔鎖
回到 wait 方法,節點入隊了以後,會調用 int savedState = fullyRelease(node);
方法釋放鎖,注意,這裏是完全釋放獨佔鎖,因爲 ReentrantLock 是可以重入的。
// 首先,我們要先觀察到返回值 savedState 代表 release 之前的 state 值
// 對於最簡單的操作:先 lock.lock(),然後 condition1.await()。
// 那麼 state 經過這個方法由 1 變爲 0,鎖釋放,此方法返回 1
// 相應的,如果 lock 重入了 n 次,savedState == n
// 如果這個方法失敗,會將節點設置爲"取消"狀態,並拋出異常 IllegalMonitorStateException
final int fullyRelease(Node node) {
boolean failed = true;
try {
int savedState = getState();
// 這裏使用了當前的 state 作爲 release 的參數,也就是完全釋放掉鎖,將 state 置爲 0
if (release(savedState)) {
failed = false;
return savedState;
} else {
throw new IllegalMonitorStateException();
}
} finally {
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
3. 等待進入阻塞隊列
釋放掉鎖以後,接下來是這段,這邊會自旋,如果發現自己還沒到阻塞隊列,那麼掛起,等待被轉移到阻塞隊列。
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 線程掛起
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
isOnSyncQueue(Node node) 用於判斷節點是否已經轉移到阻塞隊列了:
// 在節點入條件隊列的時候,初始化時設置了 waitStatus = Node.CONDITION
// 前面我提到,signal 的時候需要將節點從條件隊列移到阻塞隊列,
// 這個方法就是判斷 node 是否已經移動到阻塞隊列了
final boolean isOnSyncQueue(Node node) {
// 移動過去的時候,node 的 waitStatus 會置爲 0,這個之後在說 signal 方法的時候會說到
// 如果 waitStatus 還是 Node.CONDITION,也就是 -2,那肯定就是還在條件隊列中
// 如果 node 的前驅 prev 指向還是 null,說明肯定沒有在 阻塞隊列
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 如果 node 已經有後繼節點 next 的時候,那肯定是在阻塞隊列了
if (node.next != null)
return true;
// 這個方法從阻塞隊列的隊尾開始從後往前遍歷找,如果找到相等的,說明在阻塞隊列,否則就是不在阻塞隊列
// 可以通過判斷 node.prev() != null 來推斷出 node 在阻塞隊列嗎?答案是:不能。
// 這個可以看上篇 AQS 的入隊方法,首先設置的是 node.prev 指向 tail,
// 然後是 CAS 操作將自己設置爲新的 tail,可是這次的 CAS 是可能失敗的。
// 調用這個方法的時候,往往我們需要的就在隊尾的部分,所以一般都不需要完全遍歷整個隊列的
return findNodeFromTail(node);
}
// 從同步隊列的隊尾往前遍歷,如果找到,返回 true
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
回到前面的循環,isOnSyncQueue(node) 返回 false 的話,那麼進到 LockSupport.park(this);
這裏線程掛起。
4. signal 喚醒線程,轉移到阻塞隊列
爲了大家理解,這裏我們先看喚醒操作,因爲剛剛到 LockSupport.park(this); 把線程掛起了,等待喚醒。
喚醒操作通常由另一個線程來操作,就像生產者-消費者模式中,如果線程因爲等待消費而掛起,那麼當生產者生產了一個東西后,會調用 signal 喚醒正在等待的線程來消費。
// 喚醒等待了最久的線程
// 其實就是,將這個線程對應的 node 從條件隊列轉移到阻塞隊列
public final void signal() {
// 調用 signal 方法的線程必須持有當前的獨佔鎖
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first);
}
// 從條件隊列隊頭往後遍歷,找出第一個需要轉移的 node
// 因爲前面我們說過,有些線程會取消排隊,但是還在隊列中
private void doSignal(Node first) {
do {
// 將 firstWaiter 指向 first 節點後面的第一個
// 如果將隊頭移除後,後面沒有節點在等待了,那麼需要將 lastWaiter 置爲 null
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 因爲 first 馬上要被移到阻塞隊列了,和條件隊列的鏈接關係在這裏斷掉
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
// 這裏 while 循環,如果 first 轉移不成功,那麼選擇 first 後面的第一個節點進行轉移,依此類推
}
// 將節點從條件隊列轉移到阻塞隊列
// true 代表成功轉移
// false 代表在 signal 之前,節點已經取消了
final boolean transferForSignal(Node node) {
// CAS 如果失敗,說明此 node 的 waitStatus 已不是 Node.CONDITION,說明節點已經取消,
// 既然已經取消,也就不需要轉移了,方法返回,轉移後面一個節點
// 否則,將 waitStatus 置爲 0
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// enq(node): 自旋進入阻塞隊列的隊尾
// 注意,這裏的返回值 p 是 node 在阻塞隊列的前驅節點
Node p = enq(node);
int ws = p.waitStatus;
// ws > 0 說明 node 在阻塞隊列中的前驅節點取消了等待鎖,直接喚醒 node 對應的線程。喚醒之後會怎麼樣,後面再解釋
// 如果 ws <= 0, 那麼 compareAndSetWaitStatus 將會被調用,上篇介紹的時候說過,節點入隊後,需要把前驅節點的狀態設爲 Node.SIGNAL(-1)
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 如果前驅節點取消或者 CAS 失敗,會進到這裏喚醒線程,之後的操作看下一節
LockSupport.unpark(node.thread);
return true;
}
正常情況下,ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL)
這句中,ws <= 0,而且 compareAndSetWaitStatus(p, ws, Node.SIGNAL) 會返回 true,所以一般也不會進去 if 語句塊中喚醒 node 對應的線程。然後這個方法返回 true,也就意味着 signal 方法結束了,節點進入了阻塞隊列。
假設發生了阻塞隊列中的前驅節點取消等待,或者 CAS 失敗,只要喚醒線程,讓其進到下一步即可。
5. 喚醒後檢查中斷狀態
上一步 signal 之後,我們的線程由條件隊列轉移到了阻塞隊列,之後就準備獲取鎖了。只要重新獲取到鎖了以後,繼續往下執行。
等線程從掛起中恢復過來,繼續往下看
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 線程掛起
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
先解釋下 interruptMode。interruptMode 可以取值爲 REINTERRUPT(1),THROW_IE(-1),0
- REINTERRUPT: 代表 await 返回的時候,需要重新設置中斷狀態
- THROW_IE: 代表 await 返回的時候,需要拋出 InterruptedException 異常
- 0 :說明在 await 期間,沒有發生中斷
有以下三種情況會讓 LockSupport.park(this); 這句返回繼續往下執行:
- 常規路勁。signal -> 轉移節點到阻塞隊列 -> 獲取了鎖(unpark)
- 線程中斷。在 park 的時候,另外一個線程對這個線程進行了中斷
- signal 的時候我們說過,轉移以後的前驅節點取消了,或者對前驅節點的CAS操作失敗了
- 假喚醒。這個也是存在的,和 Object.wait() 類似,都有這個問題
線程喚醒後第一步是調用 checkInterruptWhileWaiting(node) 這個方法,此方法用於判斷是否在線程掛起期間發生了中斷,如果發生了中斷,是 signal 調用之前中斷的,還是 signal 之後發生的中斷。
// 1. 如果在 signal 之前已經中斷,返回 THROW_IE
// 2. 如果是 signal 之後中斷,返回 REINTERRUPT
// 3. 沒有發生中斷,返回 0
private int checkInterruptWhileWaiting(Node node) {
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
Thread.interrupted():如果當前線程已經處於中斷狀態,那麼該方法返回 true,同時將中斷狀態重置爲 false,所以,纔有後續的
重新中斷(REINTERRUPT)
的使用。
看看怎麼判斷是 signal 之前還是之後發生的中斷:
// 只有線程處於中斷狀態,纔會調用此方法
// 如果需要的話,將這個已經取消等待的節點轉移到阻塞隊列
// 返回 true:如果此線程在 signal 之前被取消,
final boolean transferAfterCancelledWait(Node node) {
// 用 CAS 將節點狀態設置爲 0
// 如果這步 CAS 成功,說明是 signal 方法之前發生的中斷,因爲如果 signal 先發生的話,signal 中會將 waitStatus 設置爲 0
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
// 將節點放入阻塞隊列
// 這裏我們看到,即使中斷了,依然會轉移到阻塞隊列
enq(node);
return true;
}
// 到這裏是因爲 CAS 失敗,肯定是因爲 signal 方法已經將 waitStatus 設置爲了 0
// signal 方法會將節點轉移到阻塞隊列,但是可能還沒完成,這邊自旋等待其完成
// 當然,這種事情還是比較少的吧:signal 調用之後,沒完成轉移之前,發生了中斷
while (!isOnSyncQueue(node))
Thread.yield();
return false;
}
這裏再說一遍,即使發生了中斷,節點依然會轉移到阻塞隊列。
到這裏,大家應該都知道這個 while 循環怎麼退出了吧。要麼中斷,要麼轉移成功。
6. 獲取獨佔鎖
while 循環出來以後,下面是這段代碼:
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
由於 while 出來後,我們確定節點已經進入了阻塞隊列,準備獲取鎖。
這裏的 acquireQueued(node, savedState) 的第一個參數 node 之前已經經過 enq(node) 進入了隊列,參數 savedState 是之前釋放鎖前的 state,這個方法返回的時候,代表當前線程獲取了鎖,而且 state == savedState了。
注意,前面我們說過,不管有沒有發生中斷,都會進入到阻塞隊列,而 acquireQueued(node, savedState) 的返回值就是代表線程是否被中斷。如果返回 true,說明被中斷了,而且 interruptMode != THROW_IE,說明在 signal 之前就發生中斷了,這裏將 interruptMode 設置爲 REINTERRUPT,用於待會重新中斷。
繼續往下:
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
本着一絲不苟的精神,這邊說說 node.nextWaiter != null
怎麼滿足。我前面也說了 signal 的時候會將節點轉移到阻塞隊列,有一步是 node.nextWaiter = null,將斷開節點和條件隊列的聯繫。
可是,在判斷髮生中斷的情況下,是 signal 之前還是之後發生的?
這部分的時候,我也介紹了,如果 signal 之前就中斷了,也需要將節點進行轉移到阻塞隊列,這部分轉移的時候,是沒有設置 node.nextWaiter = null 的。
之前我們說過,如果有節點取消,也會調用 unlinkCancelledWaiters 這個方法,就是這裏了。
7. 處理中斷狀態
到這裏,我們終於可以好好說下這個 interruptMode 幹嘛用了。
- 0:什麼都不做。
- THROW_IE:await 方法拋出 InterruptedException 異常
- REINTERRUPT:重新中斷當前線程
private void reportInterruptAfterWait(int interruptMode)
throws InterruptedException {
if (interruptMode == THROW_IE)
throw new InterruptedException();
else if (interruptMode == REINTERRUPT)
selfInterrupt();
}
爲什麼這麼處理?這部分的知識在本文的最後一節
* 帶超時機制的 await
經過前面的 7 步,整個 ConditionObject 類基本上都分析完了,接下來簡單分析下帶超時機制的 await 方法。
public final long awaitNanos(long nanosTimeout)
throws InterruptedException
public final boolean awaitUntil(Date deadline)
throws InterruptedException
public final boolean await(long time, TimeUnit unit)
throws InterruptedException
這三個方法都差不多,我們就挑一個出來看看吧:
public final boolean await(long time, TimeUnit unit)
throws InterruptedException {
// 等待這麼多納秒
long nanosTimeout = unit.toNanos(time);
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
// 當前時間 + 等待時長 = 過期時間
final long deadline = System.nanoTime() + nanosTimeout;
// 用於返回 await 是否超時
boolean timedout = false;
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
// 時間到啦
if (nanosTimeout <= 0L) {
// 這裏因爲要 break 取消等待了。取消等待的話一定要調用 transferAfterCancelledWait(node) 這個方法
// 如果這個方法返回 true,在這個方法內,將節點轉移到阻塞隊列成功
// 返回 false 的話,說明 signal 已經發生,signal 方法將節點轉移了。也就是說沒有超時嘛
timedout = transferAfterCancelledWait(node);
break;
}
// spinForTimeoutThreshold 的值是 1000 納秒,也就是 1 毫秒
// 也就是說,如果不到 1 毫秒了,那就不要選擇 parkNanos 了,自旋的性能反而更好
if (nanosTimeout >= spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
// 得到剩餘時間
nanosTimeout = deadline - System.nanoTime();
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
return !timedout;
}
超時的思路還是很簡單的,不帶超時參數的 await 是 park,然後等待別人喚醒。而現在就是調用 parkNanos 方法來休眠指定的時間,醒來後判斷是否 signal 調用了,調用了就是沒有超時,否則就是超時了。超時的話,自己來進行轉移到阻塞隊列,然後搶鎖。
* 不拋出 InterruptedException 的 await
關於 Condition 最後一小節了。
public final void awaitUninterruptibly() {
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
boolean interrupted = false;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if (Thread.interrupted())
interrupted = true;
}
if (acquireQueued(node, savedState) || interrupted)
selfInterrupt();
}
很簡單,我就不廢話了。
AbstractQueuedSynchronizer 獨佔鎖的取消排隊
這篇文章說的是 AbstractQueuedSynchronizer,只不過好像 Condition 說太多了,趕緊把思路拉回來。
接下來,我想說說怎麼取消對鎖的競爭?
上篇文章提到過,最重要的方法是這個,我們要在這裏面找答案:
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
首先,到這個方法的時候,節點一定是入隊成功的。
我把 parkAndCheckInterrupt() 代碼貼過來:
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
這兩段代碼聯繫起來看,是不是就清楚了。
如果我們要取消一個線程的排隊,我們需要在另外一個線程中對其進行中斷。比如某線程調用 lock() 老久不返回,我想中斷它。一旦對其進行中斷,此線程會從 LockSupport.park(this);
中喚醒,然後 Thread.interrupted();
返回 true。
我們發現一個問題,即使是中斷喚醒了這個線程,也就只是設置了 interrupted = true
然後繼續下一次循環。而且,由於 Thread.interrupted();
會清除中斷狀態,第二次進 parkAndCheckInterrupt 的時候,返回會是 false。
所以,我們要看到,在這個方法中,interrupted 只是用來記錄是否發生了中斷,然後用於方法返回值,其他沒有做任何相關事情。
所以,我們看外層方法怎麼處理 acquireQueued 返回 false 的情況。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
所以說,lock() 方法處理中斷的方法就是,你中斷歸中斷,我搶鎖還是照樣搶鎖,幾乎沒關係,只是我搶到鎖了以後,設置線程的中斷狀態而已,也不拋出任何異常出來。調用者獲取鎖後,可以去檢查是否發生過中斷,也可以不理會。
來條分割線。有沒有被騙的感覺,我說了一大堆,可是和取消沒有任何關係啊。
我們來看 ReentrantLock 的另一個 lock 方法:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
方法上多了個 throws InterruptedException
,經過前面那麼多知識的鋪墊,這裏我就不再囉裏囉嗦了。
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
繼續往裏:
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())
// 就是這裏了,一旦異常,馬上結束這個方法,拋出異常。
// 這裏不再只是標記這個方法的返回值代表中斷狀態
// 而是直接拋出異常,而且外層也不捕獲,一直往外拋到 lockInterruptibly
throw new InterruptedException();
}
} finally {
// 如果通過 InterruptedException 異常出去,那麼 failed 就是 true 了
if (failed)
cancelAcquire(node);
}
}
既然到這裏了,順便說說 cancelAcquire 這個方法吧:
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
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);
}
node.next = node; // help GC
}
}
到這裏,我想我應該把取消排隊這件事說清楚了吧。
再說 java 線程中斷和 InterruptedException 異常
在之前的文章中,我們接觸了大量的中斷,這邊算是個總結吧。如果你完全熟悉中斷了,沒有必要再看這節,本節爲新手而寫。
線程中斷
首先,我們要明白,中斷不是類似 linux 裏面的命令 kill -9 pid,不是說我們中斷某個線程,這個線程就停止運行了。中斷代表線程狀態,每個線程都關聯了一箇中斷狀態,是一個 true 或 false 的 boolean 值,初始值爲 false。
關於中斷狀態,我們需要重點關注以下幾個方法:
// Thread 類中的實例方法,持有線程實例引用即可檢測線程中斷狀態
public boolean isInterrupted() {}
// Thread 中的靜態方法,檢測調用這個方法的線程是否已經中斷
// 注意:這個方法返回中斷狀態的同時,會將此線程的中斷狀態重置爲 false
// 所以,如果我們連續調用兩次這個方法的話,第二次的返回值肯定就是 false 了
public static boolean interrupted() {}
// Thread 類中的實例方法,用於設置一個線程的中斷狀態爲 true
public void interrupt() {}
我們說中斷一個線程,其實就是設置了線程的 interrupted status 爲 true,至於說被中斷的線程怎麼處理這個狀態,那是那個線程自己的事。如以下代碼:
while (!Thread.interrupted()) {
doWork();
System.out.println("我做完一件事了,準備做下一件,如果沒有其他線程中斷我的話");
}
當然,中斷除了是線程狀態外,還有其他含義,否則也不需要專門搞一個這個概念出來了。
如果線程處於以下三種情況,那麼當線程被中斷的時候,能自動感知到:
-
來自 Object 類的 wait()、wait(long)、wait(long, int),
來自 Thread 類的 join()、join(long)、join(long, int)、sleep(long)、sleep(long, int)
這幾個方法的相同之處是,方法上都有: throws InterruptedException
如果線程阻塞在這些方法上(我們知道,這些方法會讓當前線程阻塞),這個時候如果其他線程對這個線程進行了中斷,那麼這個線程會從這些方法中立即返回,拋出 InterruptedException 異常,同時重置中斷狀態爲 false。
-
實現了 InterruptibleChannel 接口的類中的一些 I/O 阻塞操作,如 DatagramChannel 中的 connect 方法和 receive 方法等
如果線程阻塞在這裏,中斷線程會導致這些方法拋出 ClosedByInterruptException 並重置中斷狀態。
-
Selector 中的 select 方法,這個有機會我們在講 NIO 的時候說
一旦中斷,方法立即返回
對於以上 3 種情況是最特殊的,因爲他們能自動感知到中斷(這裏說自動,當然也是基於底層實現),並且在做出相應的操作後都會重置中斷狀態爲 false。
那是不是隻有以上 3 種方法能自動感知到中斷呢?不是的,如果線程阻塞在 LockSupport.park(Object obj) 方法,也叫掛起,這個時候的中斷也會導致線程喚醒,但是喚醒後不會重置中斷狀態,所以喚醒後去檢測中斷狀態將是 true。
InterruptedException 概述
它是一個特殊的異常,不是說 JVM 對其有特殊的處理,而是它的使用場景比較特殊。通常,我們可以看到,像 Object 中的 wait() 方法,ReentrantLock 中的 lockInterruptibly() 方法,Thread 中的 sleep() 方法等等,這些方法都帶有 throws InterruptedException
,我們通常稱這些方法爲阻塞方法(blocking method)。
阻塞方法一個很明顯的特徵是,它們需要花費比較長的時間(不是絕對的,只是說明時間不可控),還有它們的方法結束返回往往依賴於外部條件,如 wait 方法依賴於其他線程的 notify,lock 方法依賴於其他線程的 unlock等等。
當我們看到方法上帶有 throws InterruptedException
時,我們就要知道,這個方法應該是阻塞方法,我們如果希望它能早點返回的話,我們往往可以通過中斷來實現。
除了幾個特殊類(如 Object,Thread等)外,感知中斷並提前返回是通過輪詢中斷狀態來實現的。我們自己需要寫可中斷的方法的時候,就是通過在合適的時機(通常在循環的開始處)去判斷線程的中斷狀態,然後做相應的操作(通常是方法直接返回或者拋出異常)。當然,我們也要看到,如果我們一次循環花的時間比較長的話,那麼就需要比較長的時間才能注意到線程中斷了。
處理中斷
一旦中斷髮生,我們接收到了這個信息,然後怎麼去處理中斷呢?本小節將簡單分析這個問題。
我們經常會這麼寫代碼:
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
// ignore
}
// go on
當 sleep 結束繼續往下執行的時候,我們往往都不知道這塊代碼是真的 sleep 了 10 秒,還是隻休眠了 1 秒就被中斷了。這個代碼的問題在於,我們將這個異常信息吞掉了。(對於 sleep 方法,我相信大部分情況下,我們都不在意是否是中斷了,這裏是舉例)
AQS 的做法很值得我們借鑑,我們知道 ReentrantLock 有兩種 lock 方法:
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
前面我們提到過,lock() 方法不響應中斷。如果 thread1 調用了 lock() 方法,過了很久還沒搶到鎖,這個時候 thread2 對其進行了中斷,thread1 是不響應這個請求的,它會繼續搶鎖,當然它不會把“被中斷”這個信息扔掉。我們可以看以下代碼:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 我們看到,這裏也沒做任何特殊處理,就是記錄下來中斷狀態。
// 這樣,如果外層方法需要去檢測的時候,至少我們沒有把這個信息丟了
selfInterrupt();// Thread.currentThread().interrupt();
}
而對於 lockInterruptibly() 方法,因爲其方法上面有 throws InterruptedException
,這個信號告訴我們,如果我們要取消線程搶鎖,直接中斷這個線程即可,它會立即返回,拋出 InterruptedException 異常。
在併發包中,有非常多的這種處理中斷的例子,提供兩個方法,分別爲響應中斷和不響應中斷,對於不響應中斷的方法,記錄中斷而不是丟失這個信息。如 Condition 中的兩個方法就是這樣的:
void await() throws InterruptedException;
void awaitUninterruptibly();
通常,如果方法會拋出 InterruptedException 異常,往往方法體的第一句就是:
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); ...... }
熟練使用中斷,對於我們寫出優雅的代碼是有幫助的,也有助於我們分析別人的源碼。
參考:https://www.ibm.com/developerworks/library/j-jtp05236/index.html
翻譯:https://www.ibm.com/developerworks/cn/java/j-jtp05236.html
總結
這部分就留着讀者吧,希望讀者看完有所收穫。
評論區
孫同學2018-03-25 10:12
大神你好!
我有一個問題,在:“6獲取獨佔鎖”這一節。
文中說:“注意,前面我們說過,不管有沒有發生中斷,都會進入到阻塞隊列,而 acquireQueued(node, savedState) 的返回值就是代表線程是否被中斷。如果返回 true,說明被中斷了,而且 interruptMode != THROW_IE,說明在 signal 之前就發生中斷了,這裏將 interruptMode 設置爲 REINTERRUPT,用於待會重新中斷。”
這裏,“而且 interruptMode != THROW_IE,說明在 signal 之前就發生中斷了”我不太理解。
如果是signal前發生中斷的話,不是會返回THROW_IE嗎?
HongJie2018-03-26 17:27
非常抱歉,週末的留言一般我很難及時回覆,然後就只能等工作日想起來的時候回覆了?
你看得非常細心。初略看了一下,應該是我寫錯了,後面有時間的時候,我再推敲推敲~~~
Adrian2018-04-02 11:00
HongJie大哥,您好。在第3節isOnSyncQueue(Node node)方法裏面。 文中是: // 如果 waitStatus 還是 Node.CONDITION,也就是 2,那肯定就是還在條件隊列中
這裏的Node.CONDITION應該是 -2 纔對吧。。。
HongJie2018-04-02 11:02
抱歉,犯低級錯誤了~~~謝謝!
NJU_YZF2018-07-19 10:43
await()函數裏 while (!isOnSyncQueue(node))可以把while換成if嗎?爲什麼需要while?
NJU_YZF2018-07-19 11:46
如果signal調用了之後,isOnSyncQueue卻還是返回false,那麼是不是意味着該線程一致阻塞下去?
HongJie2018-07-25 14:15
因爲 park 方法可能被假喚醒,所以碰到 wait(...)、park(...) 這些的時候,基本上都是這麼處理
HongJie2018-07-25 14:20
你這個問題你可以好好考慮一下
行者2018-07-27 15:49
####大神,請問文章中講到的生成者-消費者模式,我看很多資料都是定義兩個Condition,但我覺得只用一個Condition是不是效果也是一樣的,在put的時候檢測如果滿了則wait,在get的時候檢測如果爲空則wait,爲什麼大家都寫成兩個condition呢,只是爲了在名字上區分開兩種情況嗎?#
zhongxuan2018-07-27 15:56
"6. 獲取獨佔鎖 while 循環出來以後,下面是這段代碼:
if (acquireQueued(node, savedState) && interruptMode != THROW_IE) interruptMode = REINTERRUPT; 由於 while 出來後,我們確定節點已經進入了阻塞隊列,準備獲取鎖。" 這裏我看了半天代碼, 發現在退出while的時候,就已經獲取了獨佔鎖, 也就是說signal 僅僅是transfer 隊列,然後由signal的線程unlock釋放鎖,阻塞隊列裏的線程獲取鎖, 所以到了if (acquireQueued(node, savedState) && interruptMode != THROW_IE) 的時候,線程早已經獲取鎖了,這裏應該是僅僅設置 savedState 鎖狀態把?
zhongxuan2018-07-27 15:58
不對,應該還有 setHead 等操作。是吧
HongJie2018-07-27 16:42
感覺你還沒有理解透 Condition 的模型,這裏只用一個 Condition 是不行的。
還有,要實現生產者-消費者有很多種辦法,你可以把其他方式和兩個 Condition 搭配的做法比較一下。
show tables;2018-08-07 16:57
show tables;
huangchengxiang2018-09-06 19:43
好難啊,看的腦瓜仁疼
琴絃子2018-09-10 22:19
lock.lock(); // 當前線程獲取鎖 try{ notEmpty.await(); //當前線程(當前線程正在運行)加入到notEmpty條件的等待隊列中(加入到隊列最尾端),當前線程釋放鎖和CPU執行權。await()包含unlock()的功能(完全釋放鎖) … }finally{ lock.unlock(); //當前線程釋放鎖 } notEmpty.await(); 執行後,完全釋放鎖(即鎖是空閒狀態,鎖的state=0),finally塊在調用lock.unlock();後,鎖的state知不是小於0(我分析state=-1),鎖豈不是未空閒?
lv_2018-09-13 22:25
你好,你工作幾年了,看了你的文章,我突然感覺自己什麼都不會.
楓子LY2018-10-22 10:58
cancelAcquire方法中只設置了pred.next指向node的next,卻沒設置next.prev指向pred,請問下大神這是怎麼回事,謝謝!!
這篇文章是 AQS 系列的最後一篇,第一篇,我們通過 ReentrantLock 公平鎖分析了 AQS 的核心,第二篇的重點是把 Condition 說明白,同時也說清楚了對於線程中斷的使用。
這篇,我們的關注點是 AQS 最後的部分,共享模式的使用。有前兩篇文章的鋪墊,剩下的源碼分析將會簡單很多。
本文先用 CountDownLatch 將共享模式說清楚,然後順着把其他 AQS 相關的類 CyclicBarrier、Semaphore 的源碼一起過一下。
老規矩:不放過任何一行代碼,沒有任何糊弄,沒有任何瞎說。
CountDownLatch
CountDownLatch 這個類是比較典型的 AQS 的共享模式的使用,這是一個高頻使用的類。latch 的中文意思是門栓、柵欄,具體怎麼解釋我就不廢話了,大家隨意,看兩個例子就知道在哪裏用、怎麼用了。
使用例子
我們看下 Doug Lea 在 java doc 中給出的例子,這個例子非常實用,我們經常會寫這個代碼。
假設我們有 N ( N > 0 ) 個任務,那麼我們會用 N 來初始化一個 CountDownLatch,然後將這個 latch 的引用傳遞到各個線程中,在每個線程完成了任務後,調用 latch.countDown() 代表完成了一個任務。
調用 latch.await() 的方法的線程會阻塞,直到所有的任務完成。
class Driver2 { // ...
void main() throws InterruptedException {
CountDownLatch doneSignal = new CountDownLatch(N);
Executor e = Executors.newFixedThreadPool(8);
// 創建 N 個任務,提交給線程池來執行
for (int i = 0; i < N; ++i) // create and start threads
e.execute(new WorkerRunnable(doneSignal, i));
// 等待所有的任務完成,這個方法纔會返回
doneSignal.await(); // wait for all to finish
}
}
class WorkerRunnable implements Runnable {
private final CountDownLatch doneSignal;
private final int i;
WorkerRunnable(CountDownLatch doneSignal, int i) {
this.doneSignal = doneSignal;
this.i = i;
}
public void run() {
try {
doWork(i);
// 這個線程的任務完成了,調用 countDown 方法
doneSignal.countDown();
} catch (InterruptedException ex) {
} // return;
}
void doWork() { ...}
}
所以說 CountDownLatch 非常實用,我們常常會將一個比較大的任務進行拆分,然後開啓多個線程來執行,等所有線程都執行完了以後,再往下執行其他操作。這裏例子中,只有 main 線程調用了 await 方法。
我們再來看另一個例子,這個例子很典型,用了兩個 CountDownLatch:
class Driver { // ...
void main() throws InterruptedException {
CountDownLatch startSignal = new CountDownLatch(1);
CountDownLatch doneSignal = new CountDownLatch(N);
for (int i = 0; i < N; ++i) // create and start threads
new Thread(new Worker(startSignal, doneSignal)).start();
// 這邊插入一些代碼,確保上面的每個線程先啓動起來,才執行下面的代碼。
doSomethingElse(); // don't let run yet
// 因爲這裏 N == 1,所以,只要調用一次,那麼所有的 await 方法都可以通過
startSignal.countDown(); // let all threads proceed
doSomethingElse();
// 等待所有任務結束
doneSignal.await(); // wait for all to finish
}
}
class Worker implements Runnable {
private final CountDownLatch startSignal;
private final CountDownLatch doneSignal;
Worker(CountDownLatch startSignal, CountDownLatch doneSignal) {
this.startSignal = startSignal;
this.doneSignal = doneSignal;
}
public void run() {
try {
// 爲了讓所有線程同時開始任務,我們讓所有線程先阻塞在這裏
// 等大家都準備好了,再打開這個門栓
startSignal.await();
doWork();
doneSignal.countDown();
} catch (InterruptedException ex) {
} // return;
}
void doWork() { ...}
}
這個例子中,doneSignal 同第一個例子的使用,我們說說這裏的 startSignal。N 個新開啓的線程都調用了startSignal.await() 進行阻塞等待,它們阻塞在柵欄上,只有當條件滿足的時候(startSignal.countDown()),它們才能同時通過這個柵欄。
如果始終只有一個線程調用 await 方法等待任務完成,那麼 CountDownLatch 就會簡單很多,所以之後的源碼分析讀者一定要在腦海中構建出這麼一個場景:有 m 個線程是做任務的,有 n 個線程在某個柵欄上等待這 m 個線程做完任務,直到所有 m 個任務完成後,n 個線程同時通過柵欄。
源碼分析
Talk is cheap, show me the code.
構造方法,需要傳入一個不小於 0 的整數:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
// 老套路了,內部封裝一個 Sync 類繼承自 AQS
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
// 這樣就 state == count 了
setState(count);
}
...
}
代碼都是套路,先分析套路:AQS 裏面的 state 是一個整數值,這邊用一個 int count 參數其實初始化就是設置了這個值,所有調用了 await 方法的等待線程會掛起,然後有其他一些線程會做 state = state - 1 操作,當 state 減到 0 的同時,那個線程會負責喚醒調用了 await 方法的所有線程。都是套路啊,只是 Doug Lea 的套路很深,代碼很巧妙,不然我們也沒有要分析源碼的必要。
對於 CountDownLatch,我們僅僅需要關心兩個方法,一個是 countDown() 方法,另一個是 await() 方法。countDown() 方法每次調用都會將 state 減 1,直到 state 的值爲 0;而 await 是一個阻塞方法,當 state 減爲 0 的時候,await 方法纔會返回。await 可以被多個線程調用,讀者這個時候腦子裏要有個圖:所有調用了 await 方法的線程阻塞在 AQS 的阻塞隊列中,等待條件滿足(state == 0),將線程從隊列中一個個喚醒過來。
我們用以下程序來分析源碼,t1 和 t2 負責調用 countDown() 方法,t3 和 t4 調用 await 方法阻塞:
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(5000);
} catch (InterruptedException ignore) {
}
// 休息 5 秒後(模擬線程工作了 5 秒),調用 countDown()
latch.countDown();
}
}, "t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10000);
} catch (InterruptedException ignore) {
}
// 休息 10 秒後(模擬線程工作了 10 秒),調用 countDown()
latch.countDown();
}
}, "t2");
t1.start();
t2.start();
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 阻塞,等待 state 減爲 0
latch.await();
System.out.println("線程 t3 從 await 中返回了");
} catch (InterruptedException e) {
System.out.println("線程 t3 await 被中斷");
Thread.currentThread().interrupt();
}
}
}, "t3");
Thread t4 = new Thread(new Runnable() {
@Override
public void run() {
try {
// 阻塞,等待 state 減爲 0
latch.await();
System.out.println("線程 t4 從 await 中返回了");
} catch (InterruptedException e) {
System.out.println("線程 t4 await 被中斷");
Thread.currentThread().interrupt();
}
}
}, "t4");
t3.start();
t4.start();
}
}
上述程序,大概在過了 10 秒左右的時候,會輸出:
線程 t3 從 await 中返回了
線程 t4 從 await 中返回了
// 這兩條輸出,順序不是絕對的
// 後面的分析,我們假設 t3 先進入阻塞隊列
接下來,我們按照流程一步一步走:先 await 等待,然後被喚醒,await 方法返回。
首先,我們來看 await() 方法,它代表線程阻塞,等待 state 的值減爲 0。
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 這也是老套路了,我在第二篇的中斷那一節說過了
if (Thread.interrupted())
throw new InterruptedException();
// t3 和 t4 調用 await 的時候,state 都大於 0。
// 也就是說,這個 if 返回 true,然後往裏看
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
// 只有當 state == 0 的時候,這個方法纔會返回 1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
從方法名我們就可以看出,這個方法是獲取共享鎖,並且此方法是可中斷的(中斷的時候拋出 InterruptedException 退出這個方法)。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 1. 入隊
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 同上,只要 state 不等於 0,那麼這個方法返回 -1
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 2
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我們來仔細分析這個方法,線程 t3 經過第 1 步 addWaiter 入隊以後,我們應該可以得到這個:
由於 tryAcquireShared 這個方法會返回 -1,所以 if (r >= 0) 這個分支不會進去。到 shouldParkAfterFailedAcquire 的時候,t3 將 head 的 waitStatus 值設置爲 -1,如下:
然後進入到 parkAndCheckInterrupt 的時候,t3 掛起。
我們再分析 t4 入隊,t4 會將前驅節點 t3 所在節點的 waitStatus 設置爲 -1,t4 入隊後,應該是這樣的:
然後,t4 也掛起。接下來,t3 和 t4 就等待喚醒了。
接下來,我們來看喚醒的流程,我們假設用 10 初始化 CountDownLatch。
當然,我們的例子中,其實沒有 10 個線程,只有 2 個線程 t1 和 t2,只是爲了讓圖好看些罷了。
我們再一步步看具體的流程。首先,我們看 countDown() 方法:
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
// 只有當 state 減爲 0 的時候,tryReleaseShared 才返回 true
// 否則只是簡單的 state = state - 1 那麼 countDown 方法就結束了
if (tryReleaseShared(arg)) {
// 喚醒 await 的線程
doReleaseShared();
return true;
}
return false;
}
// 這個方法很簡單,用自旋的方法實現 state 減 1
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
countDown 方法就是每次調用都將 state 值減 1,如果 state 減到 0 了,那麼就調用下面的方法進行喚醒阻塞隊列中的線程:
// 調用這個方法的時候,state == 0
// 這個方法先不要看所有的代碼,按照思路往下到我寫註釋的地方,其他的之後還會仔細分析
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// t3 入隊的時候,已經將頭節點的 waitStatus 設置爲 Node.SIGNAL(-1) 了
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 就是這裏,喚醒 head 的後繼節點,也就是阻塞隊列中的第一個節點
// 在這裏,也就是喚醒 t3
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
一旦 t3 被喚醒後,我們繼續回到 await 的這段代碼,parkAndCheckInterrupt 返回,我們先不考慮中斷的情況:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r); // 2. 這裏是下一步
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 1. 喚醒後這個方法返回
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
接下來,t3 會進到 setHeadAndPropagate(node, r) 這個方法,先把 head 給佔了,然後喚醒隊列中其他的線程:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 下面說的是,喚醒當前 node 之後的節點,即 t3 已經醒了,馬上喚醒 t4
// 類似的,如果 t4 後面還有 t5,那麼 t4 醒了以後,馬上將 t5 給喚醒了
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 又是這個方法,只是現在的 head 已經不是原來的空節點了,是 t3 的節點了
doReleaseShared();
}
}
又回到這個方法了,那麼接下來,我們好好分析 doReleaseShared 這個方法,我們根據流程,頭節點 head 此時是 t3 節點了:
// 調用這個方法的時候,state == 0
private void doReleaseShared() {
for (;;) {
Node h = head;
// 1. h == null: 說明阻塞隊列爲空
// 2. h == tail: 說明頭結點可能是剛剛初始化的頭節點,
// 或者是普通線程節點,但是此節點既然是頭節點了,那麼代表已經被喚醒了,阻塞隊列沒有其他節點了
// 所以這兩種情況不需要進行喚醒後繼節點
if (h != null && h != tail) {
int ws = h.waitStatus;
// t4 將頭節點(此時是 t3)的 waitStatus 設置爲 Node.SIGNAL(-1) 了
if (ws == Node.SIGNAL) {
// 這裏 CAS 失敗的場景請看下面的解讀
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 就是這裏,喚醒 head 的後繼節點,也就是阻塞隊列中的第一個節點
// 在這裏,也就是喚醒 t4
unparkSuccessor(h);
}
else if (ws == 0 &&
// 這個 CAS 失敗的場景是:執行到這裏的時候,剛好有一個節點入隊,入隊會將這個 ws 設置爲 -1
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果到這裏的時候,前面喚醒的線程已經佔領了 head,那麼再循環
// 否則,就是 head 沒變,那麼退出循環,
// 退出循環是不是意味着阻塞隊列中的其他節點就不喚醒了?當然不是,喚醒的線程之後還是會調用這個方法的
if (h == head) // loop if head changed
break;
}
}
我們分析下最後一個 if 語句,然後才能解釋第一個 CAS 爲什麼可能會失敗:
- h == head:說明頭節點還沒有被剛剛用 unparkSuccessor 喚醒的線程(這裏可以理解爲 t4)佔有,此時 break 退出循環。
- h != head:頭節點被剛剛喚醒的線程(這裏可以理解爲 t4)佔有,那麼這裏重新進入下一輪循環,喚醒下一個節點(這裏是 t4 )。我們知道,等到 t4 被喚醒後,其實是會主動喚醒 t5、t6、t7...,那爲什麼這裏要進行下一個循環來喚醒 t5 呢?我覺得是出於吞吐量的考慮。
滿足上面的 2 的場景,那麼我們就能知道爲什麼上面的 CAS 操作 compareAndSetWaitStatus(h, Node.SIGNAL, 0) 會失敗了?
因爲當前進行 for 循環的線程到這裏的時候,可能剛剛喚醒的線程 t4 也剛剛好到這裏了,那麼就有可能 CAS 失敗了。
for 循環第一輪的時候會喚醒 t4,t4 醒後會將自己設置爲頭節點,如果在 t4 設置頭節點後,for 循環才跑到 if (h == head),那麼此時會返回 false,for 循環會進入下一輪。t4 喚醒後也會進入到這個方法裏面,那麼 for 循環第二輪和 t4 就有可能在這個 CAS 相遇,那麼就只會有一個成功了。
CyclicBarrier
字面意思是“可重複使用的柵欄”,CyclicBarrier 相比 CountDownLatch 來說,要簡單很多,其源碼沒有什麼高深的地方,它是 ReentrantLock 和 Condition 的組合使用。看如下示意圖,CyclicBarrier 和 CountDownLatch 是不是很像,只是 CyclicBarrier 可以有不止一個柵欄,因爲它的柵欄(Barrier)可以重複使用(Cyclic)。
首先,CyclicBarrier 的源碼實現和 CountDownLatch 大相徑庭,CountDownLatch 基於 AQS 的共享模式的使用,而 CyclicBarrier 基於 Condition 來實現。
因爲 CyclicBarrier 的源碼相對來說簡單許多,讀者只要熟悉了前面關於 Condition 的分析,那麼這裏的源碼是毫無壓力的,就是幾個特殊概念罷了。
廢話結束,先上基本屬性和構造方法,往下拉一點點,和圖一起看:
public class CyclicBarrier {
// 我們說了,CyclicBarrier 是可以重複使用的,我們把每次從開始使用到穿過柵欄當做"一代"
private static class Generation {
boolean broken = false;
}
/** The lock for guarding barrier entry */
private final ReentrantLock lock = new ReentrantLock();
// CyclicBarrier 是基於 Condition 的
// Condition 是“條件”的意思,CyclicBarrier 的等待線程通過 barrier 的“條件”是大家都到了柵欄上
private final Condition trip = lock.newCondition();
// 參與的線程數
private final int parties;
// 如果設置了這個,代表越過柵欄之前,要執行相應的操作
private final Runnable barrierCommand;
// 當前所處的“代”
private Generation generation = new Generation();
// 還沒有到柵欄的線程數,這個值初始爲 parties,然後遞減
// 還沒有到柵欄的線程數 = parties - 已經到柵欄的數量
private int count;
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
public CyclicBarrier(int parties) {
this(parties, null);
}
我用一圖來描繪下 CyclicBarrier 裏面的一些概念:
看圖我們也知道了,CyclicBarrier 的源碼最重要的就是 await() 方法了。
首先,先看怎麼開啓新的一代:
// 開啓新的一代,當最後一個線程到達柵欄上的時候,調用這個方法來喚醒其他線程,同時初始化“下一代”
private void nextGeneration() {
// 首先,需要喚醒所有的在柵欄上等待的線程
trip.signalAll();
// 更新 count 的值
count = parties;
// 重新生成“新一代”
generation = new Generation();
}
看看怎麼打破一個柵欄:
private void breakBarrier() {
// 設置狀態 broken 爲 true
generation.broken = true;
// 重置 count 爲初始值 parties
count = parties;
// 喚醒所有已經在等待的線程
trip.signalAll();
}
這兩個方法之後用得到,現在開始分析最重要的等待通過柵欄方法 await 方法:
// 不帶超時機制
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
// 帶超時機制,如果超時拋出 TimeoutException 異常
public int await(long timeout, TimeUnit unit)
throws InterruptedException,
BrokenBarrierException,
TimeoutException {
return dowait(true, unit.toNanos(timeout));
}
繼續往裏看:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
// 先要獲取到鎖,然後在 finally 中要記得釋放鎖
// 如果記得 Condition 部分的話,我們知道 condition 的 await 會釋放鎖,signal 的時候需要重新獲取鎖
lock.lock();
try {
final Generation g = generation;
// 檢查柵欄是否被打破,如果被打破,拋出 BrokenBarrierException 異常
if (g.broken)
throw new BrokenBarrierException();
// 檢查中斷狀態,如果中斷了,拋出 InterruptedException 異常
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// index 是這個 await 方法的返回值
// 注意到這裏,這個是從 count 遞減後得到的值
int index = --count;
// 如果等於 0,說明所有的線程都到柵欄上了,準備通過
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 如果在初始化的時候,指定了通過柵欄前需要執行的操作,在這裏會得到執行
final Runnable command = barrierCommand;
if (command != null)
command.run();
// 如果 ranAction 爲 true,說明執行 command.run() 的時候,沒有發生異常退出的情況
ranAction = true;
// 喚醒等待的線程,然後開啓新的一代
nextGeneration();
return 0;
} finally {
if (!ranAction)
// 進到這裏,說明執行指定操作的時候,發生了異常,那麼需要打破柵欄
// 之前我們說了,打破柵欄意味着喚醒所有等待的線程,設置 broken 爲 true,重置 count 爲 parties
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
// 如果是最後一個線程調用 await,那麼上面就返回了
// 下面的操作是給那些不是最後一個到達柵欄的線程執行的
for (;;) {
try {
// 如果帶有超時機制,調用帶超時的 Condition 的 await 方法等待,直到最後一個線程調用 await
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 如果到這裏,說明等待的線程在 await(是 Condition 的 await)的時候被中斷
if (g == generation && ! g.broken) {
// 打破柵欄
breakBarrier();
// 打破柵欄後,重新拋出這個 InterruptedException 異常給外層調用的方法
throw ie;
} else {
// 到這裏,說明 g != generation, 說明新的一代已經產生,即最後一個線程 await 執行完成,
// 那麼此時沒有必要再拋出 InterruptedException 異常,記錄下來這個中斷信息即可
// 或者是柵欄已經被打破了,那麼也不應該拋出 InterruptedException 異常,
// 而是之後拋出 BrokenBarrierException 異常
Thread.currentThread().interrupt();
}
}
// 喚醒後,檢查柵欄是否是“破的”
if (g.broken)
throw new BrokenBarrierException();
// 這個 for 循環除了異常,就是要從這裏退出了
// 我們要清楚,最後一個線程在執行完指定任務(如果有的話),會調用 nextGeneration 來開啓一個新的代
// 然後釋放掉鎖,其他線程從 Condition 的 await 方法中得到鎖並返回,然後到這裏的時候,其實就會滿足 g != generation 的
// 那什麼時候不滿足呢?barrierCommand 執行過程中拋出了異常,那麼會執行打破柵欄操作,
// 設置 broken 爲true,然後喚醒這些線程。這些線程會從上面的 if (g.broken) 這個分支拋 BrokenBarrierException 異常返回
// 當然,還有最後一種可能,那就是 await 超時,此種情況不會從上面的 if 分支異常返回,也不會從這裏返回,會執行後面的代碼
if (g != generation)
return index;
// 如果醒來發現超時了,打破柵欄,拋出異常
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
好了,我想我應該講清楚了吧,我好像幾乎沒有漏掉任何一行代碼吧?
下面開始收尾工作。
首先,我們看看怎麼得到有多少個線程到了柵欄上,處於等待狀態:
public int getNumberWaiting() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return parties - count;
} finally {
lock.unlock();
}
}
判斷一個柵欄是否被打破了,這個很簡單,直接看 broken 的值即可:
public boolean isBroken() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return generation.broken;
} finally {
lock.unlock();
}
}
前面我們在說 await 的時候也幾乎說清楚了,什麼時候柵欄會被打破,總結如下:
- 中斷,我們說了,如果某個等待的線程發生了中斷,那麼會打破柵欄,同時拋出 InterruptedException 異常;
- 超時,打破柵欄,同時拋出 TimeoutException 異常;
- 指定執行的操作拋出了異常,這個我們前面也說過。
最後,我們來看看怎麼重置一個柵欄:
public void reset() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
breakBarrier(); // break the current generation
nextGeneration(); // start a new generation
} finally {
lock.unlock();
}
}
我們設想一下,如果初始化時,指定了線程 parties = 4,前面有 3 個線程調用了 await 等待,在第 4 個線程調用 await 之前,我們調用 reset 方法,那麼會發生什麼?
首先,打破柵欄,那意味着所有等待的線程(3個等待的線程)會喚醒,await 方法會通過拋出 BrokenBarrierException 異常返回。然後開啓新的一代,重置了 count 和 generation,相當於一切歸零了。
怎麼樣,CyclicBarrier 源碼很簡單吧。
Semaphore
有了 CountDownLatch 的基礎後,分析 Semaphore 會簡單很多。Semaphore 是什麼呢?它類似一個資源池(讀者可以類比線程池),每個線程需要調用 acquire() 方法獲取資源,然後才能執行,執行完後,需要 release 資源,讓給其他的線程用。
大概大家也可以猜到,Semaphore 其實也是 AQS 中共享鎖的使用,因爲每個線程共享一個池嘛。
套路解讀:創建 Semaphore 實例的時候,需要一個參數 permits,這個基本上可以確定是設置給 AQS 的 state 的,然後每個線程調用 acquire 的時候,執行 state = state - 1,release 的時候執行 state = state + 1,當然,acquire 的時候,如果 state = 0,說明沒有資源了,需要等待其他線程 release。
構造方法:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
這裏和 ReentrantLock 類似,用了公平策略和非公平策略。
看 acquire 方法:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public void acquire(int permits) throws InterruptedException {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}
public void acquireUninterruptibly(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.acquireShared(permits);
}
這幾個方法也是老套路了,大家基本都懂了吧,這邊多了兩個可以傳參的 acquire 方法,不過大家也都懂的吧,如果我們需要一次獲取超過一個的資源,會用得着這個的。
我們接下來看不拋出 InterruptedException 異常的 acquireUninterruptibly() 方法吧:
public void acquireUninterruptibly() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
前面說了,Semaphore 分公平策略和非公平策略,我們對比一下兩個 tryAcquireShared 方法:
// 公平策略:
protected int tryAcquireShared(int acquires) {
for (;;) {
// 區別就在於是不是會先判斷是否有線程在排隊,然後才進行 CAS 減操作
if (hasQueuedPredecessors())
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
// 非公平策略:
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
也是老套路了,所以從源碼分析角度的話,我們其實不太需要關心是不是公平策略還是非公平策略,它們的區別往往就那麼一兩行。
我們再回到 acquireShared 方法,
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
由於 tryAcquireShared(arg) 返回小於 0 的時候,說明 state 已經小於 0 了(沒資源了),此時 acquire 不能立馬拿到資源,需要進入到阻塞隊列等待,雖然貼了很多代碼,不在乎多這點了:
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
這個方法我就不介紹了,線程掛起後等待有資源被 release 出來。接下來,我們就要看 release 的方法了:
// 任務介紹,釋放一個資源
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
// 溢出,當然,我們一般也不會用這麼大的數
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
tryReleaseShared 方法總是會返回 true,然後是 doReleaseShared,這個也是我們熟悉的方法了,我就貼下代碼,不分析了,這個方法用於喚醒所有的等待線程:
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
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;
}
}
Semphore 的源碼確實很簡單,基本上都是分析過的老代碼的組合使用了。
總結
寫到這裏,終於把 AbstractQueuedSynchronizer 基本上說完了,對於 Java 併發,Doug Lea 真的是神一樣的存在。日後我們還會接觸到很多 Doug Lea 的代碼,希望我們大家都可以朝着大神的方向不斷打磨自己的技術,少一些高大上的架構,多一些實實在在的優秀代碼吧。
(全文完)
評論區
Collin2018-02-06 17:00
寫得非常好~ 感謝~ 另外請教一個問題: 在doReleaseShared中, 如head.waitStatus == 0, 會將其CAS爲Node.PROPAGATE. 但是實際上, 設置爲Node.SIGNAL也沒有任何區別對吧?
HongJie2018-02-07 00:20
今天比較忙,白天就看到你的留言了,抱歉沒有及時回覆!
現在有點晚了,明天起來再看下你的問題吧,請多擔待些?
Collin2018-02-07 00:22
哈哈哈哈 沒事沒事. 早點睡 辛苦了
HongJie2018-02-07 14:04
我把 doReleaseShared() 的代碼先貼一下:
// 爲了顯示需要,我修改了縮進
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
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;
}
}
HongJie2018-02-07 14:05
進到 compareAndSetWaitStatus(h, 0, Node.PROPAGATE) 說明 ws 剛剛等於 0,ws 是頭結點的狀態。
如果這次 CAS 失敗,這種情況很好理解,說明剛剛有新的節點進來了,那個節點將此結點的狀態值置爲 SIGNAL 了。這種情況下,continue 進到下一個循環裏就好了,在下一個循環,會喚醒這個新進來的節點。
回過頭來回答你的問題:不可以將頭結點設置爲 SIGNAL。
此場景很難模擬出現,也就是在喚醒的時候,阻塞隊列的最後一個節點成爲了頭結點,我們知道,最後一個節點的狀態通常爲 0,但是突然出現了一個新的節點,這個節點跑到 shouldParkAfterFailedAcquire 這個方法了。
如果按照你的設想設置爲 SIGNAL 了,那麼新的節點進到 shouldParkAfterFailedAcquire 以後,會返回 true,然後會進到 parkAndCheckInterrupt 掛起,而頭節點的這個線程也已經從共享鎖退出,那麼已經沒有線程會負責喚醒這個新節點了。
如果是設置爲 PROPAGATE,那麼新的節點在 shouldParkAfterFailedAcquire 回來的時候是 false,也就不會掛起,而會進到下一個 for 循環,接下來就簡單了。
destiny10202018-03-30 17:44
大神你好,首先給你的文章手動點贊。 請問你的配圖使用什麼工具畫的啊,我覺得都還挺讚的!
HongJie2018-03-30 17:48
我用的是 keynote,畫些簡單的圖形很方便?
袋鼠2018-04-14 22:35
頂了,博主。
博主,提兩個建議。 一是弄個二維碼,大家覺得文章好的話,自願地搭上一些小錢,略表心意。這一篇篇文章是花了很多心血在裏面,我也寫一些博文,所以知道里面的辛苦。 二是以後在文章前方,您給我出幾個問題可好?這樣大家看完好,相信能理解得更好。
suk2018-04-25 16:39
T3,T4入隊的時候,首先是被包裝爲Node,它們的下一個的Node都爲SHARED。這是做什麼用的沒看懂
h2pl2018-05-19 22:49
建議先講一下AQS的共享模式如何實現,直接看CountDownLatch的實現有點懵
yuehahah2018-06-22 16:43
講的很好啊,我有個問題哈,如果是semaphore的話,假設現在permit是10,有90個在等待,那這個在喚醒的時候會喚醒所有的後續線程,這個不會造成比較大的性能消耗嗎,
yuehahah2018-06-22 17:04
想錯了,如果t4獲取到之後還沒有釋放,那麼即便在t3的循環裏unpark了t5,t5的tryacquire也是不成功的,還會重新park,然後t3的循環break掉。
HongJie2018-06-22 17:05
我都準備回答你問題了?
yuehahah2018-06-22 20:24
??辛苦博主了,博主人好好啊
小白2018-09-09 21:27
大神你好,文章寫的好棒!看完之後有個疑問,看評論區有一個人問能不能把Node.PROPAGATE直接換成Node.SIGNAL,大神你已經回答過了;我有一個類似的疑問:能不能直接把
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
這一整段去掉,對於ws等於0的情況直接不處理?因爲好像shouldParkAfterFailedAcquire對於0和對於Node.PROPAGATE的處理是一樣的。
*旭2018-10-23 21:54
大神你好,謝謝你寫的文章。我看了你對CyclicBarrier源碼的分析,有2個問題想請教,應該是同一個地方沒搞懂。
文中提到CyclicBarrier是使用的共享模式AQS,但是既然用的是ReentrantLock,不應該是獨佔型嗎?
在newGeneration或者breakBarrier時,使用condition.signalALL();所有的線程不應該在ReentranLock中排隊獲取鎖嗎?那我怎麼感覺這些線程不是同時啓動的。
*旭2018-10-23 22:06
不好意思,第一個問題忽略吧,文中說的是CyclicBarrier基於Condition,沒說共享AQS,但我第二個問題還是想不通