併發程式設計之 執行緒協作工具類

莫那·魯道發表於2019-01-24

前言

在併發程式設計的時候,Doug Lea 大師為我們準備了很多的工具,都在 JDK 1.5 版本後的java.util.concurrent 包下,今天樓主就和大家分享一些常用的執行緒協作的工具。

  1. Semaphore 訊號量
  2. CountDownLatch 倒數計時器
  3. CyclicBarrier 迴圈柵欄
  4. Exchanger 交換器

1. Semaphore 訊號量

我們在上一篇文章中說到了3把鎖,無論是 synchronized 還是 重入鎖還是讀寫鎖,一次只能允許一個執行緒進行訪問。當然這是為了保證執行緒的安全。但是,如果我們有的時候想一次讓多個執行緒訪問同一個程式碼呢?並且指定執行緒數量。

在 JDK 1.5 中,doug lea 大師已經為我們寫好了這個工具類,什麼呢?就是 Semephore,訊號量。訊號量為多執行緒協作提供了更為強大的控制方法。從某種程度上說:訊號量是對鎖的擴充套件。訊號量可以指定多個執行緒,同時訪問某一個資源。

該類有2個建構函式:


   public Semaphore(int permits) 

   public Semaphore(int permits, boolean fair) 

複製程式碼

permits 表示的是訊號量的數量,簡單點說就是指定了同時又多少個執行緒可以同時訪問某一個資源。而 fair 參數列示的是否是公平的。那麼訊號量還有哪些方法呢?

下面是阻塞方法,也就是會無限等待的方法:

  1. public void acquire() throws InterruptedException { } //獲取一個許可
  2. public void acquire(int permits) throws InterruptedException { } //獲取permits個許可
  3. public void release() { } //釋放一個許可
  4. public void release(int permits) { } //釋放permits個許可

下面是非阻塞方法:

  1. public boolean tryAcquire() { }; //嘗試獲取一個許可,若獲取成功,則立即返回true,若獲取失敗,則立即返回false
  2. public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException { }; //嘗試獲取一個許可,若在指定的時間內獲取成功,則立即返回true,否則則立即返回false
  3. public boolean tryAcquire(int permits) { }; //嘗試獲取permits個許可,若獲取成功,則立即返回true,若獲取失敗,則立即返回false
  4. public boolean tryAcquire(int permits, long timeout, TimeUnit unit) throws InterruptedException { }; //嘗試獲取permits個許可,若在指定的時間內獲取成功,則立即返回true,否則則立即返回false

如何使用呢?我們還是來個例子吧:

public class SemaphoreTest implements Runnable {

  // 需要指定訊號量的准入數,相當於指定了同時有多少個執行緒可以同時訪問某一個資源
  final Semaphore semaphore = new Semaphore(5);

  @Override
  public void run() {
    try {
      semaphore.acquire();
      // 模擬耗時操作
      Thread.sleep(2000);
      System.out.println(Thread.currentThread().getId() + ":done!");
      semaphore.release();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) {
    ExecutorService exec = Executors.newFixedThreadPool(20);
    final SemaphoreTest test = new SemaphoreTest();
    for (int i = 0; i < 20; i++) {
      exec.execute(test);
    }


  }
}
複製程式碼

在上面的程式碼中,我們指定了可以有5個訊號量的例項,線上程池中被20個執行緒執行,列印的結果都是5個一組,5個一組,說明,的確是每5個執行緒同時訪問該段程式碼。

其實訊號量就是一種限制策略,在 web 伺服器中,訊號量就是一種限流策略,限制多少執行緒執行,和這個模式差不多。

2. CountDownLatch 倒數計時器

從名字上來看,可以翻譯成倒數計時門閂,但我們其實不必管門閂,他其實就是個倒數計時器。門閂的含義是什麼呢?把們鎖起來,不讓裡面的執行緒跑出來,因此,這個工具常用來控制執行緒等待,有點像我們的 join 方法,可以讓某一個執行緒等待知道倒數計時結束,再開始執行。

CountDownLatch 的建構函式:

public CountDownLatch(int count) 其中 int 型別的參數列示當前這個計時器的計數個數。

我們還是直接來個例子吧:

/**
 * 相當於join功能,讓呼叫  await 方法的執行緒等待 countdownlatch 的執行緒執行完畢
 */
public class CountDownLatchTest implements Runnable {

  /**
   * 表示需要10個執行緒完成任務,等待在倒數計時上的執行緒才能繼續執行。
   */
  static final CountDownLatch end = new CountDownLatch(10);
  static final CountDownLatchTest test = new CountDownLatchTest();

  @Override
  public void run() {
    try {
      // 模擬檢查任務
      Thread.sleep(new Random().nextInt(10) * 1000);
      System.out.println("check complete");
      end.countDown();
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }

  public static void main(String[] args) throws InterruptedException {
    ExecutorService exec = Executors.newFixedThreadPool(10);
    for (int i = 0; i < 20; i++) {
      exec.submit(test);
    }

    end.await();

    System.out.println("Fire");

    exec.shutdown();
  }
}

複製程式碼

我們模擬了火箭發射的場景,火箭發射前,都需要做一些檢查任務,等到所有的檢查任務完成了才能反射。那我們這裡怎麼實現的呢?開啟20個執行緒執行 檢查任務,注意,任務中,呼叫了 CountdownLatch 的 countDown 方法,就是倒數計時方法,每個執行緒執行到這裡,都會讓該倒數計時減一,直到為0.

而再完美的main 執行緒中,有一行則是 await 方法,該方法讓 main 執行緒等待 countdown 的執行緒都執行完畢。當20個執行緒全都成功呼叫了 run 方法,並且呼叫了 countdown 的 countdown 方法,countdown 此時為0,main 執行緒就可以執行 “發射” 了。所以該方法的使用就是讓 呼叫 await 方法的執行緒等待 呼叫 countdown 方法的執行緒,和 join 很相似,join 是呼叫方等待被呼叫方。

3. CyclicBarrier 迴圈柵欄

迴圈柵欄也是控制多執行緒併發的工具。和 CountDownLatch 非常類似,但是比 CountdDownLatch 複雜,強大。

看名字也很奇怪,CyclicBarrier,迴圈柵欄。柵欄是用來攔住別人不要進來的,在我們這裡,其實就是攔住執行緒不要進來,而且可以迴圈使用。

加入有一個場景:司令下達命令,要求10個士兵一起去完成一項任務,這是,就會要求10個士兵先集合報導,接著,再一起去執行任務,當10個士兵的任務都完成了,司令對外宣佈,任務完成。

我們是不是想到了使用 coundownLatch 來完成,注意,我們這裡需要兩次計數,而 countdown 是無法實現的,這就是迴圈柵欄比倒數計時器強大的地方—–可以迴圈。

如何使用呢?

CyclicBarrier 迴圈柵欄提供了2 個構造方法:

public CyclicBarrier(int parties) int 型別表示計數的數量

public CyclicBarrier(int parties, Runnable barrierAction) Runnable 表示每次計數結束需要執行的任務(執行一次)

我們就各個司令士兵的例子寫一段程式碼:

public class CyclicBarrierDemo {

  static class Soldier implements Runnable {

    // 軍人
    String soldier;
    // 迴圈柵欄
    final CyclicBarrier cyclic;

    public Soldier(CyclicBarrier cyclic, String soldier) {
      this.cyclic = cyclic;
      this.soldier = soldier;
    }

    @Override
    public void run() {
      // 等待所有士兵到齊
      try {
        System.out.println("準備");
        cyclic.await();// 到了 10 才開始走,否則執行緒等待

        doWork();
        // 等待所有士兵完成工作
        cyclic.await();
      } catch (InterruptedException | BrokenBarrierException e) {
        e.printStackTrace();
      }

    }

    private void doWork() {
      try {
        Thread.sleep(Math.abs(new Random().nextInt() % 10000));
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println(soldier + ": 任務完成");
    }
  }

  /**
   * 當計數器一次計數完成後,系統會派一個執行緒執行的這個執行緒的run方法。
   */
  static class BarrierRun implements Runnable {

    boolean flag;
    int N;

    public BarrierRun(boolean flag, int n) {
      this.flag = flag;
      N = n;
    }

    @Override
    public void run() {
      if (flag) {
        System.out.println("司令:【士兵 " + N + "個, 任務完成!】");
      } else {
        System.out.println("司令:【士兵 " + N + "個, 集合完畢!】");
        flag = true;
      }
    }
  }


  public static void main(String[] args) {
    final int n = 10;

    Thread[] allSoldier = new Thread[n];
    boolean flag = false;
    // parties 表示計數總數,也就是參與的執行緒總數, barrierAction 就是當計數器一次計數完成後,系統會執行的動作
    CyclicBarrier cyclic = new CyclicBarrier(n, new BarrierRun(false, n));

    // 設定屏障點,主要是為了執行這個方法
    System.out.println("集合隊伍");
    for (int i = 0; i < n; i++) {
      System.out.println("士兵" + i + "報導");
      allSoldier[i] = new Thread(new Soldier(cyclic, "士兵" + i));
      allSoldier[i].start();
//      if (i== 5){ // 會導致所有的執行緒全部停止 BrokenBarrierException * 9 + InterruptedException * 1
//        allSoldier[i].interrupt();
//
//      }
    }
  }

}

複製程式碼

程式碼不少,我們場景10個士兵執行緒,每個士兵執行緒都含有同一個迴圈柵欄,再士兵呼叫run方法的時候,需要呼叫迴圈柵欄的 await 方法,此時,執行緒就開始等待,直到柵欄的數字變成了0,因為我們在建立的時候設定的是10,因此,需要10個執行緒觸發此方法,當10個執行緒全部都觸發了該方法,也就是計數器歸零了,注意,此時迴圈柵欄會隨機呼叫一個執行緒執行柵欄處發生的行動。就是我們的 BarrierRun 任務。在執行完該任務後,所有的士兵執行 doWork 方法,下面又開始 執行柵欄的 await 方法,所有的士兵又開始等待,等待所有的士兵都執行完畢,可以看到,該柵欄被迴圈使用了,而 countdown 是做不到的。等到所有的士兵都是呼叫了 await 方法,迴圈柵欄再次隨機抽取一個執行緒呼叫 BarrierRun 的 run 方法。最後完成了所有的任務。

可以說,CyclicBarrier 和 CountDownLatch 還有 Samephore 類都是協作多個執行緒同時工作的工具。什麼時候使用什麼工具,各位可以自己思考。

只需要記住:countDown 和 CyclicBarrier 很相似,但不能迴圈,而 Samephore 可以控制每次又多少個執行緒進入某個程式碼塊。相當於多執行緒的鎖。具體使用場景自己看。

4. Exchanger 交換器

Exchanger 是要給交換器,也是用於執行緒間協作的工具類。什麼用處呢?假如現在有一個需求,需要你將兩個執行緒的資料進行狡猾,你該怎麼做?

我猜測大家肯定使用類似 wait notify 之類的方法進行執行緒之間的通訊,或者使用訊息機制。但 Doug Lea 為我們提供另外一種選擇:交換器,直接交換兩個執行緒的資料。6不6?

兩個執行緒可以通過 exchange 方法交換資料,如果第一個執行緒先執行 exchange 方法,他會一直等待第二個執行緒也執行 exchange 方法, 當兩個執行緒都到達同步點時,這兩個執行緒就可以交換資料,將本執行緒生產出來的資料傳遞給對方。

寫個例子大家看看:

public class ExchangerDemo {

  static final Exchanger<String> exgr = new Exchanger<>();

  static ExecutorService threadPool = Executors.newFixedThreadPool(2);

  public static void main(String[] args) {
    threadPool.execute(new Runnable() {
      @Override
      public void run() {
        try {
          String a = "銀行流水A";
          String b = exgr.exchange(a);
          System.err.println("A 和 B 資料是否一致: " + a.equals(b) + ", A 錄入的是:" + a + ", B 錄入的是:" + b);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    threadPool.execute(new Runnable() {
      @Override
      public void run() {
        try {
          String b = "銀行流水B";
          String a = exgr.exchange(b);
          System.out.println("A 和 B 資料是否一致: " + a.equals(b) + ", A 錄入的是:" + a + ", B 錄入的是:" + b);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    threadPool.shutdown();
  }
複製程式碼

執行結果

執行結果

可以看到,兩個執行緒都得到了對方的資料,可以說非常的牛逼。如果兩個執行緒有一個沒有執行 exchange 方法,另一個則會一直等待,如果擔心 exchanger 時間過長,可以設定過長時間 exchange(V x, long timeout, TimeUnit unit)。

總結

好了,我們今天介紹了 java.util.concurrent 包下的4個多執行緒協作工具類,讓我們在今後併發程式設計中可以有更順手的工具,有些業務場景完全可以使用這些現成的工具。比如 Samephore,CountDownLatch, CyclicBarrier,Exchanger,每個工具都有自己的應用場景。

好了,今天的併發工具使用就介紹到這裡。

good luck !!!!

相關文章