AQS之CountDownLatch源碼解析

前言:

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 就會感覺簡單些,邏輯也更加清晰些。

不要忘記留下你學習的足跡 [點贊 + 收藏 + 評論]嘿嘿ヾ

一切看文章不點贊都是“耍流氓”,嘿嘿ヾ(◍°∇°◍)ノ゙!開個玩笑,動一動你的小手,點贊就完事了,你每個人出一份力量(點贊 + 評論)就會讓更多的學習者加入進來!非常感謝! ̄ω ̄=

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章