Java並發包CountDownLatch、CyclicBarrier、Semaphore原理解析

2020-12-22 計算機java編程

前言

JUC中提供了很多同步工具類,比如CountDownLatch、CyclicBarrier、Semaphore等,都可以作用同步手段來實現多線程之間的同步效果

一、CountDownLatch

1.1、CountDownLatch的使用

CountDownLatch可以理解為是同步計數器,作用是允許一個或多個線程等待其他線程執行完成之後才繼續執行,比如打dota、LoL或者王者榮耀時,創建了一個五人房,只有當五個玩家都準備了之後,遊戲才能正式開始,否則遊戲主線程會一直等待著直到玩家全部準備。在玩家沒準備之前,遊戲主線程會一直處於等待狀態。如果把CountDownLatch比做此場景都話,相當於開始定義了匹配遊戲需要5個線程,只有當5個線程都準備完成了之後,主線程才會開始進行匹配操作。

CountDownLatch案例如下:

執行結果如下:

等待玩家準備中……遊戲房間等待玩家加入……線程Thread-2組隊準備,還需等待4人準備線程Thread-3組隊準備,還需等待3人準備線程Thread-4組隊準備,還需等待2人準備線程Thread-5組隊準備,還需等待1人準備線程Thread-6組隊準備,還需等待0人準備遊戲匹配中……遊戲房間已鎖定……線程Thread-7組隊準備,房間已滿不可加入線程Thread-8組隊準備,房間已滿不可加入線程Thread-9組隊準備,房間已滿不可加入線程Thread-10組隊準備,房間已滿不可加入線程Thread-11組隊準備,房間已滿不可加入線程Thread-12組隊準備,房間已滿不可加入線程Thread-13組隊準備,房間已滿不可加入

本案例中有兩個線程都調用了latch.await()方法,則這兩個線程都會被阻塞,直到條件達成。當5個線程調用countDown方法之後,達到了計數器的要求,則後續再執行countDown方法的效果就無效了,因為CountDownLatch僅一次有效

1.2、CountDownLatch的實現原理

CountDownLatch的實現原理主要是通過內部類Sync來實現的,內部類Sync是AQS的子類,主要是通過重寫AQS的共享式獲取和釋放同步狀態方法來實現的。源碼如下:

CountDownLatch初始化時需要定義調用count的次數,然後每調用一次countDown方法都會計數減一,源碼如下:

可以看出CountDownLatch的實現邏輯全部都是調用內部類Sync的對應方法實現的,Sync源碼如下:

通過內部類Sync的源碼可以分析出,CountDownLatch的實現完整邏輯如下:

1、初始化CountDownLatch實際就是設置了AQS的state為計數的值

2、調用CountDownLatch的countDown方法時實際就是調用AQS的釋放同步狀態的方法,每調用一次就自減一次state值

3、調用await方法實際就調用AQS的共享式獲取同步狀態的方法acquireSharedInterruptibly(1),這個方法的實現邏輯就調用子類Sync的tryAcquireShared方法,只有當子類Sync的tryAcquireShared方法返回大於0的值時才算獲取同步狀態成功,

否則就會一直在死循環中不斷重試,直到tryAcquireShared方法返回大於等於0的值,而Sync的tryAcquireShared方法只有當AQS中的state值為0時才會返回1,否則都返回-1,也就相當於只有當AQS的state值為0時,await方法才會執行成功,否則

就會一直處於死循環中不斷重試。

總結:

CountDownLatch實際完全依靠AQS的共享式獲取和釋放同步狀態來實現,初始化時定義AQS的state值,每調用countDown實際就是釋放一次AQS的共享式同步狀態,await方法實際就是嘗試獲取AQS的同步狀態,只有當同步狀態值為0時才能獲取成功。

二、CyclicBarrier

2.1、CyclicBarrier的使用

CyclicBarrier可以理解為一個循環同步屏障,定義一個同步屏障之後,當一組線程都全部達到同步屏障之前都會被阻塞,直到最後一個線程達到了同步屏障之後才會被打開,其他線程才可繼續執行。

還是以dota、LoL和王者榮耀為例,當第一個玩家準備了之後,還需要等待其他4個玩家都準備,遊戲才可繼續,否則準備的玩家會被一直處於等待狀態,只有當最後一個玩家準備了之後,遊戲才會繼續執行。

CyclicBarrier使用案例如下:

執行結果如下1:

線程Thread-0組隊準備,當前1人已準備線程Thread-1組隊準備,當前2人已準備線程Thread-2組隊準備,當前3人已準備線程Thread-3組隊準備,當前4人已準備線程Thread-4組隊準備,當前5人已準備線程:Thread-4開始組隊遊戲線程:Thread-0開始組隊遊戲線程:Thread-1開始組隊遊戲線程:Thread-2開始組隊遊戲線程:Thread-3開始組隊遊戲線程Thread-5組隊準備,當前1人已準備線程Thread-6組隊準備,當前2人已準備線程Thread-7組隊準備,當前3人已準備線程Thread-8組隊準備,當前4人已準備線程Thread-9組隊準備,當前5人已準備線程:Thread-9開始組隊遊戲線程:Thread-5開始組隊遊戲線程:Thread-7開始組隊遊戲線程:Thread-6開始組隊遊戲線程:Thread-8開始組隊遊戲線程Thread-10組隊準備,當前1人已準備線程Thread-11組隊準備,當前2人已準備

本案例中定義了達到同步屏障的線程為5個,每當一個線程調用了barrier.await()方法之後表示該線程已達到屏障,此時當前線程會被阻塞,只有當最後一個線程調用了await方法之後,被阻塞的其他線程才會被喚醒繼續執行。

另外CyclicBarrier是循環同步屏障,同步屏障打開之後立馬會繼續計數,等待下一組線程達到同步屏障。而CountDownLatch僅單次有效。

2.2、CyclicBarrier的實現原理

先看下CyclicBarrier的構造方法

CyclicBarrier的構造方法沒有特殊之處,主要是給兩個屬性parties(總線程數)、count(當前剩餘線程數)進行賦值,這裡需要兩個值的原因是CyclicBarrier提供了重置的功能,當調用reset方法重置時就需要將count值再賦值成parties的值

再看下await方法的實現邏輯

從源碼可以看出CyclicBarrier的實現原理主要是通過ReentrantLock和Condition來實現的,主要實現流程如下:

1、創建CyclicBarrier時定義了CyclicBarrier對象需要達到的線程數count

2、每當一個線程執行了await方法時,需要先通過ReentrantLock進行加鎖操作,然後對count進行自減操作,操作成功則判斷當前count是否為0;

3、如果當前count不為0則調用Condition的await方法使當前線程進入等待狀態;

4、如果當前count為0則表示同步屏障已經完全,此時調用Condition的signalAll方法喚醒之前所有等待的線程,並開啟循環的下一次同步屏障功能;

5、喚醒其他線程之後,其他線程繼續執行剩餘的邏輯。

2.3、通過Synchronized和wait/notify實現CyclicBarrier

通過分析了解了CyclicBarrier是通過ReentrantLock和Condition來實現的,而ReentrantLock+Condition在使用上基本上等同於Synchronized+wait/notify,既然如此就可以通過Synchronized+wait/notify來自定義一個CyclicBarrier,話不多說,代碼如下:

執行結果如下2:

線程Thread-0組隊準備,當前1人已準備 線程Thread-1組隊準備,當前2人已準備 線程Thread-2組隊準備,當前3人已準備 線程Thread-3組隊準備,當前4人已準備 線程Thread-4組隊準備,當前5人已準備 線程:Thread-4開始組隊遊戲 線程:Thread-3開始組隊遊戲 線程:Thread-0開始組隊遊戲 線程:Thread-1開始組隊遊戲 線程:Thread-2開始組隊遊戲 線程Thread-5組隊準備,當前1人已準備 線程Thread-6組隊準備,當前2人已準備 線程Thread-7組隊準備,當前3人已準備 線程Thread-8組隊準備,當前4人已準備 線程Thread-9組隊準備,當前5人已準備 線程:Thread-9開始組隊遊戲 線程:Thread-7開始組隊遊戲 線程:Thread-5開始組隊遊戲 線程:Thread-6開始組隊遊戲 線程:Thread-8開始組隊遊戲 線程Thread-10組隊準備,當前1人已準備 線程Thread-11組隊準備,當前2人已準備

可以看出實現的效果和CyclicBarrier實現的效果完全一樣

三、Semaphore

3.1、Semaphore的使用

Semaphore字面意思是信號量,實際可以看作是一個限流器,初始化Semaphore時就定義好了最大通行證數量,每次調用時調用方法來消耗,業務執行完畢則釋放通行證,如果通行證消耗完,再獲取通行證時就需要阻塞線程直到有通行證可以獲取。

比如銀行櫃檯的窗口,一共有5個窗口可以使用,當窗口都被佔用時,後面來的人就需要排隊等候,直到有窗口用戶辦理完業務離開之後後面的人才可繼續爭取。模擬代碼如下:

執行結果如下:

初始化5個銀行櫃檯窗口用戶Thread-0佔用窗口用戶Thread-0開始辦理業務用戶Thread-1佔用窗口用戶Thread-1開始辦理業務用戶Thread-2佔用窗口用戶Thread-2開始辦理業務用戶Thread-3佔用窗口用戶Thread-3開始辦理業務用戶Thread-4佔用窗口用戶Thread-4開始辦理業務用戶Thread-0離開窗口用戶Thread-5佔用窗口用戶Thread-5開始辦理業務用戶Thread-1離開窗口用戶Thread-6佔用窗口用戶Thread-6開始辦理業務用戶Thread-2離開窗口用戶Thread-7佔用窗口用戶Thread-7開始辦理業務用戶Thread-3離開窗口用戶Thread-8佔用窗口用戶Thread-8開始辦理業務用戶Thread-4離開窗口用戶Thread-9佔用窗口用戶Thread-9開始辦理業務用戶Thread-5離開窗口用戶Thread-6離開窗口用戶Thread-7離開窗口用戶Thread-8離開窗口用戶Thread-9離開窗口

可以看出前5個線程可以直接佔用窗口,但是後5個線程需要等待前面的線程離開了窗口之後才可佔用窗口。

Semaphore調用acquire方法獲取許可證,可以同時獲取多個,但是也需要對應的釋放多個,否則會造成其他線程獲取不到許可證的情況。一旦許可證被消耗完,那麼線程就需要被阻塞,直到許可證被釋放才可繼續執行。

另外Semaphore還具有公平模式和非公平模式兩種用法,公平模式則遵循FIFO原則先排隊的線程先拿到許可證;非公平模式則自行爭取。

3.2、Semaphore實現原理

Semaphore的構造方法

構造方法只有兩個參數,一個是許可證總數量,一個是是否為公平模式;默認是非公平模式

Semaphore的實現全部是通過其內部類Sync來實現了,Sync也是AQS的子類,Semaphore的實現方式基本上和ReentrantLock的實現原理如出一轍。

公平模式實現原理:

公平模式就是噹噹前線程是AQS同步隊列首節點的後繼節點時才有權利嘗試獲取共享式的同步狀態,並將同步狀態值減去需要佔用的許可證數量,如果剩餘許可證數量小於0則表示獲取失敗進入AQS的死循環不停重試;

如果許可證數量大於0並且CAS設置成功了,則返回剩餘許可證數量表示搶佔許可證成功;

非公平模式實現原理:

看我公平模式的實現基本是就可以猜到非公平模式是如何實現的,只是會少了一步判斷當前節點是否是首節點的後繼節點而已。

了解完Semaphore的公平模式和非公平模式的佔有許可證的方法,再分析釋放許可證的方法,不過可以先自行猜測下會是如何實現的,既然獲取許可證是通過state欄位不斷減少來實現的,那麼毫無疑問釋放許可證就肯定是不斷給state增加來實現的。

釋放許可證源碼如下:

Semaphore的釋放許可證實際就是調用AQS的共享式釋放同步狀態的方法,然後調用內部類Sync重寫的AQS的tryReleaseShared方法,實現邏輯就是不停CAS設置state的值加上需要釋放的數量,直到CAS成功。這裡少了AQS的邏輯解析,有興趣可自行回顧AQS的共享式釋放同步狀態的實現原理。

四、Extra Knowledge

4.1、CountDownLatch 和 CyclicBarrier的區別?

CountDownLatch和CyclicBarrier實現的效果看似都是某個線程等待一組線程達到條件之後才可繼續執行,但是實際上兩者存在很多區別。

1、CountDownLatch阻塞的是調用await()的線程,不會阻塞達到條件的線程;CyclicBarrier阻塞的是達到同步屏障的所有線程

2、CountDownLatch採用倒數計數,定義數量之後,每當一個線程達到要求之後就減一;CyclicBarrier是正數計數,當數量達到定義的數量之後就打開同步屏障

3、CountDownLatch僅單次有效,不可重複使用;CyclicBarrir可以循環重複使用

4、CountDownLatch定義的數量和實際線程數無關,可以有一個線程執行多次countDown();CyclicBarrier定義的數量和實際線程數一致,必須由多個線程都達到要求執行才行(線程調用await()方法之後就會被阻塞,想調用多次也不行的)

5、CountDownLatch是通過內部類Sync繼承AQS來實現的;CyclicBarrier是通過重入鎖ReentrantLock來實現的

6、CountDownLatch不可重置;CyclicBarrier可以調用reset方法進行重置

相關焦點

  • Java項目實踐,CountDownLatch實現多線程閉鎖
    摘要本文主要介紹Java多線程並發中閉鎖(Latch)的基本概念、原理、示例代碼、應用場景,通過學習,可以掌握多線程並發時閉鎖(Latch)的使用方法。概念「閉鎖」就是指一個被鎖住了的門將線程a擋在了門外(等待執行),只有當門打開後(其他線程執行完畢),門上的鎖才會被打開,a才能夠繼續執行。
  • Java並發工具三劍客之CountDownLatch源碼解析
    CountDownLatch是Java並發包下的一個工具類,latch是門閂的意思,顧名思義,CountDownLatch就是有一個門閂擋住了裡面的人(線程)出來,當count減到0的時候,門閂就打開了,人(線程)就可以出來了。
  • 「原創」Java並發編程系列22|倒計時器CountDownLatch
    本文轉載自【微信公眾號:java進階架構師,ID:java_jiagoushi】經微信公眾號授權轉載,如需轉載與原文作者聯繫並發編程中常遇到這種情況,一個線程需要等待另外多個線程執行後再執行。遇到這種情況你一般怎麼做呢?今天就介紹一種JDk提供的解決方案來優雅的解決這一問題,那就是倒計時器CountDownLatch。
  • Latch-up
    晶片中的latch up和ESD是一個很大的方向,有些大公司會有專門的設計團隊。本期講latch up為什麼要提及ESD呢?原因很簡單,這個latch up跟ESD相關。       相信很多IC designer對latch up都不陌生,它是指晶片內部觸發寄生npn和pnp引起電源地之間出現低阻通路進而導致電源地之間出現大電流的一種現象。
  • 一文搞懂 CountDownLatch 用法和源碼!
    TCountDownLatch {      public static void main(String[] args) {         CountDownLatch latch = new CountDownLatch(5);         Increment increment = new Increment(latch);         Decrement decrement = new Decrement(latch);
  • Java中的並發工具類CountDownLatch
  • java練習本(2019-06-12)
    題目java語言使用的字符集是?A.BCDB.ASCIIC.GBKD.Unicode答案與解析1.相關知識字符編碼英文全稱為Chacter encoding,設定一個默認的字符集,用字符集的每一個字符來指定集合中的某一個對象,來便於文本在計算機中的存儲和網絡通信。
  • Android-skin-support 換膚原理全面解析
    本篇來自 瀟風寒月 的投稿,對 Android 開發中的換膚原理進行了詳細解析。一起來看看!希望大家喜歡。這裡只講原理,具體的使用方式還是去看官方的文檔吧,源碼地址:https://github.com/ximsfei/Android-skin-support該庫的支持的功能如下:支持布局中用到的資源換膚。支持代碼中設置的資源換膚。
  • Java編發編程中CountDownLatch到底是個啥?
  • 鎖存器Latch和觸發器Flip-flop有何區別
    鎖存器Latch結構   latch:鎖存器,是由電平觸發,結構圖如下:   鎖存器latch的優缺點   優點:   1、面積比ff小   門電路是構建組合邏輯電路的基礎,而鎖存器和觸發器是構建時序邏輯電路的基礎
  • java——Scnner()類下面有幾種方法呢?
    一、Scnner類 Scnner類在java.util包中一個可以使用正則表達式來解析基本類型和字符串的簡單文本掃描器下面是從一個字符串讀取若干項的例子: 輸出為: 1 2 red blue 以下代碼使用正則表達式同時解析所有的 4 個標記,並可以產生與上例相同的輸出結果:
  • 【伴奏】賭神開場曲The Final Countdown(附譜)
    —————————————————————————————今天的曲目the final countdown,賭神bgm就源於該曲
  • Java中10大常問的關於String的問題
    從 JDK7 開始,這是可以的java 6 及以前的版本都不支持這樣做// 只在java 7及更高版本有!switch (str.toLowerCase()) {case 「a」:value = 1;break;case 「b」:value = 2;break;}4.
  • Java基礎面試題簡單總結
    Int是java的原始數據類型,Integer是java為int提供的封裝類。Java為每個原始類型提供了封裝類。java編譯器要求方法必須聲明拋出可能發生的非運行時異常,但是並不要求必須聲明拋出未被捕獲的運行時異常。
  • java集合詳解合集
    所以的集合類都位於java.util包下,後來為了處理多線程環境下的並發安全問題,java5還在java.util.concurrent包下提供了一些多線程支持的集合類。在學習Java中的集合類的API、編程原理的時候,我們一定要明白,"集合"是一個很古老的數學概念,它遠遠早於Java的出現。
  • Java之throw關鍵字的簡單介紹
    各位小夥伴們大家好,在之前的文章中小編介紹了異常的產生過程Java之異常產生過程解析,這次小編要介紹的是throw關鍵字,這個關鍵字可以在指定的方法中拋出指定的異常。使用格式:throw new xxxException("異常產生的原因");使用這個關鍵字,大家要注意以下幾點:throw關鍵字必須寫在方法的內部。
  • 最全面的Java多線程用法解析
    最全面的java多線程用法解析,如果你對Java的多線程機制並沒有深入的研究,那麼本文可以幫助你更透徹地理解Java多線程的原理以及使用方法。1.創建線程在Java中創建線程有兩種方法:使用Thread類和使用Runnable接口。在使用Runnable接口時需要建立一個Thread實例。
  • countdown軟體測試死亡時間是真的嗎 感興趣的朋友們一起來看看吧
    countdown軟體測試死亡時間是真的嗎 感興趣的朋友們一起來看看吧時間:2020-08-13 13:01   來源:114手機樂園   責任編輯:凌君 川北在線核心提示:原標題:countdown軟體測試死亡時間是真的嗎 感興趣的朋友們一起來看看吧 countdown死亡倒計時真的假的?
  • countdown軟體測試死亡時間是真的嗎 app測試時間準嗎
    countdown死亡倒計時真的假的?countdown這款軟體是一款有電影衍生出來的產品,很多朋友都很好奇這個countdown app測試死亡時間準不準,那麼今天小編就為大家帶來詳細的介紹,感興趣的朋友們一起來看看吧!  countdown軟體測試死亡時間是真的嗎 app測試時間準嗎