高併發之ReentrantLock、CountDownLatch、CyclicBarrier

等不到的口琴發表於2021-02-16

本系列研究總結高併發下的幾種同步鎖的使用以及之間的區別,分別是:ReentrantLock、CountDownLatch、CyclicBarrier、Phaser、ReadWriteLock、StampedLock、Semaphore、Exchanger、LockSupport。由於部落格園對部落格字數的要求限制,會分為三個篇幅:

高併發之ReentrantLock、CountDownLatch、CyclicBarrier

高併發之Phaser、ReadWriteLock、StampedLock

高併發之Semaphore、Exchanger、LockSupport

ReentrantLock

ReentrantLock重入鎖,是實現Lock介面的一個類,也是在實際程式設計中使用頻率很高的一個鎖,支援重入性,表示能夠對共享資源能夠重複加鎖,即當前執行緒獲取該鎖再次獲取不會被阻塞。在java關鍵字synchronized隱式支援重入性(關於synchronized可以看這篇文章),synchronized通過獲取自增,釋放自減的方式實現重入。與此同時,ReentrantLock還支援公平鎖和非公平鎖兩種方式。那麼,要想完完全全的弄懂ReentrantLock的話,主要也就是ReentrantLock同步語義的學習:1. 重入性的實現原理;2. 公平鎖和非公平鎖。

重入性的實現原理

要想支援重入性,就要解決兩個問題:
1. 線上程獲取鎖的時候,如果已經獲取鎖的執行緒是當前執行緒的話則直接再次獲取成功;
2. 由於鎖會被獲取n次,那麼只有鎖在被釋放同樣的n次之後,該鎖才算是完全釋放成功。

我們知道,同步元件主要是通過重寫AQS的幾個protected方法來表達自己的同步語義。針對第一個問題,我們來看看ReentrantLock是怎樣實現的,以非公平鎖為例,判斷當前執行緒能否獲得鎖為例,核心方法為nonfairTryAcquire:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    //1. 如果該鎖未被任何執行緒佔有,該鎖能被當前執行緒獲取
	if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
	//2.若被佔有,檢查佔有執行緒是否是當前執行緒
    else if (current == getExclusiveOwnerThread()) {
		// 3. 再次獲取,計數加一
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
}

為了支援重入性,在第二步增加了處理邏輯,如果該鎖已經被執行緒所佔有了,會繼續檢查佔有執行緒是否為當前執行緒,如果是的話,同步狀態加1返回true,表示可以再次獲取成功。每次重新獲取都會對同步狀態進行加一的操作,那麼釋放的時候處理思路是怎樣的了?(依然還是以非公平鎖為例)核心方法為tryRelease:

protected final boolean tryRelease(int releases) {
	//1. 同步狀態減1
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
		//2. 只有當同步狀態為0時,鎖成功被釋放,返回true
        free = true;
        setExclusiveOwnerThread(null);
    }
	// 3. 鎖未被完全釋放,返回false
    setState(c);
    return free;
}

需要注意的是,重入鎖的釋放必須得等到同步狀態為0時鎖才算成功釋放,否則鎖仍未釋放。如果鎖被獲取n次,釋放了n-1次,該鎖未完全釋放返回false,只有被釋放n次才算成功釋放,返回true。到現在我們可以理清ReentrantLock重入性的實現了,也就是理解了同步語義的第一條。

公平鎖與非公平鎖

ReentrantLock支援兩種鎖:公平鎖和非公平鎖。何謂公平性,是針對獲取鎖而言的,如果一個鎖是公平的,那麼鎖的獲取順序就應該符合請求上的絕對時間順序,滿足FIFO。ReentrantLock的構造方法無參時是構造非公平鎖,原始碼為:

public ReentrantLock() {
    sync = new NonfairSync();
}

另外還提供了另外一種方式,可傳入一個boolean值,true時為公平鎖,false時為非公平鎖,原始碼為:

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

在上面非公平鎖獲取時(nonfairTryAcquire方法)只是簡單的獲取了一下當前狀態做了一些邏輯處理,並沒有考慮到當前同步佇列中執行緒等待的情況。我們來看看公平鎖的處理邏輯是怎樣的,核心方法為:

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false;
  }
}

這段程式碼的邏輯與nonfairTryAcquire基本上一致,唯一的不同在於增加了hasQueuedPredecessors的邏輯判斷,方法名就可知道該方法用來判斷當前節點在同步佇列中是否有前驅節點的判斷,如果有前驅節點說明有執行緒比當前執行緒更早的請求資源,根據公平性,當前執行緒請求資源失敗。如果當前節點沒有前驅節點的話,再才有做後面的邏輯判斷的必要性。公平鎖每次都是從同步佇列中的第一個節點獲取到鎖,而非公平性鎖則不一定,有可能剛釋放鎖的執行緒能再次獲取到鎖

公平鎖與非公平鎖總結

  1. 公平鎖每次獲取到鎖為同步佇列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的執行緒下次繼續獲取該鎖,則有可能導致其他執行緒永遠無法獲取到鎖,造成“飢餓”現象
  2. 公平鎖為了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低效能開銷。因此,ReentrantLock預設選擇的是非公平鎖,則是為了減少一部分上下文切換,保證了系統更大的吞吐量

CountDownLatch

CountDownLatch,這個詞語是由Count Down、Latch兩部分組成,意思是倒數門閂,為了形象記憶,可以理解為一個門上有很多個門閂,只有所有門閂都開啟,也就是為0時才可以通過這個門。

建立物件時需要在構造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. ");
  }
}

其中的Service類如下:

public class Service {
  private CountDownLatch latch;
//需要將countdownLatch傳遞進來
  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 {
        //放在finally中是為了異常也能執行,不至於主執行緒死鎖!
      latch.countDown();
    }
  }

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

首先宣告瞭一個CountDownLatch物件,並且由主執行緒建立了5個執行緒,分別執行任務,在每個任務中,當前執行緒會休眠2秒。在啟動執行緒之後,主執行緒呼叫了CountDownLatch.await()方法,此時,主執行緒將在此處等待建立的5個執行緒執行完任務之後才繼續往下執行。

如下是執行結果:

main thread await. 
Thread-1 execute task. 
Thread-3 execute task. 
Thread-0 execute task. 
Thread-2 execute task. 
Thread-4 execute task. 
Thread-2 finished task. 
Thread-3 finished task. 
Thread-1 finished task. 
Thread-0 finished task. 
Thread-4 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原始碼

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原始碼

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)方法中。

doAcquireSharedInterruptibly原始碼

如下是該方法的具體實現:

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原始碼

這裡我們看看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()方法也就是我們前面講到的會將離頭結點最近的一個處於等待狀態的節點喚醒的方法。

CyclicBarrier

CyclicBarrier也叫同步屏障,在JDK1.5被引入,可以讓一組執行緒達到一個屏障時被阻塞,直到最後一個執行緒達到屏障時,所以被阻塞的執行緒才能繼續執行。 CyclicBarrier好比一扇門,預設情況下關閉狀態,堵住了執行緒執行的道路,直到所有執行緒都就位,門才開啟,讓所有執行緒一起通過。

使用方法

  1. 預設的構造方法是CyclicBarrier(int parties),其參數列示屏障攔截的執行緒數量,每個執行緒呼叫await方法告訴CyclicBarrier已經到達屏障位置,執行緒被阻塞。
  2. 另外一個構造方法CyclicBarrier(int parties, Runnable barrierAction),其中barrierAction任務會在所有執行緒到達屏障後執行。

應用場景及案例

可以想象飛虎隊(American Volunteer Group,AVG)要執行某項任務,需要等所有人到齊之後才能開始行動

class AVG implements Runnable {

    private CyclicBarrier cyclicBarrier;
    private String name;

    public AVG(CyclicBarrier cyclicBarrier, String name) {
        this.cyclicBarrier = cyclicBarrier;
        this.name = name;
    }

    @Override
    public void run() {
        System.out.println(name + "就位");
        try {
            cyclicBarrier.await();
            Random random =new Random();
            double time = random.nextDouble() + 9;
            System.out.println(name + ": "+ time);
        } catch (Exception e) {
        }
    }
}

就位:

class Ready {
    private CyclicBarrier cyclicBarrier = new CyclicBarrier(8);
    public void start() {
        List<Athlete> avgList = new ArrayList<>();
        athleteList.add(new AVG(cyclicBarrier,"何永道"));
        athleteList.add(new AVG(cyclicBarrier,"約翰·理查德·羅西"));
        athleteList.add(new AVG(cyclicBarrier,"查爾斯·龐德"));
        athleteList.add(new AVG(cyclicBarrier,"羅伯特·尼爾"));
        athleteList.add(new AVG(cyclicBarrier,"羅伯特·桑德爾"));
        athleteList.add(new AVG(cyclicBarrier,"法蘭克·史基爾"));
        athleteList.add(new AVG(cyclicBarrier,"約翰·牛柯克"));
        athleteList.add(new AVG(cyclicBarrier,"大衛·李·希爾"));
        Executor executor = Executors.newFixedThreadPool(8);
        for (AVG avgmember : avgList) {
            executor.execute(avgmember);
        }
    }
}

實現原理

CyclicBarrier實現主要基於ReentrantLock

public class CyclicBarrier {
    private static class Generation {
        boolean broken = false;
    }
    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();
    /** The number of parties */
    private final int parties;
    /* The command to run when tripped */
    private final Runnable barrierCommand;
    /** The current generation */
    private Generation generation = new Generation();
    ...省略後面程式碼
}

其中Generation用來控制屏障的迴圈使用,如果generation.broken為true的話,說明這個屏障已經損壞,當某個執行緒await的時候,直接丟擲異常。

await原始碼實現

private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
    final ReentrantLock lock = this.lock;
	//利用ReentrantLock加鎖               
    lock.lock();
    try {
        final Generation g = generation;
        //如果已經損壞,則丟擲BrokenBarrierException異常
        if (g.broken)
            throw new BrokenBarrierException();
        if (Thread.interrupted()) {
            breakBarrier();
            throw new InterruptedException();
        }
        int index = --count;
        if (index == 0) {  // tripped
            boolean ranAction = false;
            try {
                final Runnable command = barrierCommand;
                if (command != null)
                    command.run();
                ranAction = true;
                nextGeneration();
                return 0;
            } finally {
                if (!ranAction)
                    breakBarrier();
            }
        }

        // loop until tripped, broken, interrupted, or timed out
        for (;;) {
            try {
                if (!timed)
                    trip.await();
                else if (nanos > 0L)
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    //即使沒有被中斷,也將完成等待,因此該中斷被視為“屬於”後續執行。
                    Thread.currentThread().interrupt();
                }
            }
            if (g.broken)
                throw new BrokenBarrierException();
            if (g != generation)
                return index;
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        lock.unlock();
    }
}

解釋

  1. 每當執行緒執行await,內部變數count減1,如果count!= 0,說明有執行緒還未到屏障處,則在鎖條件變數trip上等待。
  2. 當count == 0時,說明所有執行緒都已經到屏障處,執行條件變數的signalAll方法喚醒等待的執行緒。
    其中 nextGeneration方法可以實現屏障的迴圈使用:

重新生成Generation物件

恢復count值

與CountDownLatch的區別

  1. CountDownLatch 允許一個或多個執行緒等待一些特定的操作完成,而這些操作是在其它的執行緒中進行的,也就是說會出現 等待的執行緒被等的執行緒 這樣分明的角色;
  2. CountDownLatch 建構函式中有一個 count 引數,表示有多少個執行緒需要被等待,對這個變數的修改是在其它執行緒中呼叫 countDown 方法,每一個不同的執行緒呼叫一次 countDown 方法就表示有一個被等待的執行緒到達,count 變為 0 時,latch(門閂)就會被開啟,處於等待狀態的那些執行緒接著可以執行;
  3. CountDownLatch 是一次性使用的,也就是說latch門閂只能只用一次,一旦latch門閂被開啟就不能再次關閉,將會一直保持開啟狀態,因此 CountDownLatch 類也沒有為 count 變數提供 set 的方法;

舉例子:CD:司機在等人坐滿了才開車,阻塞主體是外部執行緒。 CB:人在等其他人來了再上車,阻塞主體是多個執行緒。

相關文章