19.詳解AQS家族的成員:CountDownLatch

2023-06-13 12:01:09

關注王有志,一個分享硬核Java技術的互金摸魚俠
歡迎你加入Java人的提桶跑路群共同富裕的Java人

今天我們來聊一聊AQS家族中的另一個重要成員CountDownLatch。關於CountDownLatch的面試題並不多,除了問「是什麼」和「如何實現的「外,CountDownLatch還會和CyclicBarrier進行對比:

  • 什麼是CountDownLatch?它是如何實現的?

  • CountDownLatch和CyclicBarrier有什麼區別?

按照慣例,我們依舊是按照「是什麼」,「怎麼用」和「如何實現的」這3步來分析CountDownLatch,至於與CyclicBarrier的差異,下一篇我們再詳細分析。

Tips:今天的「是什麼」和「怎麼用」合併了。

CountDownLatch的使用

不知道你有沒有參加過那種感動老闆,並伴以「提升」組織凝聚力為主旨的公司團建?通常行政會組織一場越野徒步活動,規定每個人都到達終點後才能吃飯,美名其曰「不拋棄不放棄的團隊精神」。而老闆會早早的在終點拿著花名冊等待,當員工到達終點後,在花名冊上劃掉自己的名字,當最後一名員工到達終點後,還要敲響鑼鼓,告知老闆可以開始下一輪的折磨了。

那麼這樣一場越野徒步活動就可以用CountDownLatch來進行簡單的程式碼描述:

CountDownLatch countDownLatch = new CountDownLatch(10);

// 10個人進行越野徒步
for (int i = 0; i < 10; i++) {
  int finalI = i;
  new Thread(() -> {
    try {
      // 每個人比前一個選手晚1秒
      TimeUnit.SECONDS.sleep((finalI + 1));
    } catch (InterruptedException e) {
      throw new RuntimeException(e);
    }
    System.out.println("選手[" + finalI + "]到達終點!!!");
    countDownLatch.countDown();
  }).start();
}

// 老闆在目的地吃瓜,等待每個選手到達
countDownLatch.await();
// 開飯啦!
System.out.println("老闆說:所有人都到齊了,午飯是每人一個吐司!!!");

看到這裡,參加過此類團建活動的小夥伴是不是血壓有些高了?但是你先別高,因為在這樣一場血壓飆升的團建中,我們已經不知不覺的掌握了CountDownLatch的用法了。

我們先試著從名字來理解CountDownLatch,CountDownLatch是一個組合詞,CountDown譯為「倒計時」,Latch譯為「門閂」,結合起來就是倒計時結束後開啟門閂(進行後續的動作)。再來看Doug Lea是如何解釋CountDownLatch的作用的:

A synchronization aid that allows one or more threads to wait until a set of operations being performed in other threads completes.

CountDownLatch是一個同步輔助工具,它允許一個或多個執行緒等待其他執行緒完成操作(進而執行後續操作)。

需要注意的是,CountDownLatch允許一個或多個執行緒進入等待,我們只需要在不同的執行緒中呼叫CountDownLatch.await就可以實現多個執行緒的等待。

CountDownLatch的原理

先來看作為AQS家族的成員,CountDownLatch是如何與AQS產生聯絡的:

很熟悉的結構,與ReentrantLock和Semaphore一樣,都是內部的同步器類Sync繼承了AQS,但不同的是CountDownLatch中的Sync不再是抽象類。

既然是繼承自AQS,並且內部有計數器(倒計數也是計數)的使用,那麼我們就再次搬出《AQS的今生,構建出JUC的基礎》中那段關於同步狀態作為計數器特性的說明:

AQS中,state不僅用作表示同步狀態,也是某些同步器實現的計數器,如:Semaphore中允許通過的執行緒數量,ReentrantLock中可重入特性的實現,都依賴於state作為計數器的特性。

雖然沒有舉CountDownLatch的例子,但我知道在經過Semaphore的分析後你一定能夠猜到CountDownLatch是如何使用同步狀態作為計數器特性的。接下來我們就一起來看一下同步狀態在CountDownLatch中的應用。

構造方法

通過AQS家族成員的類圖可以看到,CountDownLatch中的同步器Sync並沒有公平與非公平的區別,因此構造器只需要提供設定計數的能力即可:

public class CountDownLatch {
  public CountDownLatch(int count) {
    if (count < 0){
      throw new IllegalArgumentException("count < 0");
    }
    this.sync = new Sync(count);
  }
  
  private static final class Sync extends AbstractQueuedSynchronizer {
    Sync(int count) {
      setState(count);
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  protected final void setState(int newState) {
    state = newState;
  }
}

不出所料,CountDownLatch的計數依舊是迴歸到了AQS的state上。

countDown方法

回到徒步活動中,員工到達終點後,需要在花名冊上劃掉自己的名字,最後一名到達後還要敲響鑼鼓。在程式碼實現中,我們使用了CountDownLatch.countDown表示員工到達的狀態,並執行相應的動作:

public class CountDownLatch {
  public void countDown() {
    sync.releaseShared(1);
  }
  
  private static final class Sync extends AbstractQueuedSynchronizer {
    protected boolean tryReleaseShared(int releases) {
      for (;;) {
        // 獲取同步狀態
        int c = getState();
        // 同步狀態為0,返回失敗
        if (c == 0){
          return false;
        }
        // 計數減1,並通過CAS更新
        int nextc = c - 1;
        if (compareAndSetState(c, nextc)) {
          // 計數器為0時返回true
          return nextc == 0;
        }
      }
    }
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
      doReleaseShared();
      return true;
    }
    return false;
  }
}

回憶下《詳解AQS家族的成員:Semaphore》中Semaphore#release方法的實現,是不是覺得似曾相識?同樣是執行Sync#tryReleaseShared方法,並在成功後呼叫AQS的doReleaseShared方法。區別是Semaphore#tryReleaseShared的實現是計數加1,而CountDownLatch#tryReleaseShared實現是計數減1。

我們注意另一個問題,CountDownLatch的Sync#tryReleaseShared方法只有在計數器減為0時才會返回true,此時能進入AQS的doReleaseShared方法,否則都只是執行了計數器減一的操作。

此外,我們也知道AQS的doReleaseShared方法起到了喚醒AQS等待佇列中節點的作用,也就是說只有在計數器減為0時,CountDownLatch才會執行一次喚醒工作

Tips:AQS的doReleaseShared已經在《詳解AQS家族的成員:Semaphore》中分析過了,就不再贅述了~~

await方法

我們知道老闆一早就乘車到達了終點等待,那麼老闆是如何判斷自己要等待呢?老闆提前抵達終點後,拿出花名冊統計到達人數,當發現還有人沒有到達終點時,他就準備打個盹,睡一覺。

我們使用了CountDownLatch.await表示老闆進入等待狀態:

public class CountDownLatch {
  public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
  }
}

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
  public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
    if (Thread.interrupted()) {
      throw new InterruptedException();
    }
    if (tryAcquireShared(arg) < 0) {
      doAcquireSharedInterruptibly(arg);
    }
  }
}

是不是還是很眼熟?與Semaphore一樣使用了AQS的acquireSharedInterruptibly方法,那我們重點關注CountDownLatch的Sync#tryAcquireShared方法:

public class CountDownLatch {
  private static final class Sync extends AbstractQueuedSynchronizer {
    protected int tryAcquireShared(int acquires) {
      // 同步狀態為0返回1,不為0返回-1
      return (getState() == 0) ? 1 : -1;
    }
  }
}

該方法對同步狀態做出了判斷,結合AQS的acquireSharedInterruptibly方法我們可以得到以下結論:

  • 當同步狀態等於0時,tryAcquireShared返回1,不執行doAcquireSharedInterruptibly,即執行了足夠次數的countDownLatch#countDown,無需進入等待佇列;

  • 當同步狀態不等於0時,tryAcquireShared返回-1,執行doAcquireSharedInterruptibly,即尚未執行足夠次數的countDownLatch#countDown,需要進入等待佇列。

簡單來說就是在呼叫CountDownLatch#await方法時計數器不為0構建等待佇列,為0就什麼也不執行。

Tips:AQS的doAcquireSharedInterruptibly已經在《詳解AQS家族的成員:Semaphore》中分析過了,就不再贅述了~~

結語

關於CountDownLatch的內容到這裡就結束了,內容並不多。當我們不熟悉AQS的時候,不認識CountDownLatch的時候,會覺得CountDownLatch是一種「挺高階」的工具,但當我們深入其中時就會發現,「高階」的技術其實並不難學。

好了,如果本文對你有幫助的話,還希望你不要吝嗇點贊。最後歡迎大家關注分享硬核技術的金融摸魚俠王有志,以及關注我的專欄《Java面試都問啥?》,我們下次再見!