CountDownLatch CyclicBarrier 介紹 CountDownLatch CyclicBarrier Semaphore 二者比較

CountDownLatch

作用

讓一些阻塞等待的線程,被一定數量的線程完成後完成喚醒.

但需要注意的是CountDownLatch只能使用一次,相當於說一旦用完了,就不會再阻塞了

原理步驟:

  1. 計數器由構造函數初始化,並用它來初始化AQS的states的值
  2. 當線程調用await方法時會檢查state的值是否爲0
  • 如果是的話
    • 表示資源池的資源已經被用光了,則不會被阻塞
  • 如果不是的話
    • 將該線程節點加入等待隊列
    • 將自身進行阻塞
  1. 當其他線程調用countDown方法時
    1. 將計數器減一
    2. 判斷計數器是否爲0
      1. 爲0時喚醒隊列中的第一個節點
        1. 由於CountDownLatch使用了共享模式所以第一個節點被喚醒之後,又會觸發下一個節點的釋放(自旋),並且依此類推,使得所有節點都能被喚醒

方法原理介紹

await

// 嘗試共享中斷
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 1.  將該節點加入到等待隊列
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
      // 自旋
        for (;;) {
            // 獲取當前等待隊列中的前繼節點
            final Node p = node.predecessor();
            // 如果當前節點等於前繼節點
            // 假設
            // 1. 可能是第一個進入等待的,所以隊列中只有一個
            // 2. 可能是等待隊列中的節點已經放棄光了,
            if (p == head) {
                // 嘗試獲取鎖,看是否已經資源沒有了
                int r = tryAcquireShared(arg);
                // 如果資源已經沒有了,說明可以釋放鎖了
                if (r >= 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);
    }
}

countDown

// 釋放共享鎖
public final boolean releaseShared(int arg) {
    //1. 嘗試釋放共享鎖
    if (tryReleaseShared(arg)) {
        // 釋放鎖的操作
        doReleaseShared();
        return true;
    }
    return false;
}

// 1. 嘗試釋放共享鎖
protected boolean tryReleaseShared(int arg) {
  // Decrement count; signal when transition to zero
  // 自旋
  for (; ; ) {
    // 獲取當前資源數
    int c = getState();
    // 如果爲0的話,這裏的話表示不需要阻塞了
    if (c == 0) {
      return false;
    }
    // --------------------------如果資源數還有的話-------------------------------------
    // 遞減1
    int nextc = c - 1;
    // 之後通過CAS自旋去獲取 , 這裏的c就是getState()的state是volatile修飾,其他線程改了這裏一定能看到   // 這裏通過CAS去判斷當前資源是否有競爭,沒有競爭的話會賦值成功
    // 條件中會判斷是否爲0 , 這裏爲true的話,會觸發上面的釋放鎖的操作
    if (compareAndSetState(c, nextc)) {
      return nextc == 0;
    }
  }
}

// 這裏就是真正的釋放鎖的操作
private void doReleaseShared() {
  /*
           * Ensure that a release propagates, even if there are other
           * in-progress acquires/releases.  This proceeds in the usual
           * way of trying to unparkSuccessor of head if it needs
           * signal. But if it does not, status is set to PROPAGATE to
           * ensure that upon release, propagation continues.
           * Additionally, we must loop in case a new node is added
           * while we are doing this. Also, unlike other uses of
           * unparkSuccessor, we need to know if CAS to reset status
           * fails, if so rechecking.
           */
  // 自旋,通過頭結點進行釋放
  /**
  這裏需要先聲明一個細節點,不然很容易被繞進去,這裏的正常邏輯應該是
  1. head的節點是Node.SIGNAL狀態
  2. 單個線程釋放頭結點的時候肯定會經過unparkSuccessor方法,這個方法會將頭結點喚醒之後,會經過自旋迴到doAcquireSharedInterruptibly中的setHeadAndPropagate方法重新更換頭結點,一般是讓下一級節點頂上
  3. h == head 一定是爲true的 (前提是單個線程的情況下)
  
  而下面的代碼,除了正常情況,其他的都是搶佔併發資源的情況,如何去調整?都是通過自旋的方式,一遍一遍的去釋放,直道最終釋放完畢h == head 自旋結束
  */
  for (;;) {
    Node h = head;
    
    if (h != null && h != tail) {
      // 獲取頭結點的狀態
      int ws = h.waitStatus;
      // 如果是阻塞的狀態則開始設置爲自由狀態
      if (ws == Node.SIGNAL) {
        // 如果存在多個線程的競爭,則跳過這個循環,下一次繼續
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
          continue;            // loop to recheck cases
        // 如果上面設置成功了,則這裏開始針對這個節點做喚醒操作
        unparkSuccessor(h);
      }
      // 如果本身就是自由狀態,則將這個狀態設置爲傳播狀態,等待
      else if (ws == 0 &&
               !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;                // loop on failed CAS
    }
    if (h == head)                   // loop if head changed // __ 這裏需要注意的是,如果h!=head說明已經被其他線程操作過一遍了,重新再來,又從頭結點開始釋放
      break;
  }
}

CyclicBarrier

作用

構造方法傳遞一個初始值,當線程執行到該對象的await方法時會先阻塞。

如果阻塞的線程的數量達到初始值,則會開始喚醒阻塞的線程。然後再繼續下一輪的線程統計。

方法原理介紹

CyclicBarrier 構造方法

public CyclicBarrier(int parties) {
    this(parties, null);
}

public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
    // 構建一個初始變量,用於重複使用
    this.parties = parties;
    // 當前總數的變量,用來做計算
    this.count = parties;
    // 當count變量變成0的時候,會觸發這個線程中的方法
    this.barrierCommand = barrierAction;
}

dowait

/**
 * 主要的屏障代碼,涵蓋了各種策略
 */
private int dowait(boolean timed, long nanos)
    throws InterruptedException, BrokenBarrierException,
           TimeoutException {
             
    // 使用獨佔所,確保只能有一個線程獲取鎖,執行下面操作
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 當前屏障中的範圍的對象是否有效的標識
        final Generation g = generation;
        // 一旦屏障出現故障,便報錯
        if (g.broken)
            throw new BrokenBarrierException();

        // 如果線程已經中斷了
        if (Thread.interrupted()) {
            // 一旦當前處於屏障中的線程發生了中斷
            // 則直接影響到當前屏障範圍內的所有線程
            // 直接喚醒屏障內的線程,並且將該屏障內的週期重置
            breakBarrier();
            throw new InterruptedException();
        }
        // 當前屏障總數減一
        int index = --count;
        // 如果爲0的話,表示屏障已經使用完畢
        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)
                    // 如果還沒有到達超時時間,則直接通過conditions的方法進行處理
                    nanos = trip.awaitNanos(nanos);
            } catch (InterruptedException ie) {
                if (g == generation && ! g.broken) {
                    breakBarrier();
                    throw ie;
                } else {
                    // We're about to finish waiting even if we had not
                    // been interrupted, so this interrupt is deemed to
                    // "belong" to subsequent execution.
                    Thread.currentThread().interrupt();
                }
            }

            if (g.broken)
                throw new BrokenBarrierException();
            // 如果是同一個屏障內生成的對象相等
            if (g != generation)
                return index;// 也按照index處理

            // 如果超時了,則按照線程中斷的處理方式去處理
            if (timed && nanos <= 0L) {
                breakBarrier();
                throw new TimeoutException();
            }
        }
    } finally {
        //釋放鎖
        lock.unlock();
    }
}



/**
* Sets current barrier generation as broken and wakes up everyone.
* Called only while holding lock.
* 其實就是說當前屏障已經被打破了,直接喚醒所有屏障內的線程
*/
private void breakBarrier() {
  // 當前標識對象設置爲true會被異常觸發
  generation.broken = true;
  // 重新回到初始值
  count = parties;
  // 喚醒所有線程
  trip.signalAll();
}

/**
* Updates state on barrier trip and wakes up everyone.
* Called only while holding lock.
* 重置成一個新的屏障
*/
private void nextGeneration() {
  // signal completion of last generation
  // 喚醒所有線程
  trip.signalAll();
  // set up next generation
  // 將統計總數重置爲初始值
  count = parties;
  // 重新構建一個生成對象
  generation = new Generation();
}

執行流程

  1. 構建一個線程屏障個數範圍值
  2. 當一個線程開始阻塞的時候,會用ReentrantLock進行加鎖
  3. 判斷當前屏障範圍內的線程是否有效
    1. 如果其中一個線程無效了(中斷或者超時)
      1. 則喚醒該屏障內的所有線程
      2. 重新初始化屏障環境
  4. 屏障範圍數遞減
  5. 判斷範圍數是否已經爲0
    1. 如果爲0 則表示範圍內的線程數量已經達到喚醒的條件了
      1. 重新初始化屏障環境
      2. 執行觸發線程(由使用者傳遞)
  6. 如果沒有範圍數不爲0
    1. 通過ReentrantLockCondition的條件組進行阻塞
      1. 如果是超時情況的話通過awaitNanos方法進行阻塞
      2. 一旦超時,則該範圍內的線程都會被喚醒
      3. 屏障環境重置

Semaphore

Semaphore是信號量,用於管理一組資源。其內部是基於AQS的共享模式,AQS的狀態表示許可證的數量,在許可證數量不夠時,線程將會被掛起;而一旦有一個線程釋放一個資源,那麼就有可能重新喚醒等待隊列中的線程繼續執行。

作用

可以用於資源保護機制,例如同一時間允許的最大併發量。

方法原理介紹

acquire:

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    // 如果資源的數量小於0了
    if (tryAcquireShared(arg) < 0)
        // 阻塞當前還在搶佔的線程
        doAcquireSharedInterruptibly(arg);
}

// 嘗試獲取共享鎖
protected int tryAcquireShared(int acquires) {
  for (;;) {
    // 如果等待隊列中還有線程等待,則說明資源已經被搶光了,直接排到後面等待吧
    if (hasQueuedPredecessors())
      return -1;
    // 獲取狀態之後進行遞減得到的資源數是否爲0
    int available = getState();
    int remaining = available - acquires;
    // 如果小於0,或者CAS成功之後,返回值
    if (remaining < 0 ||
        compareAndSetState(available, remaining))
      return remaining;
  }
}

/**
 * 能進入到這個方法的話說明資源數已經被用光了
 * 這個方法只需要負責,將這些沒有搶佔到的資源給放到阻塞隊列中並阻塞即可
 */
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
    // 將當前線程構建成一個新的節點,並且加入到阻塞隊列尾部
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
      // 自旋
      for (;;) {
        // 獲取等待隊列中的最後一個節點
        final Node p = node.predecessor();
        // 如果這個節點是head節點的話
        if (p == head) {
          // 嘗試搶佔一下鎖
          int r = tryAcquireShared(arg);
          // 如果資源鎖還有的情況下
          if (r >= 0) {
            // 釋放當前正在阻塞的對象,如果有的對象正在阻塞中,則設置成PROPAGATE狀態
            setHeadAndPropagate(node, r);
            p.next = null; // help GC
            failed = false;
            return;
          }
        }
        // 加入到阻塞隊列中,並且獲取一個有效的前繼節點
        if (shouldParkAfterFailedAcquire(p, node) &&
            // 阻塞該節點
            parkAndCheckInterrupt())
          throw new InterruptedException();
      }
    } finally {
      if (failed)
        cancelAcquire(node);
    }
}

執行流程:

  1. 判斷state的資源數是否小於0

    1.1 不小於 --> 通過CAS將數值-1之後返回

    1.2 進入2

    1. 將當前線程構建成一個新的Node節點
    2. 獲取新的節點的前繼節點,如果是head節點?
    3. 再次嘗試獲取資源數,如果大於0
    4. 則釋放該節點
    5. 將當前節點掛靠到一個可靠的前節點下,並加入到等待隊列中
    6. 開始進行自我阻塞,等待被喚醒

release:

public final boolean releaseShared(int arg) {
    // 嘗試釋放鎖,資源數提升
    if (tryReleaseShared(arg)) {
        // 釋放鎖
        doReleaseShared();
        return true;
    }
    return false;
}
// 這裏就是簡單的通過CAS將資源鎖進行累加
protected final boolean tryReleaseShared(int releases) {
  for (;;) {
    int current = getState();
    int next = current + releases;
    if (next < current) // overflow
      throw new Error("Maximum permit count exceeded");
    if (compareAndSetState(current, next))
      return true;
  }
}

private void doReleaseShared() {
  for (;;) {
    // 拿到頭節點
    Node h = head;
    if (h != null && h != tail) {
      int ws = h.waitStatus;
      // 頭結點的狀態判斷
      if (ws == Node.SIGNAL) {
        // 將頭結點設置成自由狀態
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
          continue;            // loop to recheck cases
        // 釋放鎖,從尾節點一直到頭結點
        unparkSuccessor(h);
      }
      // 如果當前頭結點還沒有處於阻塞狀態,則直接設置成傳播狀態
      else if (ws == 0 &&
               !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;                // loop on failed CAS
    }
    // 如果上面的鎖已經釋放完畢了,這裏的頭結點也肯定就爲空了
    if (h == head)                   // loop if head changed
      break;
  }
}
// 具體釋放鎖的方法
private void unparkSuccessor(Node node) {
  // 獲取要釋放鎖的節點的等待狀態,一般是-1 阻塞狀態
  int ws = node.waitStatus;
  
  if (ws < 0)
    compareAndSetWaitStatus(node, ws, 0);
  // 如果該節點的下級節點爲空
  Node s = node.next;
  if (s == null || s.waitStatus > 0) {
    s = null;
    // 從下往上找,一直找到第一個狀態<=的進行釋放
    for (Node t = tail; t != null && t != node; t = t.prev)
      if (t.waitStatus <= 0)
        s = t;
  }
  // 如果該node的下級節點不爲空,則直接喚醒
  if (s != null)
    LockSupport.unpark(s.thread);
}

運行流程

  1. 獲取當前資源鎖並且通過CAS累加
  2. 嘗試釋放鎖,從頭結點開始 - doReleaseShared
  3. 拿到頭結點之後,判斷頭節點的狀態
    1. 如果阻塞,則開始喚醒
    2. 如果狀態爲自由狀態,則設置成共享狀態標誌
      1. 爲什麼會這麼做,因爲多個線程同時操作的時候,頭節點可能會被操作不及時
      2. 頭節點一旦被多個線程操作,勢必會引起線程安全問題,所以這裏也是爲什麼要使用自旋去從頭結點釋放
      3. 如果h不等於頭結點?說明已經被其他線程操作過一遍了,這裏又要重新開始釋放一次

二者比較

CountDownLatch

  1. 不可重用
  2. countDown後可以繼續執行自己的任務
  3. 一般是阻塞主線程(await),子線程不會阻塞(countDown)

CyclicBarrier

  1. 可重用
  2. await後直接阻塞,但是如果出現線程中斷或者超時,則直接喚醒該範圍內的所有線程
  3. CyclicBarrier底層是用ReentrantLock的Condition去做的組喚醒
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章