一、CountDownLatch簡介
countdownlatch 是一個同步工具類,它允許一個或多個線程一直等待,直到其他線程的操作執行完畢再執行。從命名可以解讀到 countdown 是倒數的意思,類似於我們倒計時的概念。
countdownlatch 提供了兩個方法,一個是 countDown,一個是 await, countdownlatch 初始化的時候需要傳入一個整數,在這個整數倒數到 0 之前,調用了 await 方法的程序都必須要等待直到0纔開始執行,然後通過 countDown 來倒數。
二、使用示例
我們定義一個總數爲3的計數器,t1線程調用await()方法,其他t2/t3/t4線程調用countDown()方法,會發現打印結果總是最後打印t1線程
public class CountDownLatchDemo {
// 初始化一個總數爲3的計數器
static CountDownLatch count = new CountDownLatch(3);
public static void main(String[] args) {
new Thread(()->{
try {
// 調用await()方法,當計數器不爲0的時候,阻塞
count.await();
System.out.println("線程t1執行");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"t1").start();
new Thread(()->{
// 調用countDown()方法,計數器-1
count.countDown();
System.out.println("線程t2執行");
},"t2").start();
new Thread(()->{
// 調用countDown()方法,計數器-1
count.countDown();
System.out.println("線程t3執行");
},"t3").start();
new Thread(()->{
// 調用countDown()方法,計數器-1
count.countDown();
System.out.println("線程t4執行");
},"t4").start();
}
}
打印執行結果:
線程t2執行
線程t4執行
線程t3執行
線程t1執行
Process finished with exit code 0
由於t1阻塞,必須等到其他三個線程執行到計數器爲0的時候纔會執行,所以t1總是最後打印
三、CountDownLatch作用
我們可以使用CountDownLatch做併發模擬,假設我們有100個線程,我們希望他同時在某一時刻同時執行,那麼我們就可以使用CountDownLatch
定義一個線程,調用await()方法直到計數器爲0才執行
public class ThreadDemo extends Thread {
private CountDownLatch count;
public ThreadDemo(CountDownLatch count){
this.count = count;
}
@Override
public void run() {
try {
count.await();
System.out.println("線程"+Thread.currentThread().getName()+"執行");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
測試,循環啓動線程,然後調用countDown()方法,令計數器爲0,然後線程會同時執行
public class CountDownLatchDemo02 {
static CountDownLatch count = new CountDownLatch(1);
public static void main(String[] args) {
for(int i=0;i<10;i++) {
new ThreadDemo(count).start();
}
count.countDown();
}
}
四、源碼分析
1.構造方法CountDownLatch(int count)
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
// 定義了一個內部變量sync和內部類Sync,把count傳值到內部類的構造方法
this.sync = new Sync(count); // (1)
}
我們看一下內部類Sync的實現,發現內部類Sync繼承了AQS,那根據前面對AQS的實現的學習,對此就能比較好了解了
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// (1) 初始化CountDownLatch調用此方法,把count的值賦給了state,我們學習AQS和ReentrantLock實現的時候知道state是記錄鎖的,state>0表示鎖的次數
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;
}
}
}
2.await()方法
public void await() throws InterruptedException {
// await是調用AQS的acquireSharedInterruptibly方法加鎖,從方法名字可以看出,這個實現是一種共享鎖
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 判斷是否被中斷過,如果中斷過就跑出異常
if (Thread.interrupted())
throw new InterruptedException();
//state 如果不等於 0,說明當前線程需要加入到共享鎖隊列中
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
doAcquireSharedInterruptibly添加鎖的過程
- addWaiter 設置爲 shared 模式。
- tryAcquire 和 tryAcquireShared 的返回值不同,因此會多出一個判斷過程
- 在 判 斷 前 驅 節 點 是 頭 節 點 後 , 調 用 了setHeadAndPropagate 方法,而不是簡單的更新一下頭節點。
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) {
// 如果前一個是head節點,就判斷嘗試獲取鎖
int r = tryAcquireShared(arg);
//r>=0 表示獲取到了執行權限,這個時候因爲 state!=0,所以不會執行這段代碼
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);
}
}
setHeadAndPropagate方法
這個方法的主要作用是把被喚醒的節點,設置成 head 節點。 然後繼續喚醒隊列中的其他線程。
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();
}
}
由於線程被 await 方法阻塞了,所以只有等到countdown 方法使得 state=0 的時候纔會被喚醒,我們來看看 countdown 做了什麼
3.countDown()方法
public void countDown() {
sync.releaseShared(1);
}
- 只有當 state 減爲 0 的時候,tryReleaseShared 才返回 true, 否則只是簡單的 state = state - 1
- 如果 state=0, 則調用 doReleaseShared喚醒處於 await 狀態下的線程
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// 用自旋的方法實現 state 減 1
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
AQS. doReleaseShared方法
共享鎖的釋放和獨佔鎖的釋放有一定的差別,前面喚醒鎖的邏輯和獨佔鎖是一樣,先判斷頭結點是不是SIGNAL 狀態,如果是,則修改爲 0,並且喚醒頭結點的下一個節點
PROPAGATE : 標識爲 PROPAGATE 狀態的節點,是共享鎖模式下的節點狀態,處於這個狀態下的節點,會對線程的喚醒進行傳播
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);
}
// 這個 CAS 失敗的場景是:執行到這裏的時候,剛好有一個節點入隊,入隊會將這個 ws 設置爲 -1
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果到這裏的時候,前面喚醒的線程已經佔領了 head,那麼再循環
// 通過檢查頭節點是否改變了,如果改變了就繼續循環
if (h == head) // loop if head changed
break;
}
}
一旦 線程被喚醒,代碼又會繼續回到doAcquireSharedInterruptibly 中來執行。如果當前 state滿足=0 的條件,則會執行 setHeadAndPropagate 方法
五、總結
1.從代碼的實現來看,有點類似 join 的功能,但是比 join 更加靈活。CountDownLatch 構造函數會接收一個 int 類型的參數作爲計數器的初始值,當調用 CountDownLatch 的countDown 方法時,這個計數器就會減一。通過 await 方法去阻塞主流程
2.CountDownLatch的實現是基於共享鎖機制實現的
本文是綜合自己的認識和參考各類資料(書本及網上資料)編寫,若有侵權請聯繫作者,所有內容僅代表個人認知觀點,如有錯誤,歡迎校正; 郵箱:[email protected] 博客地址:https://blog.csdn.net/qq_35576976/