theme: juejin
highlight: an-old-hope
作者:湯圓
個人部落格:javalover.cc
前言
隨著天氣的逐漸變熱,整個人也開始浮躁不安
當然這裡說的不是我,因為我是一個比較安靜的人
講的是隔壁的老大哥,在訓斥年幼的孩子
一通吼叫過後,男人安靜了下來,孩子也哭個不停
簡介
前面我們介紹了 JUC 中的併發容器,它相當於一個同步容器的升級版,很大程度上提高了併發的效能
今天我們來介紹 JUC 中的併發工具,它主要是通過改變自身的狀態來控制執行緒的執行流程;
常見的有如下幾種:
- CountDownLatch:倒數計時器(屬於閉鎖的一種實現),用來阻塞執行緒
- CyclicBarrier:迴圈柵欄,類似倒數計時器,但是比他更高階,也是用來阻塞執行緒(只不過阻塞的方式不同,下面會具體介紹)
- Semaphore:訊號量,用來控制多個執行緒同時訪問指定的資源,比如我們常用的資料庫連線池
下面讓我們開始吧
文章如果有問題,歡迎大家批評指正,在此謝過啦
目錄
- 什麼是併發工具
- 倒計數器 CountDownLatch
- 倒計數器升級版 CyclicBarrier【迴圈柵欄】
- 訊號量 Semaphore
- 區別
正文
1. 什麼是併發工具
併發工具是一組工具類,主要是用來控制執行緒的執行流程,比如阻塞某個執行緒,以等待其他執行緒
2. 倒計數器 CountDownLatch
從字面意思來看,就是一個倒計數門閂(shuan,打了半天zha就是打不出來)
通俗一點來說,就是倒計數,時間一到,門閂就開啟
注:一旦開啟,就不能再合上,即這個 CountDownLatch 的狀態改變是永久不可恢復的(記住這個點,後面會有對比)
比較官方的說法:倒計數器用來阻塞某個(某些)執行緒,以等待其他多個執行緒的任務執行完成(以這個說法為準,上面的可以用來對比參考)
下面列出 CountDownLatch 的幾個方法:
-
構造方法:
public CountDownLatch(int count)
,其中count就是我們所說的內部狀態(當count=0時,表示到達終止狀態,此時會恢復被阻塞的執行緒) -
修改狀態:
public void countDown()
,該方法會遞減上面的count狀態,每執行一次,就-1;(當count=0時,表示到達終止狀態,此時會恢復被阻塞的執行緒) -
等待:
public void await()
,該方法會阻塞當前執行緒,直到count狀態變為0,才會恢復執行(除非中斷,此時會丟擲中斷異常) -
超時等待:
public boolean await(long timeout, TimeUnit unit)
,類似上面的await,只不過可以設定超時時間,等過了超時時間,還在阻塞,則直接恢復 -
獲取狀態值 count:
public long getCount()
,獲取count的數值,以檢視還可以遞減多少次(多用來除錯)
模擬場景的話,這裡先列舉三個,肯定還有其他的
- 第一個就是計數器了,最直接的
- 第二個就是統計任務執行時長
- 第三個就是多人5V5遊戲,等所有人載入完畢,就開始遊戲
下面我們以第三個場景為例,寫個例子:多人遊戲載入畫面
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 構造一個倒計數器,給定一個狀態值10
CountDownLatch latch = new CountDownLatch(10);
System.out.println("準備載入");
// 這裡我們建立10個執行緒,模擬 5V5 遊戲的10個玩家
for (int i = 0; i < 10; i++) {
new Thread(()->{
// 這裡我們給點延時,模擬網路延時
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"載入100%");
// 2. 這裡的countDown就是用來改變倒計數器的內部狀態,每次-1
latch.countDown(); //這裡不會阻塞當前執行緒,執行完後就立馬返回了
}).start();
}
// 3. 這裡阻塞等待狀態的完成,即10變為0;
latch.await();
System.out.println("所有人載入完成,開始遊戲");
}
}
輸出如下:
準備載入
Thread-0載入100%
Thread-1載入100%
Thread-2載入100%
Thread-3載入100%
Thread-4載入100%
Thread-5載入100%
Thread-6載入100%
Thread-8載入100%
Thread-9載入100%
Thread-7載入100%
所有人載入完成,開始遊戲
這裡倒計數器的作用就是阻塞主執行緒,以等待其他10個子執行緒,等到都準備好,再恢復主執行緒
它的特點就是:一次性使用,達到終止狀態後不能再改變
3. 倒計數器升級版 CyclicBarrier【迴圈柵欄】
迴圈柵欄,類似倒計數器,也是用來阻塞執行緒,不過它的重點在於迴圈使用
而倒計數器只能用一次(這屬於他們之間最明顯的一個區別)
PS:猜測之所以叫迴圈柵欄,而不是迴圈門閂,可能是因為柵欄的作用比門閂更強大,所以叫柵欄更適合吧
官方說法:迴圈柵欄一般用來表示多個執行緒之間的相互等待(阻塞)
比如有10個執行緒,都要await等待;那要等到最後一個執行緒await時,柵欄才會開啟
如果有定義柵欄動作,那麼當柵欄開啟時,會執行柵欄動作
柵欄動作就是:柵欄開啟後需執行的動作,通過建構函式的Runnable引數指定,可選引數,下面會介紹
這個屬於迴圈柵欄和倒計數器的第二個區別:
- 迴圈柵欄強調的是多個被阻塞執行緒之間的相互協作關係(等待)
- 而倒計數器強調的是單個(或多個)執行緒被阻塞,來等待其他執行緒的任務執行
下面我們看幾個迴圈柵欄 CyclicBarrier 內部的方法:
- 構造方法:
public CyclicBarrier(int parties, Runnable barrierAction)
,第一個表示需等待(阻塞)的執行緒數,第二個barrierAction就是上面我們說的柵欄動作,即當最後一個執行緒也被阻塞時,就會觸發這個柵欄動作(這個引數可選,如果沒有,則不執行任何動作) - 等待:
public int await()
,阻塞當前執行緒,直到最後一個執行緒被阻塞,才會恢復 - 超時等待:
public boolean await(long timeout, TimeUnit unit)
,類似上面的await,只不過可以設定超時時間 - 獲取當前等待的執行緒數:
public int getNumberWaiting()
,即呼叫了await方法的執行緒數量
場景:
-
大事化小,小事合併:就是將某個大任務拆解為多個小任務,等到小任務都完成,再合併為一個結果
-
多人對戰遊戲團戰
- 上面的倒計數器表示遊戲開始前的準備工作(只需準備一次)
- 而這裡的迴圈柵欄則可以表示遊戲開始後的團戰工作(可團戰多次)
下面看下例子:多人遊戲團戰畫面
public class CyclicBarrierDemo {
public static void main(String[] args) throws InterruptedException {
// 1. 建立一個迴圈柵欄,給定等待執行緒數10和柵欄動作
CyclicBarrier barrier = new CyclicBarrier(10,()->{
// 柵欄動作,等到所有執行緒都await,就會觸發
System.out.println("=== 人齊了,開始團吧");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
System.out.println("=== 準備第一波團戰 ===");
// 2. 建立10個執行緒,模擬10個玩家
for (int i = 0; i < 10; i++) {
new Thread(()->{
try {
// 玩家到場
System.out.println(Thread.currentThread().getName()+"=>第一波團,我準備好了");
// 等待其他人,等人齊就可以團了(人齊了會執行柵欄動作,此時這邊也會恢復執行)
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
}
// 3. 查詢當前等待都執行緒數量,如果不為0,則主執行緒繼續等待
while (barrier.getNumberWaiting()!=0){
Thread.sleep(1000);
}
System.out.println("=== 第一波團戰結束 ===");
// 4. 此時還可以進行第二波第三波團戰。。。(迴圈柵欄可迴圈觸發,倒計數器只能觸發一次)
}
}
輸出如下:
=== 準備第一波團戰 ===
Thread-0=>第一波團,我準備好了
Thread-1=>第一波團,我準備好了
Thread-2=>第一波團,我準備好了
Thread-3=>第一波團,我準備好了
Thread-4=>第一波團,我準備好了
Thread-5=>第一波團,我準備好了
Thread-6=>第一波團,我準備好了
Thread-7=>第一波團,我準備好了
Thread-8=>第一波團,我準備好了
Thread-9=>第一波團,我準備好了
=== 人齊了,開始團吧
=== 第一波團戰結束 ===
4. 訊號量 Semaphore
訊號量主要是用來控制多個執行緒同時訪問指定資源,比如資料庫連線池,超過指定數量,就阻塞等待
下面我們介紹下訊號量的幾個關鍵方法:
- 構造方法:
public Semaphore(int permits, boolean fair)
,第一個引數為許可數,即允許同時訪問的的執行緒數,第二個引數為公平還是非公平模式(預設非公平)- 公平模式,誰先呼叫acquire,誰就先訪問資源,FIFO先進先出
- 非公平模式,允許插隊,如果某個執行緒剛釋放了許可,另一個執行緒就呼叫了acquire,那麼這個執行緒就會插隊訪問資源)
- 獲取許可:
public void acquire()
,如果有許可,則直接返回,並將許可數遞減1;如果沒可用的許可,就阻塞等待,或者被中斷 - 嘗試獲取許可:
public boolean tryAcquire()
,類似上面的acquire,但是不會被阻塞和中斷,因為如果沒有可用的許可,則直接返回false - 釋放許可:
public void release()
,釋放一個許可,並將許可數遞增1 - 獲取可用的許可數量:
public int availablePermits()
,這個方法一般用來除錯
場景:資料庫連線池
訊號量的特點就是可重複使用許可,所以像資料庫連線池這種場景就很適合了
這裡就不舉例子了,就是多個執行緒acquire和release,獲取許可時,如果沒有就阻塞,如果有就立即返回
5 區別
用表格看比較方便點
區別 | CountDownLatch | CyclicBarrier | Semaphore |
---|---|---|---|
可使用次數 | 單次 | 多次(迴圈使用) | 多次(迴圈使用) |
執行緒的阻塞 | 阻塞單個(多個)執行緒,以等待其他執行緒的執行 | 多個執行緒之間的相互阻塞 | 超過許可數,會阻塞 |
場景 | 1. 計數器 2. 統計任務執行時長 3. 多人對戰遊戲的開局等待 |
1. 大事化小,再合併 2. 多人對戰遊戲的團戰 |
1. 資料庫連線池 |
可以看到,倒計數器主要是用來表示單個執行緒等待多個執行緒,而迴圈柵欄主要是用來表示多個執行緒之間的相互等待
總結
- 什麼是併發工具:併發工具是一組工具類,主要是用來控制執行緒的執行流程,比如阻塞某個執行緒,以等待其他執行緒
- 倒計數器 CountDownLatch:用來表示阻塞某個(某些)執行緒,以等待其他多個執行緒的任務執行完成
- 迴圈柵欄 CyclicBarrier:用來表示多個執行緒之間的相互等待協作(阻塞)
- 訊號量 Semaphore:用來表示允許同時訪問指定資源的許可數(執行緒數)
- 區別:
區別 | CountDownLatch | CyclicBarrier | Semaphore |
---|---|---|---|
可使用次數 | 單次 | 多次(迴圈使用) | 多次(迴圈使用) |
執行緒的阻塞 | 阻塞單個(多個)執行緒,以等待其他執行緒的執行 | 多個執行緒之間的相互阻塞 | 超過許可數,會阻塞 |
場景 | 1. 計數器 2. 統計任務執行時長 3. 多人對戰遊戲的開局等待 |
1. 大事化小,再合併 2. 多人對戰遊戲的團戰 |
1. 資料庫連線池 |
參考內容:
- 《Java併發程式設計實戰》
- 《實戰Java高併發》
後記
學習之路,真夠長,共勉之
寫在最後:
願你的意中人亦是中意你之人