「原創」Java並發編程系列22|倒計時器CountDownLatch

2020-12-25 酷扯兒

本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫

並發編程中常遇到這種情況,一個線程需要等待另外多個線程執行後再執行。遇到這種情況你一般怎麼做呢?今天就介紹一種JDk提供的解決方案來優雅的解決這一問題,那就是倒計時器CountDownLatch。本文將分以下兩部分介紹:

CountDownLatch的使用CountDownLatch源碼分析1. CountDownLatch的使用

CountDownLatch的作用是讓線程等待其它線程完成一組操作後才能執行,否則就一直等待。

舉個開會的例子:

老闆先進入會議室準備材料等待5個員工陸續進入會議室員工到齊開始開會老闆不能一來就開會,必須要等員工都到了再開會,用CountDownLatch實現如下:

public class CountDownLatchTest {private static CountDownLatch countDownLatch = new CountDownLatch(5); // Boss線程,等待員工到齊開會 static class BossThread extends Thread { @Override public void run() { System.out.println("Boss進入會議室準備材料..."); System.out.println("Boss在會議室等待..."); try { countDownLatch.await(); // Boss等待 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Boss等到人齊了,開始開會..."); } } // 員工到達會議室 static class EmpleoyeeThread extends Thread { @Override public void run() { System.out.println("員工" + Thread.currentThread().getName() + ",到達會議室...."); countDownLatch.countDown(); } } public static void main(String[] args) { // Boss線程啟動 new BossThread().start(); // 員工到達會議室 for (int i = 0; i < countDownLatch.getCount(); i++) { new EmpleoyeeThread().start(); } }}

控制臺輸出:

Boss進入會議室準備材料...Boss在會議室等待...員工Thread-2,到達會議室....員工Thread-3,到達會議室....員工Thread-4,到達會議室....員工Thread-1,到達會議室....員工Thread-5,到達會議室....Boss等到人齊了,開始開會...

總結CountDownLatch的使用步驟:(比如線程A需要等待線程B和線程C執行後再執行)

創建CountDownLatch對象,設置要等待的線程數N(這裡是2);等待線程A調用await()掛起;線程B執行後調用countDown(),使N-1;線程C執行後調用countDown(),使N-1;調用countDown()後檢查N=0了,喚醒線程A,在await()掛起的位置繼續執行。2. CountDownLatch源碼分析

CountDownLatch是通過一個計數器來實現的,當我們在new 一個CountDownLatch對象的時候需要帶入該計數器值,該值就表示了線程的數量。每當一個線程完成自己的任務後,計數器的值就會減1。當計數器的值變為0時,就表示所有的線程均已經完成了任務,然後就可以恢復等待的線程繼續執行了。

2.1 類結構

CountDownLatch只有一個屬性Sync,Sync是繼承了AQS的內部類。

創建CountDownLatch時傳入一個count值,count值被賦值給

AQS.state

CountDownLatch是通過AQS共享鎖實現的,AQS這篇文章中詳細講解了AQS獨佔鎖的原理,AQS共享鎖和獨佔鎖原理只有很細微的區別,這裡大致介紹下:

線程調用acquireSharedInterruptibly()方法獲取不到鎖時,線程被構造成結點進入AQS阻塞隊列。當有線程調用releaseShared()方法將當前線程持有的鎖徹底釋放後,會喚醒AQS阻塞隊列中等鎖的線程,如果AQS阻塞隊列中有連續N個等待共享鎖的線程,就將這N個線程依次喚醒

public class CountDownLatch {private static final class Sync extends AbstractQueuedSynchronizer { Sync(int count) { setState(count); } } private final Sync sync; public CountDownLatch(int count) { if (count < 0) throw new IllegalArgumentException("count < 0"); this.sync = new Sync(count); }}

2.2 await()

await()

是將當前線程阻塞,理解

await()

的原理就是要弄清楚await()是如何將線程阻塞的。

await()

調用的就是AQS獲取共享鎖的方法。當

AQS.state=0

時才能獲取到鎖,由於創建CountDownLatch時設置了

state=count

,此時是獲取不到鎖的,所以調用

await()

的線程掛起並構造成結點進入AQS阻塞隊列。

創建CountDownLatch時設置AQS.state=count,可以理解成鎖被重入了count次。await()方法獲取鎖時鎖被佔用了,只能阻塞。

/*** CountDownLatch.await()調用的就是AQS獲取共享鎖的方法acquireSharedInterruptibly() */public void await() throws InterruptedException { sync.acquireSharedInterruptibly(1);}/** * 獲取共享鎖 * 如果獲取鎖失敗,就將當前線程掛起,並將當前線程構造成結點加入阻塞隊列 * 判斷是否獲取鎖成功的方法由CountDownLatch的內部類Sync實現 */public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) < 0) // 嘗試獲取鎖的方法由CountDownLatch的內部類Sync實現 doAcquireSharedInterruptibly(arg); // 獲取鎖失敗,就將當前線程掛起,並將當前線程構造成結點加入阻塞隊列}/** * CountDownLatch.Sync實現AQS獲取鎖的方法 * 只有AQS.state=0時獲取鎖成功。 * 創建CountDownLatch時設置了state=count,調用await()時state不為0,返回-1,表示獲取鎖失敗。 */protected int tryAcquireShared(int acquires) { return (getState() == 0) ? 1 : -1;}

2.3 countDown()

countDown()

方法是將count-1,如果發現count=0了,就喚醒阻塞的線程。

countDown()

調用AQS釋放鎖的方法,每次將state減1。當state減到0時是無鎖狀態了,就依次喚醒AQS隊列中阻塞的線程來獲取鎖,繼續執行邏輯代碼。

/*** CountDownLatch.await()調用的就是AQS釋放共享鎖的方法releaseShared() */public void countDown() { sync.releaseShared(1);}/** * 釋放鎖 * 如果鎖被全部釋放了,依次喚醒AQS隊列中等待共享鎖的線程 * 鎖全部釋放指的是同一個線程重入了N次需要N次解鎖,最終將state變回0 * 具體釋放鎖的方法由CountDownLatch的內部類Sync實現 */public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { // 釋放鎖,由CountDownLatch的內部類Sync實現 doReleaseShared(); // 鎖全部釋放之後,依次喚醒等待共享鎖的線程 return true; } return false;}/** * CountDownLatch.Sync實現AQS釋放鎖的方法 * 釋放一次,將state減1 * 如果釋放之後state=0,表示當前是無鎖狀態了,返回true */protected boolean tryReleaseShared(int releases) { for (;;) { int c = getState(); if (c == 0) return false; // state每次減1 int nextc = c-1; if (compareAndSetState(c, nextc)) return nextc == 0;// state=0時,無鎖狀態,返回true }}

總結

CountDownLatch用於一個線程A需要等待另外多個線程(B、C)執行後再執行的情況。

創建CountDownLatch時設置一個計數器count,表示要等待的線程數量。線程A調用

await()

方法後將被阻塞,線程B和線程C調用

countDown()

之後計數器count減1。當計數器的值變為0時,就表示所有的線程均已經完成了任務,然後就可以恢復等待的線程A繼續執行了。

CountDownLatch是由AQS實現的,創建CountDownLatch時設置計數器count其實就是設置

AQS.state=count

,也就是重入次數。

await()

方法調用獲取鎖的方法,由於

AQS.state=count

表示鎖被佔用且重入次數為count,所以獲取不到鎖線程被阻塞並進入AQS隊列。

countDown()

方法調用釋放鎖的方法,每釋放一次AQS.state減1,當AQS.state變為0時表示處於無鎖狀態了,就依次喚醒AQS隊列中阻塞的線程來獲取鎖,繼續執行邏輯代碼。

相關焦點

  • 原創】Java並發編程系列01|開篇獲獎感言
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫為什麼要學並發編程我曾聽一個從事15年開發工作的技術人員說過,他剛工作時的並發編程第一原則就是不要寫並發程序。
  • Java並發編程:CountDownLatch、CyclicBarrier和Semaphore
    在java 1.5中,提供了一些非常有用的輔助類來幫助我們進行並發編程,比如CountDownLatch,CyclicBarrier和Semaphore,今天我們就來學習一下這三個輔助類的用法。Thread-0所有線程寫入完畢,繼續處理其他任務... at java.util.concurrent.CyclicBarrier.dowait(Unknown Source) at java.util.concurrent.CyclicBarrier.await(Unknown Source) at com.cxh.test1.Test$Writer.run(Test.java:58) java.util.concurrent.BrokenBarrierException
  • 「原創」Java並發編程系列09|基礎乾貨
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第9篇。現在,我們進入正題:介紹並發編程的基礎性概念。
  • 「原創」Java並發編程系列02|並發編程三大核心問題
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫寫在前面編寫並發程序是比較困難的,因為並發程序極易出現Bug,這些Bug有都是比較詭異的,很多都是沒辦法追蹤,而且難以復現。
  • 「原創」Java並發編程系列28|Copy-On-Write容器
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫正文前面兩篇講了並發編程中線程安全HashMap:ConcurrentHashMap
  • 一篇不到300字短文,帶你徹底理解Java並發包中CountDownLatch
    本篇文章主要介紹了Java並發包中CountDownLatch的作用、使用場景、使用中的注意事項以及其相關擴展內容,讓大家更好地去運用Java並發包中的並發工具類。這時候可以通過Java並發包中的Condition條件變量來實現,但是Java並發包中提供了更加方便直接的工具類——java.util.concurrent.CountDownLatch。CountDownLatch可以實現一個(或者多個)線程等待其他線程完成一組特定操作之後繼續運行,這組操作被稱為先決操作。
  • 「原創」Java並發編程系列25|交換器Exchanger(3)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫正文很尷尬,發了並發編程的
  • 「原創」Java並發編程系列18|讀寫鎖(下)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 18 篇,文末有本系列文章匯總。上篇為【原創】Java並發編程系列17 | 讀寫鎖八講(上),沒看過的可以先看看。本文是下篇,從「源碼分析寫鎖的獲取與釋放」開始。7.
  • Java多線程並發編程中並發容器第二篇之List的並發類講解
    Java多線程並發編程中並發容器第二篇之List的並發類講解概述本文我們將詳細講解list對應的並發容器以及用代碼來測試ArrayList、vector以及CopyOnWriteArrayList在100個線程向list中添加1000個數據後的比較
  • 「原創」Java並發編程系列06|你不知道的final
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫final在Java中是一個保留的關鍵字,可以修飾變量、方法和類。那麼fianl在並發編程中有什麼作用呢?本文就在對final常見應用總結基礎上,講解final並發編程中的應用。
  • 快速掌握並發編程---CountDownLatch原理和實戰
    方法一在前面的文章中我們介紹了Thread類的join方法:快速掌握並發編程---Thread常用方法,join的工作原理是,不停檢查thread是否存活,如果存活則讓當前線程永遠wait,直到thread線程終止,線程的notifyAll就會被調用。下面我們就使用join來實現上面面試題。
  • Java並發工具三劍客之CountDownLatch源碼解析
    CountDownLatch是Java並發包下的一個工具類,latch是門閂的意思,顧名思義,CountDownLatch就是有一個門閂擋住了裡面的人(線程)出來,當count減到0的時候,門閂就打開了,人(線程)就可以出來了。
  • 「原創」Java並發編程系列14|AQS源碼分析
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫本文為何適原創並發編程系列第 14 篇,文末有本系列文章匯總。AbstractQueuedSynchronizer是Java並發包java.util.concurrent的核心基礎組件,是實現Lock的基礎。
  • 「原創」Java並發編程系列29|ConcurrentLinkedQueue
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫J.U.C 為常用的集合提供了並發安全的版本,前面講解了 map 的並發安全集合 ConcurrentHashMap,List 並發安全集合 CopyOnWriteArrayList,Set 並發安全集合
  • Java中的並發工具類CountDownLatch
    Java中的並發工具類在多線程編程的時候,有時候需要控制並發流,Java本身提供了幾個控制並發的工具類,比如CountDownLatch,CyclicBarrier,SemaphoreSemaphore用來控制同時訪問資源的線程數量,比如用來限制流量,限制並發數等。acquire()方法是獲取一個通行證,releas()方法是歸還通行證。比如進小區的安檢,只能一個一個的來,代碼實現如下。
  • 「原創」Java並發編程系列26|ConcurrentHashMap(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫終於輪到ConcurrentHashMap了,並發編程必備,也是面試必備。HashMap 作為使用最頻繁的集合之一,在多線程環境下是不能用的,因為 HashMap 的設計上就沒有考慮並發環境,極易導致線程安全問題。為了解決該問題,提供了 Hashtable 和 Collections.synchronizedMap(hashMap)兩種解決方案,但是這兩種方案都是對讀寫加獨佔鎖,一個線程在讀時其他線程必須等待,吞吐量和性能都較低。
  • Java並發編程之支持並發的list集合你知道嗎
    Java並發編程之-list集合的並發.我們都知道Java集合類中的arrayList是線程不安全的。那麼怎麼證明是線程不安全的呢?怎麼解決在並發環境下使用安全的list集合類呢?本篇是《凱哥(凱哥Java:kagejava)並發編程學習》系列之《並發集合系列》教程的第一篇:本文主要內容:怎麼證明arrayList不是線程安全的?怎麼解決這個問題?以及遇到問題解決的四個步驟及從源碼來分析作者思路。一:怎麼證明arrayList在並發情況下是線程不安全的呢?
  • 「零基礎學JAVA」基礎篇 第二章 JAVA編程初體驗
    JAVA【零基礎學編程】系列今天給大家帶來基礎篇 第二章 JAVA編程初體驗本節的部分編碼操作需要先安裝JDK開發工具「零基礎學JAVA」工具篇 JDK的安裝教程(WINDOWS版)和環境變量的配置「零基礎學JAVA」工具篇
  • 「原創」Java並發編程系列03|重排序-可見性和有序性問題根源
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫思維導圖寫在前面並發編程的三大問題:原子性、可見性、有序性。從java原始碼到最終實際執行的指令序列,會分別經歷下面三種重排序:編譯器優化的重排序。編譯器在不改變單線程程序語義的前提下,可以重新安排語句的執行順序。指令級並行的重排序。
  • 「原創」Java並發編程系列33|深入理解線程池(上)
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程必不可少的線程池,接下來分兩篇文章介紹線程池,本文是第一篇。介紹1.1 使用場景並發編程可以高效利用CPU資源,提升任務執行效率,但是多線程及線程間的切換也伴隨著資源的消耗。當遇到單個任務處理時間比較短,但需要處理的任務數量很大時,線程會頻繁的創建銷毀,大量的時間和資源都會浪費在線程的創建和銷毀上,效率很低。