CountDownLatch的原理以及使用方法
概述
1 ountDownLatch這個類使一個線程等待其他線程各自執行完畢後再執行。
2 是通過一個計數器來實現的,計數器的初始值是線程的數量。每當一個線程執行完畢後,計數器的值就-1,當計數器的值爲0時,表示所有線程都執行完畢,然後在閉鎖上等待的線程就可以恢復工作了。
源碼解析
構造函數
當我們調用CountDownLatch countDownLatch=new CountDownLatch(4) 時候,此時會創建一個AQS的同步隊列,並把創建CountDownLatch 傳進來的計數器賦值給AQS隊列的 state,所以state的值也代表CountDownLatch所剩餘的計數次數
//參數count爲計數值
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);//創建同步隊列,並設置初始計數器值
}
await()
創建一個節點,加入到AQS阻塞隊列,並同時把當前線程掛起。
//調用await()方法的線程會被掛起,它會等待直到count值爲0才繼續執行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//和await()類似,只不過等待一定的時間後count值還沒變爲0的話就會繼續執行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
acquireSharedInterruptibly(int arg)
//1.判斷當前線程是否有被中斷
//2.如果沒有的話,就調用tryAcquireShared(int acquires)方法,判斷當前線程是否還需要“阻塞”。
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
doAcquireSharedInterruptibly(int arg)
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//加入等待隊列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
// 進入 CAS 循環
try {
for (;;) {
//當一個節點(關聯一個線程)進入等待隊列後, 獲取此節點的 prev 節點
final Node p = node.predecessor();
// 如果獲取到的 prev 是 head,也就是隊列中第一個等待線程
if (p == head) {
// 再次嘗試申請 反應到 CountDownLatch 就是查看是否還有線程需要等待(state是否爲0)
int r = tryAcquireShared(arg);
// 如果 r >=0 說明 沒有線程需要等待了 state==0
if (r >= 0) {
//嘗試將第一個線程關聯的節點設置爲 head
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//經過自旋tryAcquireShared後,state還不爲0,就會到這裏,第一次的時候,waitStatus是0,那麼node的waitStatus就會被置爲SIGNAL,第二次再走到這裏,就會用LockSupport的park方法把當前線程阻塞住
//重組雙向鏈表,清空無效節點,掛起當前線程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
總結
1 當前線程就會進入了一個死循環當中,在這個死循環裏面,會不斷的進行判斷
2 通過調用tryAcquireShared方法,不斷判斷我們上面說的那個計數器,
3 看看它的值是否爲0了(爲0的時候,其實就是我們調用了足夠多次數的countDownLatch.countDown()方法的時候)
4 如果是爲0的話,tryAcquireShared就會返回1,設置第一個線程關聯的借點未head,然後跳出了循環,不再“阻塞”當前線程了。
5 需要注意的是,說是在不停的循環,其實也並非在不停的執行for循環裏面的內容,因爲在後面調用parkAndCheckInterrupt()方法時,在這個方法裏面是會調用 LockSupport.park(this)來禁用當前線程的。
parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
addWaiter(Node mode)
將當前線程加入等待隊列
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// 嘗試快速入隊操作,因爲大多數時候尾節點不爲 null
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果尾節點爲空(也就是隊列爲空) 或者嘗試CAS入隊失敗(由於併發原因),進入enq方法
enq(node);
return node;
}
步驟:
1.構造Node實體,參數爲當前線程和mode,mode是SHARED 或者 EXCLUSIVE
2.嘗試快速入隊列操作
3.如果尾節點爲空(也就是隊列爲空) 或者嘗試CAS入隊失敗(由於併發原因),進入enq方法
enq(final Node node)
private Node enq(final Node node) {
// 死循環+CAS保證所有節點都入隊
for (;;) {
Node t = tail;
// 如果隊列爲空 設置一個空節點作爲 head
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
//加入隊尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
說明:
1.死循環+CAS 樂觀鎖
2.CAS是實現原子性,compareAndSetTail底層調用的是unsafe類,直接操作內存,在cpu層上加鎖,直接對內存進行操作
setHeadAndPropagate(node, r)
private void setHeadAndPropagate(Node node, int propagate) {
//備份head
Node h = head;
//搶到鎖的線程被喚醒 將這個節點設置爲head
setHead(node);
// propagate 一般都會大於0 或者存在可被喚醒的線程
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 只有一個節點 或者是共享模式 釋放所有等待線程 各自嘗試搶佔鎖
if (s == null || s.isShared())
doReleaseShared();
}
}
Node 中的waitStatus四種狀態
//線程已被 cancelled ,這種狀態的節點將會被忽略,並移出隊列
static final int CANCELLED = 1;
// 表示當前線程已被掛起,並且後繼節點可以嘗試搶佔鎖
static final int SIGNAL = -1;
//線程正在等待某些條件
static final int CONDITION = -2;
//共享模式下 無條件所有等待線程嘗試搶佔鎖
static final int PROPAGATE = -3;
countDown()
//將計數值減一
//遞減鎖重入次數,當state=0時喚醒所有阻塞線程
public void countDown() {
sync.releaseShared(1);
}
releaseShared(1)
// AQS類
public final boolean releaseShared(int arg) {
// arg 爲固定值 1
// 如果計數器state 爲0 返回true,前提是調用 countDown() 之前不能已經爲0
if (tryReleaseShared(arg)) {
// 喚醒等待隊列的線程
doReleaseShared();
return true;
}
return false;
}
// CountDownLatch 重寫的方法
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
// 依然是循環+CAS配合 實現計數器減1
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
/// AQS類
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// 如果節點狀態爲SIGNAL,則他的next節點也可以嘗試被喚醒
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//成功則喚醒線程
}
// 將節點狀態設置爲PROPAGATE,表示要向下傳播,依次喚醒
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
總結
1、AQS 分爲獨佔模式和共享模式,CountDownLatch 使用了它的共享模式。
2、AQS 當第一個等待線程(被包裝爲 Node)要入隊的時候,要保證存在一個 head 節點,這個 head 節點不關聯線程,也就是一個虛節點。
3、當隊列中的等待節點(關聯線程的,非 head 節點)搶到鎖,將這個節點設置爲 head 節點。
4、第一次自旋搶鎖失敗後,waitStatus 會被設置爲 -1(SIGNAL),第二次再失敗,就會被 LockSupport 阻塞掛起。
5、如果一個節點的前置節點爲 SIGNAL 狀態,則這個節點可以嘗試搶佔鎖。
實現邏輯
1、初始化CountDownLatch實際就是設置了AQS的state爲計數的值
2、調用CountDownLatch的countDown方法時實際就是調用AQS的釋放同步狀態的方法,每調用一次就自減一次state值
3、調用await方法實際就調用AQS的共享式獲取同步狀態的方法acquireSharedInterruptibly(1),這個方法的實現邏輯就調用子類Sync的tryAcquireShared方法,只有當子類Sync的tryAcquireShared方法返回大於0的值時纔算獲取同步狀態成功,否則就會一直在死循環中不斷重試,直到tryAcquireShared方法返回大於等於0的值,而Sync的tryAcquireShared方法只有當AQS中的state值爲0時纔會返回1,否則都返回-1,也就相當於只有當AQS的state值爲0時,await方法纔會執行成功,否則就會一直處於死循環中不斷重試。
示例
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
/*創建CountDownLatch實例,計數器的值初始化爲5*/
final CountDownLatch downLatch = new CountDownLatch(5);
/*每個線程等待1s,表示執行比較耗時的任務*/
for(int i = 0;i < 5;i++){
final int num = i;
new Thread(new Runnable() {
public void run() {
try {
Thread.sleep(1000);
}catch (InterruptedException e){
e.printStackTrace();
}
System.out.println(String.format("thread %d has finished",num));
/*任務完成後調用CountDownLatch的countDown()方法*/
downLatch.countDown();
}
}).start();
}
/*主線程調用await()方法,等到其他5個線程執行完後才繼續執行*/
downLatch.await();
System.out.println("all threads have finished,main thread will continue run");
}
}
輸出
thread 1 has finished
thread 2 has finished
thread 0 has finished
thread 3 has finished
thread 4 has finished
all threads have finished,main thread will continue run
使用場景
1 需要等待某個條件達到要求後才能做後面的事情
2 同時當線程都完成後也會觸發事件,以便進行後面的操作
如果大家對java架構相關感興趣,可以關注下面公衆號,會持續更新java基礎面試題, netty, spring boot,spring cloud等系列文章,一系列乾貨隨時送達, 超神之路從此展開, BTAJ不再是夢想!