CountDownLatch詳解

 CountDownLatch中count down是倒數的意思,latch則是門閂的含義。整體含義可以理解爲倒數的門栓,似乎有一點“三二一,芝麻開門”的感覺。CountDownLatch的作用也是如此,在構造CountDownLatch的時候需要傳入一個整數n,在這個整數“倒數”到0之前,主線程需要等待在門口,而這個“倒數”過程則是由各個執行線程驅動的,每個線程執行完一個任務“倒數”一次。總結來說,CountDownLatch的作用就是等待其他的線程都執行完任務,必要時可以對各個任務的執行結果進行彙總,然後主線程才繼續往下執行。

        CountDownLatch主要有兩個方法:countDown()和await()。countDown()方法用於使計數器減一,其一般是執行任務的線程調用,await()方法則使調用該方法的線程處於等待狀態,其一般是主線程調用。這裏需要注意的是,countDown()方法並沒有規定一個線程只能調用一次,當同一個線程調用多次countDown()方法時,每次都會使計數器減一;另外,await()方法也並沒有規定只能有一個線程執行該方法,如果多個線程同時執行await()方法,那麼這幾個線程都將處於等待狀態,並且以共享模式享有同一個鎖。如下是其使用示例:

public class CountDownLatchExample {
  public static void main(String[] args) throws InterruptedException {
    CountDownLatch latch = new CountDownLatch(5);
    Service service = new Service(latch);
    Runnable task = () -> service.exec();

    for (int i = 0; i < 5; i++) {
      Thread thread = new Thread(task);
      thread.start();
    }

    System.out.println("main thread await. ");
    latch.await();
    System.out.println("main thread finishes await. ");
  }
}

public class Service {
  private CountDownLatch latch;

  public Service(CountDownLatch latch) {
    this.latch = latch;
  }

  public void exec() {
    try {
      System.out.println(Thread.currentThread().getName() + " execute task. ");
      sleep(2);
      System.out.println(Thread.currentThread().getName() + " finished task. ");
    } finally {
      latch.countDown();
    }
  }

  private void sleep(int seconds) {
    try {
      TimeUnit.SECONDS.sleep(seconds);
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
  }
}

        在上面的例子中,首先聲明瞭一個CountDownLatch對象,並且由主線程創建了5個線程,分別執行任務,在每個任務中,當前線程會休眠2秒。在啓動線程之後,主線程調用了CountDownLatch.await()方法,此時,主線程將在此處等待創建的5個線程執行完任務之後才繼續往下執行。如下是執行結果:

Thread-0 execute task. 
Thread-1 execute task. 
Thread-2 execute task. 
Thread-3 execute task. 
Thread-4 execute task. 
main thread await. 
Thread-0 finished task. 
Thread-4 finished task. 
Thread-3 finished task. 
Thread-1 finished task. 
Thread-2 finished task. 
main thread finishes await. 

        從輸出結果可以看出,主線程先啓動了五個線程,然後主線程進入等待狀態,當這五個線程都執行完任務之後主線程才結束了等待。上述代碼中需要注意的是,在執行任務的線程中,使用了try...finally結構,該結構可以保證創建的線程發生異常時CountDownLatch.countDown()方法也會執行,也就保證了主線程不會一直處於等待狀態。

        CountDownLatch非常適合於對任務進行拆分,使其並行執行,比如某個任務執行2s,其對數據的請求可以分爲五個部分,那麼就可以將這個任務拆分爲5個子任務,分別交由五個線程執行,執行完成之後再由主線程進行彙總,此時,總的執行時間將決定於執行最慢的任務,平均來看,還是大大減少了總的執行時間。

        另外一種比較合適使用CountDownLatch的地方是使用某些外部鏈接請求數據的時候,比如圖片。在本人所從事的項目中就有類似的情況,因爲我們使用的圖片服務只提供了獲取單個圖片的功能,而每次獲取圖片的時間不等,一般都需要1.5s~2s。當我們需要批量獲取圖片的時候,比如列表頁需要展示一系列的圖片,如果使用單個線程順序獲取,那麼等待時間將會極長,此時我們就可以使用CountDownLatch對獲取圖片的操作進行拆分,並行的獲取圖片,這樣也就縮短了總的獲取時間。

        CountDownLatch是基於AbstractQueuedSynchronizer實現的,在AbstractQueuedSynchronizer中維護了一個volatile類型的整數state,volatile可以保證多線程環境下該變量的修改對每個線程都可見,並且由於該屬性爲整型,因而對該變量的修改也是原子的。創建一個CountDownLatch對象時,所傳入的整數n就會賦值給state屬性,當countDown()方法調用時,該線程就會嘗試對state減一,而調用await()方法時,當前線程就會判斷state屬性是否爲0,如果爲0,則繼續往下執行,如果不爲0,則使當前線程進入等待狀態,直到某個線程將state屬性置爲0,其就會喚醒在await()方法中等待的線程。如下是countDown()方法的源代碼:

public void countDown() {
  sync.releaseShared(1);
}

        這裏sync也即一個繼承了AbstractQueuedSynchronizer的類實例,該類是CountDownLatch的一個內部類,其聲明如下:

private static final class Sync extends AbstractQueuedSynchronizer {
  private static final long serialVersionUID = 4982264981922014374L;

  Sync(int count) {
    setState(count);
  }

  int getCount() {
    return getState();
  }

  protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
  }

  protected boolean tryReleaseShared(int releases) {
    for (;;) {
      int c = getState();   // 獲取當前state屬性的值
      if (c == 0)   // 如果state爲0,則說明當前計數器已經計數完成,直接返回
        return false;
      int nextc = c-1;
      if (compareAndSetState(c, nextc)) // 使用CAS算法對state進行設置
        return nextc == 0;  // 設置成功後返回當前是否爲最後一個設置state的線程
    }
  }
}

        這裏tryReleaseShared(int)方法即對state屬性進行減一操作的代碼。可以看到,CAS也即compare and set的縮寫,jvm會保證該方法的原子性,其會比較state是否爲c,如果是則將其設置爲nextc(自減1),如果state不爲c,則說明有另外的線程在getState()方法和compareAndSetState()方法調用之間對state進行了設置,當前線程也就沒有成功設置state屬性的值,其會進入下一次循環中,如此往復,直至其成功設置state屬性的值,即countDown()方法調用成功。

        在countDown()方法中調用的sync.releaseShared(1)調用時實際還是調用的tryReleaseShared(int)方法,如下是releaseShared(int)方法的實現:

public final boolean releaseShared(int arg) {
  if (tryReleaseShared(arg)) {
    doReleaseShared();
    return true;
  }
  return false;
}

        可以看到,在執行sync.releaseShared(1)方法時,其在調用tryReleaseShared(int)方法時會在無限for循環中設置state屬性的值,設置成功之後其會根據設置的返回值(此時state已經自減了一),即當前線程是否爲將state屬性設置爲0的線程,來判斷是否執行if塊中的代碼。doReleaseShared()方法主要作用是喚醒調用了await()方法的線程。需要注意的是,如果有多個線程調用了await()方法,這些線程都是以共享的方式等待在await()方法處的,試想,如果以獨佔的方式等待,那麼當計數器減少至零時,就只有一個線程會被喚醒執行await()之後的代碼,這顯然不符合邏輯。如下是doReleaseShared()方法的實現代碼:

private void doReleaseShared() {
  for (;;) {
    Node h = head;  // 記錄等待隊列中的頭結點的線程
    if (h != null && h != tail) {   // 頭結點不爲空,且頭結點不等於尾節點
      int ws = h.waitStatus;
      if (ws == Node.SIGNAL) {  // SIGNAL狀態表示當前節點正在等待被喚醒
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))    // 清除當前節點的等待狀態
          continue;
        unparkSuccessor(h); // 喚醒當前節點的下一個節點
      } else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;
    }
    if (h == head)  // 如果h還是指向頭結點,說明前面這段代碼執行過程中沒有其他線程對頭結點進行過處理
      break;
  }
}

        在doReleaseShared()方法中(始終注意當前方法是最後一個執行countDown()方法的線程執行的),首先判斷頭結點不爲空,且不爲尾節點,說明等待隊列中有等待喚醒的線程,這裏需要說明的是,在等待隊列中,頭節點中並沒有保存正在等待的線程,其只是一個空的Node對象,真正等待的線程是從頭節點的下一個節點開始存放的,因而會有對頭結點是否等於尾節點的判斷。在判斷等待隊列中有正在等待的線程之後,其會清除頭結點的狀態信息,並且調用unparkSuccessor(Node)方法喚醒頭結點的下一個節點,使其繼續往下執行。如下是unparkSuccessor(Node)方法的具體實現:

private void unparkSuccessor(Node node) {
  int ws = node.waitStatus;
  if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);   // 清除當前節點的等待狀態

  Node s = node.next;
  if (s == null || s.waitStatus > 0) {  // s的等待狀態大於0說明該節點中的線程已經被外部取消等待了
    s = null;
    // 從隊列尾部往前遍歷,找到最後一個處於等待狀態的節點,用s記錄下來
    for (Node t = tail; t != null && t != node; t = t.prev)
      if (t.waitStatus <= 0)
        s = t;
  }
  if (s != null)
    LockSupport.unpark(s.thread);   // 喚醒離傳入節點最近的處於等待狀態的節點線程
}

        可以看到,unparkSuccessor(Node)方法的作用是喚醒離傳入節點最近的一個處於等待狀態的線程,使其繼續往下執行。前面我們講到過,等待隊列中的線程可能有多個,而調用countDown()方法的線程只喚醒了一個處於等待狀態的線程,這裏剩下的等待線程是如何被喚醒的呢?其實這些線程是被當前喚醒的線程喚醒的。具體的我們可以看看await()方法的具體執行過程。如下是await()方法的代碼:

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

        await()方法實際還是調用了Sync對象的方法acquireSharedInterruptibly(int)方法,如下是該方法的具體實現:

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
  if (Thread.interrupted())
    throw new InterruptedException();
  if (tryAcquireShared(arg) < 0)
    doAcquireSharedInterruptibly(arg);
}

        可以看到acquireSharedInterruptibly(int)方法判斷當前線程是否需要以共享狀態獲取執行權限,這裏tryAcquireShared(int)方法是AbstractQueuedSynchronizer中的一個模板方法,其具體實現在前面的Sync類中,可以看到,其主要是判斷state是否爲零,如果爲零則返回1,表示當前線程不需要進行權限獲取,可直接執行後續代碼,返回-1則表示當前線程需要進行共享權限。具體的獲取執行權限的代碼在doAcquireSharedInterruptibly(int)方法中,如下是該方法的具體實現:

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
  final Node node = addWaiter(Node.SHARED); // 使用當前線程創建一個共享模式的節點
  boolean failed = true;
  try {
    for (;;) {
      final Node p = node.predecessor();    // 獲取當前節點的前一個節點
      if (p == head) {  // 判斷前一個節點是否爲頭結點
        int r = tryAcquireShared(arg);  // 查看當前線程是否獲取到了執行權限
        if (r >= 0) {   // 大於0表示獲取了執行權限
          setHeadAndPropagate(node, r); // 將當前節點設置爲頭結點,並且喚醒後面處於等待狀態的節點
          p.next = null; // help GC
          failed = false;
          return;
        }
      }
      
      // 走到這一步說明沒有獲取到執行權限,就使當前線程進入“擱置”狀態
      if (shouldParkAfterFailedAcquire(p, node) &&
          parkAndCheckInterrupt())
        throw new InterruptedException();
    }
  } finally {
    if (failed)
      cancelAcquire(node);
  }
}

        在doAcquireSharedInterruptibly(int)方法中,首先使用當前線程創建一個共享模式的節點。然後在一個for循環中判斷當前線程是否獲取到執行權限,如果有(r >= 0判斷)則將當前節點設置爲頭節點,並且喚醒後續處於共享模式的節點;如果沒有,則對調用shouldParkAfterFailedAcquire(Node, Node)和parkAndCheckInterrupt()方法使當前線程處於“擱置”狀態,該“擱置”狀態是由操作系統進行的,這樣可以避免該線程無限循環而獲取不到執行權限,造成資源浪費,這裏也就是線程處於等待狀態的位置,也就是說當線程被阻塞的時候就是阻塞在這個位置。當有多個線程調用await()方法而進入等待狀態時,這幾個線程都將等待在此處。這裏回過頭來看前面將的countDown()方法,其會喚醒處於等待隊列中離頭節點最近的一個處於等待狀態的線程,也就是說該線程被喚醒之後會繼續從這個位置開始往下執行,此時執行到tryAcquireShared(int)方法時,發現r大於0(因爲state已經被置爲0了),該線程就會調用setHeadAndPropagate(Node, int)方法,並且退出當前循環,也就開始執行awat()方法之後的代碼。這裏我們看看setHeadAndPropagate(Node, int)方法的具體實現:

private void setHeadAndPropagate(Node node, int propagate) {
  Node h = head;
  setHead(node);    // 將當前節點設置爲頭節點
  // 檢查喚醒過程是否需要往下傳遞,並且檢查頭結點的等待狀態
  if (propagate > 0 || h == null || h.waitStatus < 0 ||
      (h = head) == null || h.waitStatus < 0) {
    Node s = node.next;
    if (s == null || s.isShared())  // 如果下一個節點是嘗試以共享狀態獲取獲取執行權限的節點,則將其喚醒
      doReleaseShared();
  }
}

        setHeadAndPropagate(Node, int)方法主要作用是設置當前節點爲頭結點,並且將喚醒工作往下傳遞,在傳遞的過程中,其會判斷被傳遞的節點是否是以共享模式嘗試獲取執行權限的,如果不是,則傳遞到該節點處爲止(一般情況下,等待隊列中都只會都是處於共享模式或者處於獨佔模式的節點)。也就是說,頭結點會依次喚醒後續處於共享狀態的節點,這也就是共享鎖與獨佔鎖的實現方式。這裏doReleaseShared()方法也就是我們前面講到的會將離頭結點最近的一個處於等待狀態的節點喚醒的方法。




from:https://www.jianshu.com/p/128476015902

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章