2.5 CountDownLatch
前兩個synchronized
和ReentrantLock
都是解決執行緒安全問題的好手,就像兩把寶劍,可以披荊斬棘大殺四方。下面我們來探索java.util.concurrent
包下面解決執行緒同步問題的功能類。
在使用多執行緒進行效能測試的過程中,經常需要基於事件、時間點進行執行緒的同步。例如我們整點搶紅包場景、前置資料併發初始化等。我們需要所有執行緒都到達某一個關鍵點之後,再進行下一步。在流程類的測試場景中,這種需求尤為常見。
要解決這類問題或者說實現此類需求,我們必然會用到執行緒同步類。CountDownLatch
是一個相對簡單同步工具類,可以實現讓一個或多個執行緒等待直到在其他執行緒中執行的操作完成後再繼續執行。如果你覺得不太好理解,類似的概念就是 JMeter 中的集合點,其含義就是所有執行緒都到達集合點集合一下,然後再各走各路。CountDownLatch
適合用於一個或多個執行緒等待其他一組執行緒完成工作後再繼續執行的場景。
CountDownLatch
是透過執行緒安全的計數器數值判斷是否到達集合點,工作流程如下:
(1)首先建立同步物件,並且設定同步數量
(2)任務執行緒執行任務,完成之後將計數器值減一
(3)等待執行緒(通常是 main 執行緒)會阻塞(可以設定超時),直到計數器歸零,繼續執行後面程式碼
2.5.1 基礎方法
CountDownLatch 的構造方法如下:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
這個方法只有一個 int 引數 count,如果 count 小於 0,會丟擲異常。這個 count 就是需要同步的數量,對應CountDownLatch
工作流的第 1 步。
CountDownLatch
工作流第 2 步,用到的計數器減一的方法如下:
public void countDown() {
sync.releaseShared(1);
}
方法沒有引數,直接呼叫即可,通常會跟try-catch-finally
同時使用,以保障每一個任務執行緒都會執行countDown()
方法。
CountDownLatch 工作流第 3 步,對應 2 個方法,方法一:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
方法二:
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
方法一可以看做方法二的無限等待版本。
當等待同步的執行緒執行到這個方法,會阻塞繼續執行,直到CountDownLatch
計數器歸零或者等待超時。一般來說,不建議新手使用某個無線等待的阻塞方法,但在 Java 效能測試最佳實戰中,筆者會推薦使用方法一。原因有兩點:一是CountDownLatch
通常是用在前置或者後置資料處理,併發執行時間無法準確估計;二是我們可以透過框架功能設計,規避CountDownLatch
真發生無限等待異常場景,還可以更加靈活控制集合時間。
2.5.2 最佳實戰
CountDownLatch 方法較少,使用流程簡單,透過下面這個例子,展示CountDownLatch
使用最佳實戰。
package org.funtester.performance.books.chapter02.section5;
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch示例
*/
public class CountDownLatchDemo {
public static void main(String[] args) {
CountDownLatch countDownLatch = new CountDownLatch(3);// 建立一個CountDownLatch例項
for (int i = 0; i < 3; i++) {// 建立3個執行緒
new Thread(() -> {// 建立一個執行緒
try {
Thread.sleep(100);// 睡眠100毫秒
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
countDownLatch.countDown();// 計數器減1
}
System.out.println(System.currentTimeMillis() + " 任務完成 " + Thread.currentThread().getName());// 列印日誌
}).start();// 啟動執行緒
}
System.out.println(System.currentTimeMillis() + " 等待任務完成 " + Thread.currentThread().getName());// 列印日誌
try {
countDownLatch.await();// 等待計數器歸零
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(System.currentTimeMillis() + " 等待結束 " + Thread.currentThread().getName());// 列印日誌
}
}
在上面的示例中,首先建立了同步數量為 3 的 CountDownLatch 物件,接著建立了 3 個執行緒,每個執行緒下面 100 毫秒,然後計數器減一,列印任務完成日誌。下面是控制檯輸出:
1698497720691 等待任務完成 main
1698497720791 任務完成 Thread-0
1698497720791 任務完成 Thread-2
1698497720791 任務完成 Thread-1
1698497720791 等待結束 main
可以看到,main
執行緒在到達await()
方法後,阻塞了 100 毫秒後,3 個任務均完成,計數器歸零,main
執行緒執行了列印等待結束的程式碼。
2.5.3 使用場景
CountDownLatch
常用的使用場景如下:
- 執行緒等待。使用
CountDownLatch
可以讓一個或者多個執行緒一直等待,直到其他執行緒均完成預定的任務。例如:在併發初始化前置資料場景。 - 傳送起止訊號。使用
CountDownLatch
功能可以給一組執行緒傳送訊號,啟動或者結束改組執行緒。例如:整點搶紅包場景。
在使用場景中,CountDownLatch
與java.lang.Thread#sleep(long)
方法有部分重合,有些場景甚至相互替代。這裡筆者建議在多執行緒場景中儘量少使用java.lang.Thread#sleep(long)
方法,相比之下,CountDownLatch
有一下幾點優勢:
- 更加精準控制執行緒同步時機。
CountDownLatch
可以精確控制等待的執行緒數目,而 sleep 只能透過輪詢來實現等待,容易出現錯誤。 - 更好的效能。
CountDownLatch
可以使執行緒處於等待狀態而不是佔用 CPU 時間片,sleep 會導致執行緒不停醒來迴圈等待。 - 更優雅退出。
CountDownLatch
透過await()
設定超時控制退出等待,而sleep
不行;CountDownLatch
一旦計數器歸零,程式會立即退出等待,而sleep
必須等到sheep
結束才行。 - 使用更優雅。
CountDownLatch
的介面簡單直接,而 sleep 需要估算時間,設定不當容易造成浪費。 - 程式碼可讀性強。
CountDownLatch
同步邏輯簡單易懂,而 sleep 方法若無註釋很難理解。
因此在需要等待其他執行緒完成的場景下,CountDownLatch
是一個更加簡單、可靠、安全的選擇。
人無完人,類無全能。下面說一下CountDownLatch
的缺點:
- 無法複用。
CountDownLatch
物件建立好之後,就無法重置計數器,無法複用物件。 - 不夠靈活。
CountDownLatch
物件建立好之後,就無法增加計數器數值,只能呼叫方法進行減一,無法應對複雜多執行緒場景。
總而言之,CountDownLatch
主要應用於一次性的簡單等待場景。對於複雜的多執行緒協調還需要其他更高階的同步工具。
書的名字:從 Java 開始做效能測試 。
如果本書內容對你有所幫助,希望各位不吝讚賞,讓我可以貼補家用。讚賞兩位數可以提前閱讀未公開章節。我也會嘗試製作本書的影片教程,包括必要的答疑。
FunTester 原創精華
【連載】從 Java 開始效能測試
- 混沌工程、故障測試、Web 前端
- 服務端功能測試
- 效能測試專題
- Java、Groovy、Go
- 白盒、工具、爬蟲、UI 自動化
- 理論、感悟、影片