併發工具類CountDownLatch的源碼分析以及使用場景

掃描下方二維碼或者微信搜索公衆號菜鳥飛呀飛,即可關注微信公衆號,閱讀更多Spring源碼分析Java併發編程文章。

微信公衆號

簡介

  • CountDownLatch是JUC包下提供的一個工具類,它的作用是讓一個或者一組線程等待其他線程執行完成後,自己再接着執行。從命名上可以猜出,它是通過倒着計數,最後打開門閂這把鎖,即每一個線程忙完自己的工作後,讓計數器遞減一次,當計數器遞減到0時,鎖(門閂)被打開,主線程(或者等待的線程)接着執行自己的工作。
  • 在實際工作中,我們可能會遇到需要利用多線程來處理問題的同時,還需要控制這些線程的執行順序,這個時候我們可以選擇使用Thread類中提供的join()方法,也可以使用今天即將介紹的CountDownLatch來解決,還可以使用JUC包下的另一個類CyclicBarrier來解決。

如何使用

  • CountDownLatch的使用十分簡單,它只有一個構造方法,在構造方法中需要傳入一個int類型的參數,這個參數就是用來控制CountDownLatch需要遞減多少次才釋放鎖(打開門閂)。CountDownLatch還提供了以下三個方法,詳細信息見下表。
方法名 方法作用
void await() 讓調用該方法的線程阻塞,當CountDownLatch的計數器減爲0時,纔會讓線程解阻塞
boolean await(long timeout, TimeUnit unit) 讓調用該方法的線程超時阻塞,如果超過了指定的時間,CountDownLatch的計數器還沒有減爲0,那麼線程就會直接返回
void countDown() 讓CountDownLatch的計數器減1,當計數器的值減爲0時,會讓阻塞在CountDownLatch的線程解阻塞
  • 下面以一個簡單的場景,簡單介紹下CountDownLatch的用法。在學生時代,總會有各種各樣的考試,每次考完試,各科老師都會進行閱卷,計算總分,總分排名。在這個過程中,各科的閱卷是同時進行的,由於每一科老師的閱卷速度不一樣,因此計算總分和總分排名的人需要等到所有老師閱卷完成後才能進行。這個時候我們可以用CountDownLatch這個工具類在程序中進行模擬一下這個場景。把每一科的老師當做一個線程,由於每一科老師的閱卷速度不一樣,因此採用讓線程隨機休眠一段時間,當每一科的老師閱卷完成後,就調用CountDownLatch的countDown()方法讓計數器減一,在主線程中調用CountDownLatch的await()的方法,目的爲了讓主線程等待所有老師閱卷完成,當所有老師閱卷完成時,計數器就減爲0了,主線程就會從await()方法處解阻塞,然後進行總分加和,排名等工作。示例的Demo如下。
public class CountDownLatchDemo {

    public static void main(String[] args) {
        CountDownLatch countDownLatch = new CountDownLatch(6);
        List<String> teachers = Arrays.asList("語文老師","數學老師","英語老師","物理老師","化學老師","生物老師");
        Random random = new Random();
        // 創建6個線程,模擬6個科目的老師同時開始閱卷
        List<Thread> threads = new ArrayList<>(6);
        for (int i = 0; i < 6; i++) {
            threads.add(new Thread(()->{
                try {
                    int workTime = random.nextInt(6) + 1;
                    // 讓線程睡眠一段時間,模擬老師的閱卷時間
                    Thread.sleep(workTime * 1000l);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName() + "閱卷完成");
                // 每位老師閱卷完成後,就讓計數器減1
                countDownLatch.countDown();
            },teachers.get(i)));
        }
        for (Thread thread : threads) {
            thread.start();
        }
        // 讓主線程等待所有老師閱卷完成後,再開始計算總分,進行排名
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("所有科目的老師均已閱卷完成");
        System.out.println("開始計算總分,然後排名");
    }
}

源碼分析

  • 瞭解了CountDownLatch的使用,接下來就看下CountDownLatch的工作原理。
  • CountDownLatch實際上是一種共享鎖,它的底層是使用隊列同步器AQS來實現的。CountDownLatch在構造方法中指定了計數器的初始值,即AQS中的同步變量state的值。在同一時刻它允許多個線程來訪問state,每當調用一次countDown()方法的時候,讓state的值減一;當調用await()方法時,會判斷此時state的值是否爲0,如果爲0,就讓當前線程返回,如果不爲0,就讓當前線程進入同步隊列等待。
  • 由於CountDownLatch是使用AQS來實現的,因此它的內部需要定義一個同步組件,該組件需要繼承AQS(這種做法是AQS系列鎖的通用做法),在CountDownLatch中定義了一個內部類Sync,Sync繼承了AQS,並重寫了AQS中的tryAcquireShared()、tryReleaseShared()方法。
  • 當使用CountDownLatch的構造方法創建CountDownLatch時,在構造方法中會實例化Sync組件,並通過Sync的有參構造器,初始化AQS中同步變量state的值,值的大小就是CountDownLatch的構造方法中傳入的int類型的數值,用來表示計數器的大小。
public CountDownLatch(int count) {
    if (count < 0) throw new IllegalArgumentException("count < 0");
    this.sync = new Sync(count);
}
private static final class Sync extends AbstractQueuedSynchronizer {

    Sync(int count) {
        setState(count);
    }
}
  • 當調用CountDownLatch的countDown()方法時,會調用到sync.releaseShared(1),即AQS的releaseShared()方法。releaseShared()方法的源碼如下。
public final boolean releaseShared(int arg) {
    // 嘗試釋放共享鎖
    if (tryReleaseShared(arg)) {
        /**
         * 當釋放鎖完成後,同步狀態state=0,此時說明後面的線程可以獲取鎖了
         * 如果此時同步隊列中有人的等待,就喚醒後面的線程
         * 如果無人等待,就將首節點的waitStatus設置爲-3,表示同步狀態可以無條件的傳播下去,即後面的線程都可以直接獲取鎖了
         */
        doReleaseShared();
        return true;
    }
    return false;
}
  • 在releaseShared()方法中,會先調用子類的tryReleaseShared()方法,那麼這裏就會調用到CountDownLatch中的內部類Sync的tryReleaseShared()方法。源碼如下。
protected boolean tryReleaseShared(int releases) {
    // Decrement count; signal when transition to zero
    // 將同步狀態進行減一,減一之後,同步狀態變爲0,就返回true,表示可以喚醒同步隊列中正在等待的線程了
    for (;;) {
        int c = getState();
        // 在對state進行減一操作之前,會先判斷一下state的值是否爲0,如果state已經爲0了,此時還有線程來對state進行減1,這個時候是不正常的操作,因此會返回false
        if (c == 0)
            return false;
        int nextc = c-1;
        // 利用CAS操作來設置state的值
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}
  • tryReleaseShared()方法的作用就是讓state的值減1,如果減一之後state的值變爲0,就返回true,表示此時可以喚醒同步隊列中正在等待的線程了,如果不爲0,就返回false。當tryReleaseShared()方法結束時,就會回到AQS的releaseShared()方法中,如果tryReleaseShared()方法返回的是false,那麼releaseShared()就直接返回結束了;如果tryReleaseShared()方法返回的是true,那麼接着就會執行doReleaseShared()方法。doReleaseShared()方法是AQS中定義的一個模板方法,是用來處理共享鎖的邏輯的,它的主要作用就是喚醒同步隊列中正在等待的線程。
  • 當調用CountDownLatch的await()時,會調用到sync.acquireSharedInterruptibly(1),即AQS的acquireSharedInterruptibly()方法。acquireSharedInterruptibly()方法的源碼如下。
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 響應中斷
    if (Thread.interrupted())
        throw new InterruptedException();
    // tryAcquireShared()方法是嘗試獲取鎖
    // 對於CountDownLatch而言,當state=0時,會返回1,這表示鎖被所有的線程都釋放了,當state不等於0時,會返回-1,表示還有線程持有鎖
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
  • 在acquireSharedInterruptibly()方法中會先調用子類的tryAcquireShared()方法,在這裏調用的是CountDownLatch中的內部類Sync的tryAcquireShared()方法。tryAcquireShared()方法的邏輯十分簡單,判斷了state的值是否爲0,如果爲0,就返回1,不爲0,就返回-1。當state爲0時,表示的是計數器的值被減爲了0,這表明所有線程已經完成了自己的工作,所以這個時候tryAcquireShared()方法返回1,那麼回到AQS中時,acquireSharedInterruptibly()方法就會直接結束,當前線程不會阻塞。如果state不爲0,就說明計數器的值還沒被減爲0,還有線程沒有執行完自己的工作,沒有調用countDown()方法,因此這個時候tryAcquireShared()方法返回-1,那麼回到AQS中時,acquireSharedInterruptibly()方法不會直接結束,而是接着執行doAcquireSharedInterruptibly()方法。doAcquireSharedInterruptibly()方法是AQS的一個模板方法,該方法的主要作用就是處理共享鎖相關邏輯,如果共享鎖獲取失敗時,就讓線程進入到同步隊列中park。在此處,如果計數器的值不爲0,那麼當前線程調用await()方法後,就會進入同步隊列中park,直到有其他線程調用countDown()方法將計數器減爲0時,纔會將當前線程喚醒,或者當前線程被中斷。
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}
  • await(long timeout, TimeUnit unit)方法的實現邏輯和await()方法類似,只不過在await()方法的基礎上添加了一個超時判斷,有興趣的朋友可以自行研究下。整體來講,CountDownLatch的原理相對而言比較簡單,和共享鎖的實現原理一樣,只不過tryAcquireShared()方法和tryReleaseShared()方法的邏輯稍微有所改動。

總結

  • 本文首先介紹了CountDownLatch的作用,它可以讓一個線程或者一組線程等待其他線程執行完成後,自己才運行,可以實現控制線程執行的順序的目的。然後本文通過一個閱卷、計算總分、排名的案例演示了CountDownLatch的使用方法。最後簡單分析了CountDownLatch的源碼實現,CountDownLatch的底層是根據AQS的共享鎖的相關方法來實現的。
  • CountDownLatch在使用過程中需要注意的是,最好使用await(long timeout, TimeUnit unit)來阻塞線程,因爲如果處理任務的子線程一直不執行完,就會一直不調用countDown()方法,這樣計數器就不會減爲0,導致主線程一直阻塞等待,什麼也幹不了,所以推薦使用await(long timeout, TimeUnit unit)。如果在具體業務中,必須要求所有線程執行完後再執行主線程的話,那就使用await()方法,不過在子線程處理任務的代碼中,最好使用try…catch…finally語句,然後在finally語句塊中進行countDown()調用,否則很容易給自己挖下深坑。
  • 本人有一次在使用CountDownLatch的過程中,由於使用的是await()方法,然後在子線程中也沒有使用try…catch…finally,最後因爲有一個線程在處理業務邏輯時報錯了,最後導致這個線程沒有調用countDown()方法,所以主線程一直阻塞,一直等在那兒。更慘的是,由於這是子線程中出了異常,也沒有try…catch…finally,這個時候主線程會 吞掉子線程的堆棧異常信息,最終導致什麼錯誤日誌也不打印。由於這段代碼是在服務啓動的時候就會執行,所以當時的現象就是,服務始終起不來,錯誤日誌不也打印。當時碰到這個問題的時候,這段代碼在線上已經運行了好久,只有在測試環境纔出現,所以壓根就沒往這個地方去想。再加上自身對多線程相關的知識掌握度幾乎爲0,查了好久都沒找到原因,服務器重啓了n次,眼看重啓大法不也好使了,只能向同事請教,最後終於找到了這個錯誤。最後的解決辦法就是在子線程中使用try…catch…finally,然後在finally語句塊中調用countDown()。這個問題其實用jstack命令,在服務器上看看線程的堆棧就能查到是哪兒出問題了,但還是因爲自身很菜,對多線程沒有足夠的瞭解,才花費了很長時間去解決。也正是因爲這次的教訓,讓我決定開始去學習併發相關的源碼。

推薦

微信公衆號

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