1.什麼是CountDownLatch
-
之前寫了一篇關於JUC包裏關於併發編程工具類的筆記,但過後總感覺不太舒服,不通透,所以,這次就想着先把整個CountDownLatch源碼擼一遍。
-
至於CountDownLatch是什麼,擼代碼前,可先看:【JUC】CountDownLatch/CyclicBarrier/Semaphore
2.開擼
2.1 類圖
先從類圖開始,一步一步看。
- idea找JUC包裏面的CountDownLatch,右鍵如圖:
- 打開類圖:
- 能看到CountDownLatch有一個Sync抽象類,找到Sync我們右鍵打開它的類圖:
- Sync的類圖:
- Sync繼承於AbstractQueuedSynchronizer
- 而AbstractQueuedSynchronizer裏面維護了一個以Node爲節點的AQS隊列。
- AQS隊列,就是CountDownLatch的核心。
2.2 AQS隊列
- 追根溯源,AQS隊列是由AbstractQueuedSynchronizer維護
- 而AbstractQueuedSynchronizer由繼承於AbstractOwnableSynchronizer
- 我們先看AbstractOwnableSynchronizer源碼:
public abstract class AbstractOwnableSynchronizer
implements java.io.Serializable {
protected AbstractOwnableSynchronizer() { }
private transient Thread exclusiveOwnerThread;
protected final void setExclusiveOwnerThread(Thread thread) {
exclusiveOwnerThread = thread;
}
protected final Thread getExclusiveOwnerThread() {
return exclusiveOwnerThread;
}
}
- exclusiveOwnerThread從名字就能看出來獨家擁有線程,也就是獨佔模式鎖的擁有者。
- 而AQS定義兩種資源共享方式:
1,Exclusive(獨佔,只有一個線程能執行,ReentrantLock使用該模式)
2,Share(共享,多個線程可同時執行,Semaphore/CountDownLatch使用該模式)。
- AQS隊列底層其實就是一個雙向鏈表,而在AbstractQueuedSynchronizer中鏈表節點是有內部類Node來充當,在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;
- exclusiveMarker to indicate a node is waiting in shared mode
共享模式:CountDownLatch使用該模式,今天我們聊的就是這個。 - Marker to indicate a node is waiting in exclusive mode
獨佔模式:ReentrantLock使用該模式,詳細可見:【鎖】【JUC】可重入鎖/AQS隊列–ReentrantLock源碼分析 - 再看一下AbstractQueuedSynchronizer的核心成員:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private static final long serialVersionUID = 7373984972572414691L;
protected AbstractQueuedSynchronizer() { }
//隊列節點
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;
/** waitStatus value to indicate thread has cancelled */ 線程已經被取消,該狀態的節點不會再次阻塞。
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */ 線程需要去被喚醒
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */ 線程正在喚醒等待條件
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should //線程的共享鎖應該被無條件傳播
* unconditionally propagate
*/
static final int PROPAGATE = -3;
//上面四個值就是下面變量waitStatus的值
volatile int waitStatus;
//前一個節點
volatile Node prev;
//下一個節點
volatile Node next;
//當前節點代表的線程
volatile Thread thread;
/**
*等待節點的後繼節點。如果當前節點是共享的,那麼這個字段是一個SHARED常量,也就是說節點類型(獨佔和共享)和
*等待隊列中的後繼節點共用一個字段。(注:比如說當前節點A是共享的,那麼它的這個字段是shared,也就是說在這個等
*待隊列中,A節點的後繼節點也是shared。如果A節點不是共享的,那麼它的nextWaiter就不是一個SHARED常量,即是獨
*佔的。
*/
Node nextWaiter;
}
//頭結點
private transient volatile Node head;
//尾節點
private transient volatile Node tail;
//狀態值
private volatile int state;
- 如果瞭解CLH隊列的話你會感覺眼熟,因爲AQS隊列就是它的一個變體,至於CLH隊列是什麼,可見:【鎖】自旋鎖-MCS/CLH隊列
- AbstractQueuedSynchronizer的這些成員中,volatile修飾的state是重點,volatile關鍵字保證了線程間可見性。可見:【JUC】volatile關鍵字相關整理
- CountDownLatch中,state的值代表着待達到條件的線程數,比如初始化爲五,表示待達到條件的線程數爲5,每次調用countDown()函數都會減一,等到state變爲0,阻塞在隊列裏的線程就可以被重新喚醒了,下面會詳細聊。
- state是被volatile修飾的,在多線程併發時,採用CAS修改state的值,至於什麼是CAS,可見:【JUC】 Java中的CAS。
- 另一個要注意的AbstractQueuedSynchronizer裏Node中的狀態屬性waitStatus,它默認爲零,還有四個值見上面Node代碼,它的值會被後置結點在獲取鎖失敗後阻塞前修改,用於提醒你在釋放鎖後去喚醒它,具體詳細情況後面源碼聊。
- AQS結構圖
- AQS隊列的頭結點並不關聯任何線程,他是一個默認的Node節點。
2.3 開始擼代碼
- 先看構造函數:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
//初始化AQS隊列
this.sync = new Sync(count);
}
//內部類,AQS隊列
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
- CountDownLatch初始化時會初始化內部類Sync中的state的值,代表線程數。
- CountDownLatch中的核心方法一個是await()方法,另一個是countDown()方法。
1,調用await()方法的線程會被掛起,它會等待直到state值爲0才繼續執行
2,調用countDown()將state值減1
2.3.1 我們先看await()方法
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
- await()調用了acquireSharedInterruptibly()方法,該方法在sync的父類AbstractQueuedSynchronizer中:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//判斷是否被中斷,中斷就拋出異常
if (Thread.interrupted())
throw new InterruptedException();
//與tryAcquireShared(arg)的返回值相比較
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
- 首先判斷是否被中斷,中斷就拋出異常
- 否則的話與tryAcquireShared(arg)的返回值相比較:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
- tryAcquireShared(arg)中state不等於0將會返回-1,進入doAcquireSharedInterruptibly(arg)方法。
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//將當前線程相關的節點將入鏈表尾部
final Node node = addWaiter(Node.SHARED);
////是否中斷
boolean failed = true;
try {
for (;;) {
// //獲取前一個節點
final Node p = node.predecessor();
//如果當前node節點是第二個節點,緊跟在head後面
if (p == head) {
//判斷state是否等於0,等於0,返回1
int r = tryAcquireShared(arg);
if (r >= 0) {
//重新設置頭結點,並喚醒後置結點
setHeadAndPropagate(node, r);
p.next = null; // 輔助 GC
failed = false;
return;
}
}
//當shouldParkAfterFailedAcquire返回成功,
//也就是前驅節點是Node.SIGNAL狀態時,
//進行真正的park將當前線程掛起,並且檢查中斷標記,
//如果是已經中斷,則設置interrupted =true。
//如果shouldParkAfterFailedAcquire返回false,
//則重複上述過程,直到獲取到資源或者被park。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- 我代碼中註釋寫的已經比較細了,可以大致看出整個邏輯:
1,調用await()方法的線程會把自己塞進一個node結點中。
2,該結點會把自己塞到AQS隊列最後面。
3,然後會進入一段類似自旋的代碼。
4,這段代碼中,會先判斷自己是不是在頭結點正後面,state值有沒有等於0,不是的話就修改前結點的waitStatus值爲-1,然後阻塞當前線程,等待前置結點的喚醒
5,等到被喚醒了,線程繼續自旋,判斷自己是不是已經到了頭結點後面,state值有沒有等於0,如果等於0了,就重置頭結點,然後喚醒下一個線程。
6,至於第一個被喚醒的線程,是在其他線程調用countDown()方法,直到state值等於0,後續會細講。
- 接下來我們對doAcquireSharedInterruptibly(int arg)方法中調用的方法一個一個看;
addWaiter(Node.SHARED)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
//1、首先嚐試以快速方式添加到隊列末尾
Node pred = tail;//pred指向現有tail末尾節點
if (pred != null) {
//新加入節點的前一個節點是現有AQS隊列的tail節點
node.prev = pred;
//CAS原子性的修改tail節點
if (compareAndSetTail(pred, node)) {
//修改成功,新節點成功加入AQS隊列,pred節點的next節點指向新的節點
pred.next = node;
return node;
}
}
//2、pred爲空,或者修改tail節點失敗,
//則走enq方法將節點插入隊列
enq(node);
return node;
}
private Node enq(final Node node) {
for(;;) {//CAS
Node t = tail;
if (t == null) {
// 必須初始化。這裏是AQS隊列爲空的情況。
//通過CAS的方式創建head節點,並且tail和head都指向
//同一個節點。
if (compareAndSetHead(new Node()))
//注意這裏初始化head節點,並不關聯任何線程!!
tail = head;
} else {
//這裏變更node節點的prev指針,並且移動tail指針指向node,
//前一個節點的next指向新插入的node
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- 上面addWaiter方法的第一行代碼new Node(Thread.currentThread(), mode);,會創建一個node對象,Node.SHARED,上面我們聊過,SHARED:共享模式。
- addWaiter首先會以快速方式將node添加到隊尾,如果失敗則走enq方法。失敗有兩種可能,一個是tail爲空,也就是AQS爲空的情況下。另一是compareAndSetTail失敗,也就是多線程併發添加到隊尾,此時會出現CAS失敗。
- 注意enq方法,在t==null時,首先創建空的頭節點,不關聯任何的線程,nextWaiter和thread變量都是null。
shouldParkAfterFailedAcquire(Node pred, Node node)和parkAndCheckInterrupt()
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws= pred.waitStatus;//獲取到上一個節點的waitStatus
if (ws == Node.SIGNAL)//前面講到當一個節點狀態時SIGNAL時,
//他有責任喚醒後面的節點。所以這裏判斷前驅節點是SIGNAL狀態,
//則可以安心的park中斷了。
return true;
if (ws > 0) {
/*
* 過濾掉中間cancel狀態的節點
* 前驅節點被取消的情況(線程允許被取消哦)。向前遍歷,
* 直到找到一個waitStatus大於0的(不是取消狀態或初始狀態)
* 的節點,該節點設置爲當前node的前驅節點。
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 修改前驅節點的WaitStatus爲Node.SIGNAL。
* 明確前驅節點必須爲Node.SIGNAL,當前節點纔可以park
* 注意,這個CAS也可能會失敗,因爲前驅節點的WaitStatus狀態
* 可能會發生變化
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
//阻塞當前線程
//park並且檢查是否被中斷過
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
- 當shouldParkAfterFailedAcquire返回成功,也就是前驅節點是Node.SIGNAL狀態時,進行真正的park將當前線程掛起。
setHeadAndPropagate(node, r)
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();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
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;
}
}
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)//注意這裏是從AQS隊列的尾節點開始查找的,
//找到最後一個 waitStatus<=0 的那個節點,將其喚醒。
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
- 這段做的就是重新設置頭結點,判斷AQS隊列後面還有沒有等待被喚醒的線程,有的話繼續去喚醒。
cancelAcquire
- acquireQueued方法在出現異常時,會執行cancelAcquire方法取消當前node的acquire操作。
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// 跳過中間CANCELLED狀態的節點
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
Node predNext = pred.next;
// 將node設置爲CANCELLED狀態
node.waitStatus = Node.CANCELLED;
// 如果當前節點是tail節點,則直接移除
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {//如果pred不是head節點並且是SIGNAL 狀態,
//或者可以設置爲SIGNAL 狀態,
//那麼將pred的next設置爲node.next,也就是移除當前節點
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);//喚醒node的後繼節點
}
node.next = node; // help GC
}
}
private void unparkSuccessor(Node node) {
//如果waitStatus爲負數,則將其設置爲0(允許失敗)
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
//喚醒當前節點後面的節點。通常是緊隨的next節點,
//但是當next被取消或者爲空,則從tail到node之間的所有節點,
//往後往前查找直到找到一個waitStatus <=0的節點,將其喚醒unpark
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);
}
- 總結一下:
1、設置thread變量爲空,並且設置狀態爲canceled
2、跳過中間的已經被取消的節點
3、如果當前節點是tail節點,則直接移除。否則:
4、如果其前驅節點不是head節點並且(前驅節點是SIGNAL狀態,或者可以被設置爲SIGNAL狀態),那麼將當前節點移除。否則通過LockSupport.unpark()喚醒node的後繼節點
2.3.1 我們再看看countDown()方法
public void countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {//state減一,返回state是否等於0
//如果等於0,開始喚醒線程
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
- 首先判斷state是否已經等於0,等於0,就調用doReleaseShared()方法開始喚醒第一個線程,至於doReleaseShared()方法,上面有提到,忘記可以往上翻,就在重置頭結點喚醒下一個結點那一塊。
- 如果不等於0,就state減一,然後再判斷state是否已經等於0,等於0,就調用doReleaseShared()方法,不等於0,結束。
【完】