前言:
CountDownLatch(倒計數器)是JDK併發包下的一個同步工具類,其內部是依賴於AQS(AbstractQueuedSynchronizer)的 共享鎖(共享模式)。
應用場景:
針對於 CountDownLatch 倒計時器, 一種典型的場景就是類似於火箭發射;在火箭發射前,爲了保證萬無一失,往往還要進行各項設備、儀器的檢測,只有等到所有的檢查完畢且沒問題後,引擎才能點火。那麼在檢測環節中多個檢測項可以同時併發進行的,只有所有檢測項全部完成後,纔會通知引擎點火的,這裏可以使用 CountDownLatch 來實現。
CountDownLatch 到底是怎麼實現的呢?彆着急,模擬代碼奉上:
代碼:
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @PACKAGE_NAME: com.lyl.aqs
* @ClassName: SimulateRocketLaunchDemo
* @Description: 使用 CountDownLatch 模擬火箭發射過程
* @Date: 2020-05-31 14:17
**/
public class SimulateRocketLaunchDemo implements Runnable{
// 設置了 10 個檢測項
static final CountDownLatch latch = new CountDownLatch(10);
static final SimulateRocketLaunchDemo demo = new SimulateRocketLaunchDemo();
@Override
public void run(){
// 模擬檢查任務
try {
Thread.sleep(new Random().nextInt(10) * 1000);
System.out.println(Thread.currentThread().getName().split("-")[3]
+ " check complete !");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//計數減一
//放在finally避免任務執行過程出現異常,導致countDown()不能被執行
latch.countDown();
}
}
// test
public static void main(String[] args) throws InterruptedException {
// 設置線程數爲10的固定線程池
ExecutorService exec = Executors.newFixedThreadPool(10);
for (int i=0; i<10; i++){
// 提交任務
exec.submit(demo);
}
// 等待檢查,只有當10個檢測項全部檢測完成後,纔會喚醒處於等待狀態的main主線程,讓其繼續執行
latch.await();
// 發射火箭
System.out.println("Fire!");
// 關閉線程池
exec.shutdown();
}
}
再提供一個CountDownLatch 的實際應用的例子,傳送門:懶漢式單例模式爲什麼要進行二次判空
源碼分析:
由於CountDownLatch 內部的實現是依賴於AQS的**共享鎖(共享模式)**的, 所以在分析源碼前,需要對AQS有基礎的瞭解,如果對AQS一點也不知道的話,請通過 AQS之ReentrantLock源碼解析 文章瞭解下AQS,這樣在後面CountDownLatch 分析源碼時會簡單些。
什麼是共享鎖、排它鎖?
①、共享鎖:允許多個線程可以同時獲取一個鎖; (CountDownLatch 使用的共享鎖)
②、排它鎖:一個鎖在同一時刻只運行一個線程擁有;(ReentrantLock 使用的排它鎖)
1、接下來主要分析CountDownLatch的這幾個方法:
2、構造方法 new CountDownLatch(10) :
public CountDownLatch(int count) {
if (count < 0) {
throw new IllegalArgumentException("count < 0");
}
// CountDownLatch內部維護了Sync內部類,內部類繼承了AQS父類
this.sync = new Sync(count);
}
①、接下來看看 Sync 類的構造方法:
Sync(int count) {
/**
* setState()方法是AQS提供的state變量的寫方法, state變量被volatile修飾,由於volatile的
* happen-before規則,被 volatile 修飾的變量單獨讀寫操作具有原子性
*/
setState(count);
}
②、然後在看看AQS提供的setState(int newCount) 方法 和 state變量:
/**
* The synchronization state.
*/
private volatile int state;
protected final void setState(int newState) {
state = newState;
}
3、CountDownLatch的 getCount( ) 方法:
public long getCount() {
// 調用 sync 內部類的getCount()方法
return sync.getCount();
}
①、Sync 內部類的getCount( ) 方法:
int getCount() {
// Sync 調用其父類AQS的 getState()方法
return getState();
}
②、AQS的getState()方法:
/**
* The synchronization state.
*/
private volatile int state;
protected final int getState() {
// 返回state同步狀態值
return state;
}
4、CountDownLatch 的 countDown( ) 方法:
public void countDown() {
// 調用Sync內部類的父類AQS的 releaseShared()共享鎖釋放模版方法
sync.releaseShared(1);
}
①、AQS的 releaseShared( ) 方法:
public final boolean releaseShared(int arg) {
/**
* tryReleaseShared()方法是嘗試釋放鎖,這個方法在AQS的子類Sync進行了重寫
*/
if (tryReleaseShared(arg)) {
/**
* 如果tryReleaseShared()方法嘗試釋放鎖成功,並且此時state同步狀態變量值爲0時,
* 則執行doReleaseShared方法,將在同步隊列中阻塞的線程喚醒
*/
doReleaseShared();
return true;
}
return false;
}
②、CountDownLatch 的 tryReleaseShared( )方法:
protected boolean tryReleaseShared(int releases) {
// for(;;) 與 while(true) 一樣的死循環
for (;;) {
// 獲取state同步變量值
int c = getState();
// 如果state同步變量值已經是0,則返回false
if (c == 0)
return false;
// 將state同步變量值進行減一
int nextc = c-1;
// 使用AQS提供的CAS算法方法更新state變量值
if (compareAndSetState(c, nextc))
// 如果nextc等於0,代表此時state同步變量值爲0了,返回true
return nextc == 0;
}
}
③、AQS提供的 doReleaseShared( ) 方法:喚醒同步隊列中阻塞的線程
Node節點的四種狀態值請參考文章:AQS之ReentrantLock源碼解析
private void doReleaseShared() {
for (;;) {
// head同步隊列中的隊列頭
Node h = head;
if (h != null && h != tail) {
/**
* 獲取head節點的狀態,AQS中的Node內部節點類中定義了四種狀態值
* 四種狀態值請參考上面 ↑ 文章
*/
int ws = h.waitStatus;
/**
* SIGNAL是四中狀態值之一:表示當前節點中的線程可以嘗試被喚醒
*/
if (ws == Node.SIGNAL) {
// 將節點的狀態使用CAS算法更新爲0,0表示初始化狀態
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
// 狀態更新0失敗,則進行下次循環
continue;
// 狀態成功更新爲0後,喚醒節點中的線程,此方法具體源碼可參考上面 ↑ 文章
unparkSuccessor(h);
}
/**
* 如果節點狀態值爲0,則使用CAS方法更新節點狀態值爲 Node.PROPAGATE
* PROPAGATE 是四中狀態值之一:該狀態表示可運行,只在共享模式下使用
*/
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
// 跳出循環
break;
}
}
5、CountDownLatch 的 await( ) 方法:
await( ) 方法:當state狀態變量值不爲0時,就一直將線程(main主線程)阻塞在同步隊列中;當state變量值爲0時,也會嘗試將線程喚醒,並將喚醒操作傳播下去。
public void await() throws InterruptedException {
// 調用Sync內部類的父類AQS的模版方法 acquireSharedInterruptibly()方法
sync.acquireSharedInterruptibly(1);
}
①、AQS的模版方法 acquireSharedInterruptibly(1) 方法:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
/**
* interrupted()判斷當前線程是否被中斷,注意:此方法會默認清除線程的中斷標誌
*/
if (Thread.interrupted())
throw new InterruptedException();
/**
* tryAcquireShared()嘗試訪問共享鎖,如果state同步狀態變量值不爲0,則返回-1
*/
if (tryAcquireShared(arg) < 0)
/**
* 將阻塞的線程創建Node節點,綁定節點類型爲共享模式,並將創建的節點加入同步隊列的隊尾
* 並且當新創建的Node節點的前驅結點爲head時,就會嘗試喚醒下一個節點中的線程
*/
doAcquireSharedInterruptibly(arg);
}
②、AQS提供的 doAcquireSharedInterruptibly( ) 方法:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 創建新Node節點,綁定共享模式,並將其插入到隊尾
final Node node = addWaiter(Node.SHARED);
// failed是中斷標誌位
boolean failed = true;
try {
for (;;) {
// 返回當前節點的前驅結點
final Node p = node.predecessor();
if (p == head) {
// 判斷當前state同步變量值是否爲0,不是0返回-1,是0返回1
int r = tryAcquireShared(arg);
// 如果 r大於0,表示state變量值爲0
if (r >= 0) {
// 將當前節點設置head隊列頭,並且嘗試喚醒同步隊列中阻塞的線程
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
/**
* shouldParkAfterFailedAcquire()是對當前節點的前驅結點的狀態進行判斷,以及去針對各種
* 狀態做出相應處理,由於文章篇幅問題,具體源碼本文不做講解;只需知道如果前驅結點p的狀態爲
* SIGNAL的話,就返回true。
*
* parkAndCheckInterrupt()方法會使當前線程進去waiting狀態,並且查看當前線程是否被中斷,
* interrupted() 同時會將中斷標誌清除。
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
/**
* 如果for(;;)循環中出現異常,並且failed=false沒有執行的話,cancelAcquire方法
* 就會將當前線程的狀態置爲 node.CANCELLED 已取消狀態,並且將當前節點node移出
* 同步隊列。
*/
cancelAcquire(node);
}
}
③、AQS提供的 setHeadAndPropagate( ) 方法:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 設置爲隊首
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果s節點是共享模式的,則調用doReleaseShared()方法
if (s == null || s.isShared())
// 喚醒阻塞在同步隊列中的線程
doReleaseShared();
}
}
end,本文解析 CountDownLatch 源碼已經寫完了,如果大家在看的時候,有些地方沒看明白的話,請先將這篇文章 AQS之ReentrantLock源碼解析 熟悉下,這篇文章中簡單講解了 AQS的原理,並且着重講解了獨佔模式(排它鎖)的 ReentrantLock,可以將這兩塊看完,在來看 CountDownLatch 就會感覺簡單些,邏輯也更加清晰些。
❤不要忘記留下你學習的足跡 [點贊 + 收藏 + 評論]嘿嘿ヾ
一切看文章不點贊都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!開個玩笑,動一動你的小手,點贊就完事了,你每個人出一份力量(點贊 + 評論)就會讓更多的學習者加入進來!非常感謝! ̄ω ̄=