CountDownLatch原理簡介和使用過程

前言

本文介紹下面試的高能考點 countDownLatch 的原理和應用

countDownLatch具有的功能

  • CountDownLatch主要有兩個方法,當一個或多個線程調用await方法時,這些線程會阻塞。

  • 其它線程調用countDown方法會將計數器減1(調用countDown方法的線程不會阻塞)

  • 當計數器的值變爲0時,因await方法阻塞的線程會被喚醒,繼續執行。

應用場景

典型的應用場景

如並行計算,當某個處理的運算量很大時,可以將該運算任務拆分成多個子任務,等待所有的子任務都完成之後,父任務再拿到所有子任務的運算結果進行彙總。

原理簡介

上圖解釋

  • TA線程 實例化 CountDownLatch對象並初始count爲3

  • TA調用await 使得當前線程等待count爲0 類似於看視屏的時候按了暫停鍵

  • 另外一個T1線程調用了該CountDownLatch對象的countDown方法 使得count變爲2

  • T2線程使得count變爲1

  • T3線程使得count變爲0

  • 此時就會喚醒處於“暫停”狀態的TA線程讓其繼續往下執行

進一步說明

  • CountDownLatch是一個計數器閉鎖,通過它可以完成類似於阻塞當前線程的功能,即:一個線程或多個線程一直等待,直到其他線程執行的操作完成。

  • CountDownLatch用一個給定的計數器來初始化,該計數器的操作是原子操作,即同時只能有一個線程去操作該計數器。

  • 調用該類await方法的線程會一直處於阻塞狀態,直到其他線程調用countDown方法使當前計數器的值變爲零,每次調用countDown計數器的值減1。

  • 當計數器值減至零時,所有因調用await()方法而處於等待狀態的線程就會繼續往下執行。

  • 這種現象只會出現一次,因爲計數器不能被重置,如果業務上需要一個可以重置計數次數的版本,可以考慮使用CycliBarrier

DEMO代碼分析

這個示例包含了 線程池和CountDownLatch 的內容 ,更加符合實際使用場景

詳細說明上述示例

針對CountDownLatch的說明

  • 初始化一個CountDownLatch 初始值count=5

  • 調用countDownLatch方法 count+-1即 count=count-1 每次調用count值都會減少1個

  • 調用await方法 就是讓初始化CountDownLatch的線程等待count值變爲0 然後纔會繼續執行下面的內容

結合線程池對CountDownDatch的說明

  • 主線程創建了一個count爲5的CountDownLatch

  • 主線程創建了一個線程池 裏面有5個核心線程

  • 往線程池中提交了5個線程並且傳入countDownLatch對象到子線程中去

  • 每一個子線程執行時會調用countDown方法將count減1

  • 主線程調用await 等待count變爲0

  • count變爲0 主線程將繼續執行

源碼分析

java.util.concurrent.CountDownLatch 

分析源碼的過程中大家把關注點放在我截圖上圈紅的地方 然後咱們慢慢的一點一點的把源碼看完

這裏將count值設置給state變量 調用了AQS類中setState方法

這個變量被volatile修飾
保證了這個變量 具有
1、可見性 
2、有序性 防止指令重排序
3、原子性

這裏先簡單介紹下可見性

上圖解釋

  • 每一個線程都會有一個本地內存 比如圖中線程A有本地內存A 該內存中保留了一份主內存中共享變量的副本

  • 線程A對本地內存A中的共享變量的副本修改了之後 然後會立刻同步刷新到主內存中

  • 並且會讓強制緩存了該變量的線程中的數據清空

  • 必須從主內存中重新讀取最新的數據

接着說CountDownLatch源碼分析

在初始化CountDownLatch的時候會實例化這個繼承了AQS的內部類Sync並且將count傳給AQS的state變量

countDownLatch的await方法 調用了 AQS的acquireSharedInterruptibly方法並且傳入了參數1 接下來對該方法分析

『tryAcquireShared』
在共享模式下嘗試獲取。這個方法需要查詢是否對象的狀態允許在共享模式下被獲取,如果允許則去獲取它。
這個方法總是被線程執行獲取共享鎖時被調用。如果這個方法報告失敗,那麼獲取方法可能會使線程排隊等待,如果它(即,線程)還沒入隊的話,直到其他的線程發出釋放的信號。
默認實現拋出一個“UnsupportedOperationException”
返回:
a)< 0 : 一個負數的返回表示失敗;
b) 0 : 0表示在共享模式下獲取鎖成功,但是後續的獲取共享鎖將不會成功
c)> 0 : 大於0表示共享模式下獲取鎖成功,並且後續的獲取共享鎖可能也會成功,在這種情況下後續等待的線程必須檢查是否有效。


看下AQS的子類Sync的tryAcquireShared方法的實現

這個邏輯過程中使用了大量的CAS來進行原子性的修改,當修改失敗的時候,是會通過for(;;)來重新循環的,也就是說『doAcquireSharedInterruptibly』使用自旋鎖(自旋+CAS)來保證在多線程併發的情況下,隊列節點狀態也是正確的以及在等待隊列的正確性,最終使得當前節點要麼獲取共享鎖成功,要麼被掛起等待喚醒

我們需要一個通知信號,主要是因爲當前線程要被掛起了(park)。
而如果waitStatus已經是’SIGNAL’的話就無需修改,直接掛起就好,
而如果waitStatus是’CANCELLED’的話,說明prev已經被取消了,是個無效節點了,那麼無需修改這個無效節點的waitStatus,而是需要先找到一個有效的prev。
因此,剩下的情況就只有當waitStatus爲’0’和’PROPAGAET’了(注意,waitStatus爲’CONDITION’是節點不在等待隊列中,所以當下情況waitStatus不可能爲’CONDITION’),這時我們需要將prev的waitStatus使用CAS的方式修改爲’SIGNAL’,而且只有修改成功的情況下,當前的線程才能安全被掛起。
還值得注意的時,因此該方法的CAS操作都是沒有自旋的,所以當它操作完CAS後都會返回false,在外層的方法中會使用自旋,當發現返回的是false時,會再次調用該方法,以檢查保證有當前node有一個有效的prev,並且其waitStatus爲’SIGNAL’,在此情況下當前的線程纔會被掛起(park)。

雙向鏈表數據結構

雙向鏈表數據結構

源碼分析未完待續 下篇文章繼續分析

DEMO代碼鏈接

https://gitee.com/pingfanrenbiji/myconcurrent/tree/master/src/main/java/pers/hanchao/concurrent/eg14

參考文章

https://www.jianshu.com/p/9ee0194d598c

本文使用 mdnice 排版

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