Java併發工具篇

湯圓學Java發表於2021-05-12

theme: juejin
highlight: an-old-hope

作者:湯圓

個人部落格:javalover.cc

前言

隨著天氣的逐漸變熱,整個人也開始浮躁不安

當然這裡說的不是我,因為我是一個比較安靜的人

講的是隔壁的老大哥,在訓斥年幼的孩子

一通吼叫過後,男人安靜了下來,孩子也哭個不停

簡介

前面我們介紹了 JUC 中的併發容器,它相當於一個同步容器的升級版,很大程度上提高了併發的效能

今天我們來介紹 JUC 中的併發工具,它主要是通過改變自身的狀態來控制執行緒的執行流程

常見的有如下幾種:

  • CountDownLatch:倒數計時器(屬於閉鎖的一種實現),用來阻塞執行緒
  • CyclicBarrier:迴圈柵欄,類似倒數計時器,但是比他更高階,也是用來阻塞執行緒(只不過阻塞的方式不同,下面會具體介紹)
  • Semaphore:訊號量,用來控制多個執行緒同時訪問指定的資源,比如我們常用的資料庫連線池

下面讓我們開始吧

文章如果有問題,歡迎大家批評指正,在此謝過啦

目錄

  1. 什麼是併發工具
  2. 倒計數器 CountDownLatch
  3. 倒計數器升級版 CyclicBarrier【迴圈柵欄】
  4. 訊號量 Semaphore
  5. 區別

正文

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,只不過可以設定超時時間,等過了超時時間,還在阻塞,則直接恢復

  • 獲取狀態值 countpublic 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. 資料庫連線池

可以看到,倒計數器主要是用來表示單個執行緒等待多個執行緒,而迴圈柵欄主要是用來表示多個執行緒之間的相互等待

總結

  1. 什麼是併發工具:併發工具是一組工具類,主要是用來控制執行緒的執行流程,比如阻塞某個執行緒,以等待其他執行緒
  2. 倒計數器 CountDownLatch:用來表示阻塞某個(某些)執行緒,以等待其他多個執行緒的任務執行完成
  3. 迴圈柵欄 CyclicBarrier:用來表示多個執行緒之間的相互等待協作(阻塞)
  4. 訊號量 Semaphore:用來表示允許同時訪問指定資源的許可數(執行緒數)
  5. 區別:
區別 CountDownLatch CyclicBarrier Semaphore
可使用次數 單次 多次(迴圈使用) 多次(迴圈使用)
執行緒的阻塞 阻塞單個(多個)執行緒,以等待其他執行緒的執行 多個執行緒之間的相互阻塞 超過許可數,會阻塞
場景 1. 計數器
2. 統計任務執行時長
3. 多人對戰遊戲的開局等待
1. 大事化小,再合併
2. 多人對戰遊戲的團戰
1. 資料庫連線池

參考內容:

  • 《Java併發程式設計實戰》
  • 《實戰Java高併發》

後記

學習之路,真夠長,共勉之

寫在最後:

願你的意中人亦是中意你之人

相關文章