前言
本文介紹下面試的高能考點 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 排版