寫在開頭
在很多的面經中都看到過提問 CountDownLatch
的問題,正好我們最近也在梳理學習AQS(抽象佇列同步器),而CountDownLatch又是其中典型的代表,我們今天就繼續來學一下這個同步工具類!
CountDownLatch有何作用?
我們知道AQS是專屬於構造鎖和同步器的一個抽象工具類,基於它Java構造出了大量的常用同步工具,如ReentrantLock、Semaphore、ReentrantReadWriteLock、SynchronousQueue等等,我們今天的主角CountDownLatch同樣如此。
CountDownLatch(倒時器)允許N個執行緒阻塞在同一個地方,直至所有執行緒的任務都執行完畢。CountDownLatch 有一個計數器,可以透過countDown()方法對計數器的數目進行減一操作,也可以透過await()方法來阻塞當前執行緒,直到計數器的值為 0。
CountDownLatch的底層原理
想要迅速瞭解一個Java類的內部構造,或者使用原理,最快速直接的辦法就是看它的原始碼,這是很多初學者比較牴觸的,會覺得很多封裝起來的原始碼都晦澀難懂,誠然很多類內部實現是複雜,但我們作為Java工程師也不能只追求CRUD呀,培養自己看原始碼的習慣,硬著頭皮看段時間,程式碼能力絕對會提升的!
廢話說的有點多了,我們直接進入CountDownLatch內部去看看它的底層原理吧
【原始碼解析1】
//幾乎所有基於AQS構造的同步類,內部都需要一個靜態內部類去繼承AQS
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
}
private final Sync sync;
//構造方法中初始化count值
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
幾乎所有基於AQS構造的同步類,內部都需要一個靜態內部類去繼承AQS,並實現其提供的鉤子方法,透過封裝AQS中的state為count來確定多個執行緒的計時器。
countDown()方法
【原始碼解析2】
//核心方法,內部封裝了共享模式下的執行緒釋放
public void countDown() {
//內部類Sync,繼承了AQS
sync.releaseShared(1);
}
//AQS內部的實現
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//喚醒後繼節點
doReleaseShared();
return true;
}
return false;
}
在CountDownLatch中透過countDown來減少倒數計時數,這是最重要的一個方法,我們繼續跟進原始碼看到它透過releaseShared()方法去釋放鎖,這個方法是AQS內部的預設實現方法,而在這個方法中再一次的呼叫了tryReleaseShared(arg),這是一個AQS的鉤子方法,方法內部僅有預設的異常處理,真正的實現由CountDownLatch內部類Sync完成
【原始碼解析3】
// 對 state 進行遞減,直到 state 變成 0;
// 只有 count 遞減到 0 時,countDown 才會返回 true
protected boolean tryReleaseShared(int releases) {
// 自選檢查 state 是否為 0
for (;;) {
int c = getState();
// 如果 state 已經是 0 了,直接返回 false
if (c == 0)
return false;
// 對 state 進行遞減
int nextc = c-1;
// CAS 操作更新 state 的值
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
await()方法
除了countDown()方法外,在CountDownLatch中還有一個重要方法就是 await
,在多執行緒環境下,執行緒的執行順序並不一致,因此,對於一個倒時器也說,先開始的執行緒應該阻塞等待直至最後一個執行緒執行完成,而實現這一效果的就是await()方法!
【原始碼解析4】
// 等待(也可以叫做加鎖)
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 帶有超時時間的等待
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
其中await()方法可以配置帶有時間引數的,表示最大阻塞時間,當呼叫 await() 的時候,如果 state 不為 0,那就證明任務還沒有執行完畢,await() 就會一直阻塞,也就是說 await() 之後的語句不會被執行。然後,CountDownLatch 會自旋 CAS 判斷 state是否等於0,若是就會釋放所有等待的執行緒,await() 方法之後的語句得到執行。
CountDownLatch的使用
由於await的實現步驟和countDown類似,我們就不貼原始碼了,大家自己跟進去也很容易看明白,我們現在直接來一個小demo感受一下如何使用CountDownLatch做一個倒時器
【程式碼樣例1】
public class Test {
public static void main(String[] args) throws InterruptedException {
// 建立一個倒計數為 3 的 CountDownLatch
CountDownLatch latch = new CountDownLatch(3);
Thread service1 = new Thread(new Service("3", 1000, latch));
Thread service2 = new Thread(new Service("2", 2000, latch));
Thread service3 = new Thread(new Service("1", 3000, latch));
service1.start();
service2.start();
service3.start();
// 等待所有服務初始化完成
latch.await();
System.out.println("發射");
}
static class Service implements Runnable {
private final String name;
private final int timeToStart;
private final CountDownLatch latch;
public Service(String name, int timeToStart, CountDownLatch latch) {
this.name = name;
this.timeToStart = timeToStart;
this.latch = latch;
}
@Override
public void run() {
try {
Thread.sleep(timeToStart);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name);
// 減少倒計數
latch.countDown();
}
}
}
輸出:
3
2
1
發射
執行結果體現出了倒數計時的效果每隔1秒進行3,2,1的倒數;其實除了倒數計時器外CountDownLatch還有另外一個使用場景:實現多個執行緒開始執行任務的最大並行性
多個執行緒在某一時刻同時開始執行。類似於賽跑,將多個執行緒放到起點,等待發令槍響,然後同時開跑。
具體做法是: 初始化一個共享的 CountDownLatch 物件,將其計數器初始化為 1 (new CountDownLatch(1)),多個執行緒在開始執行任務前首先 coundownlatch.await(),當主執行緒呼叫 countDown() 時,計數器變為 0,多個執行緒同時被喚醒。
【程式碼樣例2】
public class Test {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
System.out.println("5位運動員就位!");
//等待發令槍響
countDownLatch.await();
System.out.println(Thread.currentThread().getName() + "起跑!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
// 裁判準備發令
Thread.sleep(2000);
//發令槍響
countDownLatch.countDown();
}
}
輸出:
5位運動員就位!
5位運動員就位!
5位運動員就位!
5位運動員就位!
5位運動員就位!
Thread-0起跑!
Thread-3起跑!
Thread-4起跑!
Thread-1起跑!
Thread-2起跑!
結尾彩蛋
如果本篇部落格對您有一定的幫助,大家記得留言+點贊+收藏呀。原創不易,轉載請聯絡Build哥!
如果您想與Build哥的關係更近一步,還可以關注“JavaBuild888”,在這裡除了看到《Java成長計劃》系列博文,還有提升工作效率的小筆記、讀書心得、大廠面經、人生感悟等等,歡迎您的加入!