Java CountDownLatch的两种常用场景
轉載請標明出處:http://blog.csdn.net/zhaoyanjun6/article/details/120506758
本文出自【趙彥軍的博客】
文章目錄
- 簡介
- 場景1 讓多個線程等待:模擬并發,讓并發線程一起執行
- 場景2 讓單個線程等待:多個線程(任務)完成后,進行匯總合并
- CountDownLatch 工作原理
- CountDownLatch與Thread.join
- CountDownLatch與CyclicBarrier
- 超時時間
- Android 啟動實戰
簡介
先來看看 CountDownLatch 的源碼注釋;
/*** A synchronization aid that allows one or more threads to wait until* a set of operations being performed in other threads completes.** @since 1.5* @author Doug Lea*/ public class CountDownLatch { }描述如下:它是一個同步工具類,允許一個或多個線程一直等待,直到其他線程運行完成后再執行。
通過描述,可以清晰的看出,CountDownLatch的兩種使用場景:
- 場景1:讓多個線程等待
- 場景2:和讓單個線程等待。
場景1 讓多個線程等待:模擬并發,讓并發線程一起執行
為了模擬高并發,讓一組線程在指定時刻(秒殺時間)執行搶購,這些線程在準備就緒后,進行等待(CountDownLatch.await()),直到秒殺時刻的到來,然后一擁而上;
這也是本地測試接口并發的一個簡易實現。
在這個場景中,CountDownLatch充當的是一個發令槍的角色;
就像田徑賽跑時,運動員會在起跑線做準備動作,等到發令槍一聲響,運動員就會奮力奔跑。和上面的秒殺場景類似,代碼實現如下:
運行結果:
【Thread-0】開始執行…… 【Thread-1】開始執行…… 【Thread-4】開始執行…… 【Thread-3】開始執行…… 【Thread-2】開始執行……我們通過 CountDownLatch.await(),讓多個參與者線程啟動后阻塞等待,然后在主線程 調用CountDownLatch.countdown(1) 將計數減為 0,讓所有線程一起往下執行;
以此實現了多個線程在同一時刻并發執行,來模擬并發請求的目的。
場景2 讓單個線程等待:多個線程(任務)完成后,進行匯總合并
很多時候,我們的并發任務,存在前后依賴關系;比如數據詳情頁需要同時調用多個接口獲取數據,并發請求獲取到數據后、需要進行結果合并;或者多個數據操作完成后,需要數據check;
這其實都是:在多個線程(任務)完成后,進行匯總合并的場景。
代碼實現如下:
CountDownLatch countDownLatch = new CountDownLatch(5); for (int i = 0; i < 5; i++) {final int index = i;new Thread(() -> {try {Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));System.out.println("finish" + index + Thread.currentThread().getName());countDownLatch.countDown();} catch (InterruptedException e) {e.printStackTrace();}}).start(); }countDownLatch.await();// 主線程在阻塞,當計數器==0,就喚醒主線程往下執行。 System.out.println("主線程:在所有任務運行完成后,進行結果匯總");運行結果:
finish4Thread-4 finish1Thread-1 finish2Thread-2 finish3Thread-3 finish0Thread-0 主線程:在所有任務運行完成后,進行結果匯總在每個線程(任務) 完成的最后一行加上 CountDownLatch.countDown(),讓計數器-1;
當所有線程完成 -1,計數器減到0后,主線程往下執行匯總任務。
從上面兩個例子的代碼,可以看出 CountDownLatch的API并不多;
- CountDownLatch 的構造函數中的count就是閉鎖需要等待的線程數量。這個值只能被設置一次,而且不能重新設置;
- await():調用該方法的線程會被阻塞,直到構造方法傳入的 N 減到 0 的時候,才能繼續往下執行;
- countDown():使 CountDownLatch 計數值 減 1;
CountDownLatch 工作原理
CountDownLatch是通過一個計數器來實現的,計數器的初始值為線程的數量;
調用await()方法的線程會被阻塞,直到計數器 減到0的時候,才能繼續往下執行;
調用了await()進行阻塞等待的線程,它們阻塞在Latch門閂/柵欄上;只有當條件滿足的時候(countDown() N次,將計數減為0),它們才能同時通過這個柵欄;以此能夠實現,讓所有的線程站在一個起跑線上。
countDown()方法則是將計數器減1;
在CountDownLatch的構造函數中,指定的線程數量,只能指定一次;由于CountDownLatch采用的是減計數,因此當計數減為0時,計數器不能被重置。
這是和CyclicBarrier的一個重要區別。
CountDownLatch 的源碼在JUC并發工具中,也相對算是簡單的;
底層基于 AbstractQueuedSynchronizer 實現,CountDownLatch 構造函數中指定的count直接賦給AQS的state;每次countDown()則都是release(1)減1,最后減到0時unpark阻塞線程;這一步是由最后一個執行countdown方法的線程執行的。
而調用await()方法時,當前線程就會判斷state屬性是否為0,如果為0,則繼續往下執行,如果不為0,則使當前線程進入等待狀態,直到某個線程將state屬性置為0,其就會喚醒在await()方法中等待的線程。
CountDownLatch與Thread.join
CountDownLatch的作用就是允許一個或多個線程等待其他線程完成操作,看起來有點類似join()方法,但其提供了比 join()更加靈活的API。
CountDownLatch可以手動控制在n個線程里調用n次countDown()方法使計數器進行減一操作,也可以在一個線程里調用n次執行減一操作。
而 join() 的實現原理是不停檢查join線程是否存活,如果 join 線程存活則讓當前線程永遠等待。所以兩者之間相對來說還是CountDownLatch使用起來較為靈活。
CountDownLatch與CyclicBarrier
CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,只不過它們側重點不同:
CountDownLatch一般用于一個或多個線程,等待其他線程執行完任務后,再才執行
CyclicBarrier一般用于一組線程互相等待至某個狀態,然后這一組線程再同時執行
另外,CountDownLatch是減計數,計數減為0后不能重用;而CyclicBarrier是加計數,可置0后復用。
超時時間
通過 await 可以設置要等待的最長時間
public boolean await(long timeout, TimeUnit unit)返回值:
- 如果計數到達零,則返回true;如果在計數到達零之前超過了等待時間,則返回false
拋出異常:
- InterruptedException-如果當前線程在等待時被中斷
Android 啟動實戰
在大型的Android 項目中,我們一般會在 Application 的 onCreate 方法中初始化第三方sdk,隨著項目越來越大,需要初始化的 sdk 就越來越多,舉個例子,我們寫一個方法,模擬初始化sdk 需要耗時 200毫秒,如下:
private fun initSDK() {//模擬初始化sdk耗時200毫秒Thread.sleep(200)}假如我們有 20 個類似的 SDK 需要初始化,那么總耗時就是翻 20 倍。舉例如下:
class App : Application() {override fun onCreate() {super.onCreate()val start = System.currentTimeMillis()repeat(20) {initSDK()}val end = System.currentTimeMillis()Log.d("start-", "啟動耗時 time:${end - start}")}private fun initSDK() {//模擬初始化sdk耗時200毫秒Thread.sleep(200)} }日式輸出:
D/start-: 啟動耗時 time:4004可以看到啟動耗時 4 秒鐘,app 從啟動到進入首頁是非常慢的。
下面我們用學到的 CountDownLatch 優化一下,優化如下:
class App : Application() {override fun onCreate() {super.onCreate()val start = System.currentTimeMillis()//模擬20個異步任務val countDownLatch = CountDownLatch(20)//創建線程池val executor = Executors.newCachedThreadPool()repeat(20) {//在子線程中執行初始化工作executor.execute {initSDK()countDownLatch.countDown()}}//等待子線程執行完畢countDownLatch.await()val end = System.currentTimeMillis()Log.d("start-", "啟動耗時 time:${end - start}")}private fun initSDK() {//模擬初始化sdk耗時200毫秒Thread.sleep(200)} }輸出日志:
D/start-: 啟動耗時 time:205由此可見,我們的耗時從 4000 毫秒降為 205 毫秒,效果非常明顯。
利用這個知識,我寫了一個 Android 啟動庫:
https://github.com/zyj1609wz/Startup
總結
以上是生活随笔為你收集整理的Java CountDownLatch的两种常用场景的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: AES加密 — 详解
- 下一篇: Android ConstraintLa