Java多執行緒程式設計-(6)-兩種常用的執行緒計數器CountDownLatch和迴圈屏障CyclicBarrier
原文出自 : https://blog.csdn.net/xlgen157387/article/details/78218736
倒計時CountDownLatch
CountDownLatch是一個非常實用的多執行緒控制工具類,稱之為“倒計時器”,它允許一個或多個執行緒一直等待,直到其他執行緒的操作執行完後再執行。
舉了例子:
我們知道的集齊七顆龍珠就可以召喚神龍,那我們就一起召喚一下,下邊我需要派7個人(7個執行緒)去分別去找這7顆不同的龍珠,每個人找到之後回來告訴我還需要等待的龍珠個數減1個,那麼當全部的人都找到龍珠之後,那麼我就可以召喚神龍了。
順便寫個程式碼如下:
public class SummonDragonDemo {
private static final int THREAD_COUNT_NUM = 7;
private static CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT_NUM);
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
try {
System.out.println("第" + index + "顆龍珠已收集到!");
//模擬收集第i個龍珠,隨機模擬不同的尋找時間
Thread.sleep(new Random().nextInt(3000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//每收集到一顆龍珠,需要等待的顆數減1
countDownLatch.countDown();
}).start();
}
//等待檢查,即上述7個執行緒執行完畢之後,執行await後邊的程式碼
countDownLatch.await();
System.out.println("集齊七顆龍珠!召喚神龍!");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
執行結果如下:
第7顆龍珠已收集到!
第2顆龍珠已收集到!
第6顆龍珠已收集到!
第3顆龍珠已收集到!
第5顆龍珠已收集到!
第4顆龍珠已收集到!
第1顆龍珠已收集到!
集齊七顆龍珠!召喚神龍!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
上述的執行結果可以看出,當分配的7個人(7個執行緒)分別找到龍珠之後,也就是所有的執行緒執行完畢,才可以召喚龍珠(執行countDownLatch.await()之後的程式碼)。
注意:
(1)CountDownLatch的建構函式
CountDownLatch countDownLatch = new CountDownLatch(7);
- 1
7表示需要等待執行完畢的執行緒數量。
(2)在每一個執行緒執行完畢之後,都需要執行countDownLatch.countDown()
方法,不然計數器就不會準確;
(3)只有所有的執行緒執行完畢之後,才會執行 countDownLatch.await()
之後的程式碼;
(4)可以看出上述程式碼中CountDownLatch 阻塞的是主執行緒;
那麼,假如我們不是用計數器CountDownLatch的話,結果可想而知,示例如下:
public class SummonDragonDemo {
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) throws InterruptedException {
for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
try {
System.out.println("第" + index + "顆龍珠已收集到!");
//模擬收集第i個龍珠,隨機模擬不同的尋找時間
Thread.sleep(Thread.sleep(Math.abs(new Random().nextInt(3000))););
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
System.out.println("集齊七顆龍珠!召喚神龍!");
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
執行結果如下:
第1顆龍珠已收集到!
集齊七顆龍珠!召喚神龍!
第2顆龍珠已收集到!
第3顆龍珠已收集到!
第4顆龍珠已收集到!
第5顆龍珠已收集到!
第6顆龍珠已收集到!
第7顆龍珠已收集到!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
結果只能呵呵了!
好啦!上邊說了一堆水話,下面說點官方的解釋:
CountDownLatch是在java1.5被引入的,它存在於java.util.concurrent包下。CountDownLatch這個類能夠使一個執行緒等待其他執行緒完成各自的工作後再執行。例如,應用程式的主執行緒希望在負責啟動框架服務的執行緒已經啟動所有的框架服務之後再執行。
CountDownLatch是通過一個計數器來實現的,計數器的初始值為執行緒的數量。每當一個執行緒完成了自己的任務後,計數器的值就會減1。當計數器值到達0時,它表示所有的執行緒已經完成了任務,然後在閉鎖上等待的執行緒就可以恢復執行任務。
CountDownLatch.java類中定義的建構函式:
public CountDownLatch(int count) { ... }
- 1
構造器中的計數值(count)實際上就是閉鎖需要等待的執行緒數量。這個值只能被設定一次,而且CountDownLatch沒有提供任何機制去重新設定這個計數值。
與CountDownLatch的第一次互動是主執行緒等待其他執行緒。主執行緒必須在啟動其他執行緒後立即呼叫CountDownLatch.await()方法。這樣主執行緒的操作就會在這個方法上阻塞,直到其他執行緒完成各自的任務。
其他N 個執行緒必須引用閉鎖物件,因為他們需要通知CountDownLatch物件,他們已經完成了各自的任務。這種通知機制是通過 CountDownLatch.countDown()方法來完成的;每呼叫一次這個方法,在建構函式中初始化的count值就減1。所以當N個執行緒都呼叫了這個方法,count的值等於0,然後主執行緒就能通過await()方法,恢復執行自己的任務。
CountDownLatch在實時系統中的使用場景
讓我們嘗試羅列出在java實時系統中CountDownLatch都有哪些使用場景。我所羅列的都是我所能想到的。如果你有別的可能的使用方法,請在留言裡列出來,這樣會幫助到大家。
(1)實現最大的並行性:有時我們想同時啟動多個執行緒,實現最大程度的並行性。例如,我們想測試一個單例類。如果我們建立一個初始計數為1的CountDownLatch,並讓所有執行緒都在這個鎖上等待,那麼我們可以很輕鬆地完成測試。我們只需呼叫 一次countDown()方法就可以讓所有的等待執行緒同時恢復執行。
(2)開始執行前等待n個執行緒完成各自任務:例如應用程式啟動類要確保在處理使用者請求前,所有N個外部系統已經啟動和運行了。
(3)死鎖檢測:一個非常方便的使用場景是,你可以使用n個執行緒訪問共享資源,在每次測試階段的執行緒數目是不同的,並嘗試產生死鎖。
迴圈屏障CyclicBarrier
CyclicBarrier是另一種多執行緒併發控制使用工具,和CountDownLatch非常類似,他也可以實現執行緒間的計數等待,但他的功能要比CountDownLatch更加強大一些。
CyclicBarrier 的字面意思是可迴圈使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活。
CyclicBarrier預設的構造方法是CyclicBarrier(int parties)
,其引數表示屏障攔截的執行緒數量,每個執行緒呼叫await
方法告訴CyclicBarrier我已經到達了屏障,然後當前執行緒被阻塞。
CyclicBarrier強調的是n個執行緒,大家相互等待,只要有一個沒完成,所有人都得等著。
還接著上述“集齊七顆龍珠!召喚神龍”的故事。召喚神龍,需要7個法師去尋找龍珠,但這7個法師並不是一下子就能號召起來的,所以要等待召集齊7個法師,然後在秋名山頂燒香拜佛為這7位法師送行,讓他們同時出發,前往不同的地方尋找龍珠(敲黑板:這是第一個屏障點),在這七位法師臨行時約定找到龍珠之後還回到這個地方等待其他法師找到龍珠之後一起去見我。幾年之後,第一個法師回來了,然後等待其他的法師。。。,最後所有的法師全部到齊(敲黑板:這是第一個屏障點),然後組隊來找我召喚神龍。
示例程式碼如下:
public class SummonDragonDemo {
private static final int THREAD_COUNT_NUM = 7;
public static void main(String[] args) {
//設定第一個屏障點,等待召集齊7位法師
CyclicBarrier callMasterBarrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
@Override
public void run() {
System.out.println("7個法師召集完畢,同時出發,去往不同地方尋找龍珠!");
summonDragon();
}
});
//召集齊7位法師
for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
try {
System.out.println("召集第" + index + "個法師");
callMasterBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
/**
* 召喚神龍:1、收集龍珠;2、召喚神龍
*/
private static void summonDragon() {
//設定第二個屏障點,等待7位法師收集完7顆龍珠,召喚神龍
CyclicBarrier summonDragonBarrier = new CyclicBarrier(THREAD_COUNT_NUM, new Runnable() {
@Override
public void run() {
System.out.println("集齊七顆龍珠!召喚神龍!");
}
});
//收集7顆龍珠
for (int i = 1; i <= THREAD_COUNT_NUM; i++) {
int index = i;
new Thread(() -> {
try {
System.out.println("第" + index + "顆龍珠已收集到!");
summonDragonBarrier.await();
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
執行結果:
召集第2個法師
召集第1個法師
召集第3個法師
召集第4個法師
召集第6個法師
召集第5個法師
召集第7個法師
7個法師召集完畢,同時出發,去往不同地方尋找龍珠!
第1顆龍珠已收集到!
第2顆龍珠已收集到!
第3顆龍珠已收集到!
第4顆龍珠已收集到!
第5顆龍珠已收集到!
第6顆龍珠已收集到!
第7顆龍珠已收集到!
集齊七顆龍珠!召喚神龍!
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
程式碼中設定了兩個屏障點,第一個用於召集7個法師,等7個法師召集完後,在設定在一個屏障點,7位法師去尋找龍珠,然後召喚神龍,中間有個巢狀的關係!
上述的例子,大致說了一下屏障,因為設定了兩個屏障,並沒有演示上述說的可迴圈使用(Cyclic)的屏障(Barrier) 中的可迴圈使用(Cyclic)
/**
* Resets the barrier to its initial state. If any parties are
* currently waiting at the barrier, they will return with a
* {@link BrokenBarrierException}. Note that resets <em>after</em>
* a breakage has occurred for other reasons can be complicated to
* carry out; threads need to re-synchronize in some other way,
* and choose one to perform the reset. It may be preferable to
* instead create a new barrier for subsequent use.
*/
public void reset() { ... }
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
檢視CyclicBarrier.reset()
可知,可以使CyclicBarrier回到最初始的狀態,由於使用的相對較少,這裡不再演示。
CyclicBarrier和CountDownLatch的區別
(1)CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。所以CyclicBarrier能處理更為複雜的業務場景,比如如果計算髮生錯誤,可以重置計數器,並讓執行緒們重新執行一次。
(2)CyclicBarrier還提供其他有用的方法,比如getNumberWaiting方法可以獲得CyclicBarrier阻塞的執行緒數量。isBroken方法用來知道阻塞的執行緒是否被中斷。比如以下程式碼執行完之後會返回true。
(3)CountDownLatch會阻塞主執行緒,CyclicBarrier不會阻塞主執行緒,只會阻塞子執行緒。
參考文章: