Java併發基礎六:併發工具類(2)CyclicBarrier

CyclicBarrier是什麼

CyclicBarrier是JDK1.5開始提供的併發編程,輔助工具類。用於併發編程的。在源碼中使用 ReentrantLock 和 Condition 的組合來使用,CyclicBarrier字面意思是“可重複使用的柵欄”。通過它可以實現讓一組線程等待至某個狀態之後再全部同時執行利用CyclicBarrier類可以實現一組線程相互等待,當所有線程都到達某個屏障點後再進行後續的操作。

CyclicBarrier工作原理

看下API
CyclicBarrier有兩個構造函數

public CyclicBarrier(int parties)
public CyclicBarrier(int parties, Runnable barrierAction)

第一個參數,表示屏障攔截的線程數量,每個線程調用await方法告訴CyclicBarrier我已經到達了屏障,然後當前線程被阻塞。
第二個參數,表示用於在線程到達屏障時,優先執行barrierAction這個Runnable對象,方便處理更復雜的業務場景。

讓線程處於barrier狀態的方法await()

public int await()
public int await(long timeout, TimeUnit unit)

第一個默認方法,表示要等到所有的線程都處於barrier狀態,才一起執行
第二個方法,指定了等待的時間,當所有線程沒有都處於barrier狀態,又到了指定的時間,所在的線程就繼續執行了。

實現原理:1、首先在CyclicBarrier的內部定義了一個ReentrantLock對象,調用lock獲取鎖,同時將爲獲得鎖的線程放入阻塞隊列並掛起。2、然後定義一個count值,每當一個獲取鎖的線程調用CyclicBarrier的await方法時,count值減1同時將該線程加入到Condition條件隊列中,3、加入隊列成功後,調用tryRelease()函數釋放鎖同時喚醒(LockSupport.unpark)阻塞隊列中下一個節點,緊接着將放入阻塞隊列中的線程掛起。4、然後阻塞隊列中喚醒的線程獲取鎖重試上面動作,直到count值等於0時,如果傳遞了barrierAction這個Runnable對象,將先執行該線程,5、barrierAction執行完後,Condition.signalAll喚醒所有線程並轉到下一代,6、喚醒的所有線程將同時進行後續任務的執行,(即當前被喚醒的所有線程調用acquireQueued去搶佔同步鎖,節點會從 condition 隊列移動到 AQS 等待隊列,則進入正常鎖的獲取流程)CyclicBarrier的await方法後面的流程就可以繼續執行下去了。

上面實現原理將全部流程都表述完了,不太好理解,小白建議查看前面AQS文章,使用通俗易懂的話在表達一下:

白話原理:首先在CyclicBarrier的內部定義了一個ReentrantLock對象 和 計數器count值,每當一個獲取鎖的線程調用CyclicBarrier的await方法時,count值減1同時將該線程掛起,直到count值等於0時,將喚醒掛起的所有線程,可以同時進行後續的操作了。

看如下示意圖,CyclicBarrier 和 CountDownLatch 很像,只是 CyclicBarrier 可以有不止一個柵欄,因爲它的柵欄(Barrier)可以重複使用(Cyclic)。

cyclicbarrier-2

CyclicBarrier應用場景例子
Demo:報旅行團旅行,三人成團導遊統一辦理簽證即可旅行,可以有很多這樣的團

public class CyclicBarrierDEmo {
    /**
     * 模擬旅行社旅遊,三人開團同時旅行之前導遊統一辦理簽證後,大家即可開啓旅程
     */
    static class TravelTask implements Runnable{
        /**
         * 遊客旅行線程
         */
        private CyclicBarrier cyclicBarrier;
        private String name;
        private int arriveTime;//趕到的時間

        public TravelTask(CyclicBarrier cyclicBarrier, String name, int arriveTime){
            this.cyclicBarrier = cyclicBarrier;
            this.name = name;
            this.arriveTime = arriveTime;
        }

        @Override
        public void run() {
            try {
                //模擬達到需要花的時間
                Thread.sleep(arriveTime * 1000);
                System.out.println(Thread.currentThread().getName()+ name +"到達集合點");
                cyclicBarrier.await();
                System.out.println(Thread.currentThread().getName()+ name +"開始旅行啦~~");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } catch (BrokenBarrierException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws Exception{

        CyclicBarrier cyclicBarrier = new CyclicBarrier(3, new Runnable() {
            /**
             * 導遊線程,都到達目的地時,辦理簽證
             */
            @Override
            public void run() {
                System.out.println("****導遊辦理簽證****");
                try {
                    //模擬發護照簽證需要2秒
                    Thread.sleep(3000);
                    System.out.println("****簽證辦理完成了,可以一起出發了****");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        Executor executor = Executors.newFixedThreadPool(3);
        //登哥最大牌,到的最晚
        for (int i =0;i<3;i++) {
            executor.execute(new TravelTask(cyclicBarrier, "哈登"+i, 3));
            executor.execute(new TravelTask(cyclicBarrier, "保羅"+i, 2));
            executor.execute(new TravelTask(cyclicBarrier, "戈登"+i, 1));
        }
    }
}

CyclicBarrier源碼解析

激動人心的時刻到了,一起看下源碼實現吧!

首先,CyclicBarrier 的源碼實現和 CountDownLatch 大相徑庭,CountDownLatch 基於 AQS 的共享模式的使用,而 CyclicBarrier 基於 Condition 來實現的。因爲 CyclicBarrier 的源碼相對來說簡單許多,讀者只要熟悉了前面關於 Condition 的分析,那麼這裏的源碼是毫無壓力的,就是幾個特殊概念罷了。

在CyclicBarrier類的內部有一個計數器,每個線程在到達屏障點的時候都會調用await方法將自己阻塞,此時計數器會減1,當計數器減爲0的時候所有因調用await方法而被阻塞的線程將被喚醒。這就是實現一組線程相互等待的原理,下面我們先看看CyclicBarrier有哪些成員變量。

//同步操作鎖
private final ReentrantLock lock = new ReentrantLock();
//線程攔截器
private final Condition trip = lock.newCondition();
//每次攔截的線程數
private final int parties;
//換代前執行的任務
private final Runnable barrierCommand;
//表示柵欄的當前代
private Generation generation = new Generation();
//計數器
private int count;
 
//靜態內部類Generation
private static class Generation {
  boolean broken = false;
}

上面貼出了CyclicBarrier所有的成員變量,可以看到CyclicBarrier內部是通過條件隊列trip來對線程進行阻塞的,並且其內部維護了兩個int型的變量parties和count,parties表示每次攔截的線程數,該值在構造時進行賦值。count是內部計數器,它的初始值和parties相同,以後隨着每次await方法的調用而減1,直到減爲0就將所有線程喚醒。CyclicBarrier有一個靜態內部類Generation,該類的對象代表柵欄的當前代,就像玩遊戲時代表的本局遊戲,利用它可以實現循環等待。barrierCommand表示換代前執行的任務,當count減爲0時表示本局遊戲結束,需要轉到下一局。在轉到下一局遊戲之前會將所有阻塞的線程喚醒,在喚醒所有線程之前你可以通過指定barrierCommand來執行自己的任務。我用一圖來描繪下 CyclicBarrier 裏面的一些概念:

接下來我們看看它的構造器。

//構造器1
public CyclicBarrier(int parties, Runnable barrierAction) {
  if (parties <= 0) throw new IllegalArgumentException();
  this.parties = parties;
  this.count = parties;
  this.barrierCommand = barrierAction;
}
 
//構造器2
public CyclicBarrier(int parties) {
  this(parties, null);
}

CyclicBarrier有兩個構造器,其中構造器1是它的核心構造器,在這裏你可以指定本局遊戲的參與者數量(要攔截的線程數)以及本局結束時要執行的任務,還可以看到計數器count的初始值被設置爲parties。CyclicBarrier類最主要的功能就是使先到達屏障點的線程阻塞並等待後面的線程,其中它提供了兩種等待的方法,分別是定時等待和非定時等待。

//非定時等待
public int await() throws InterruptedException, BrokenBarrierException {
  try {
    return dowait(false, 0L);
  } catch (TimeoutException toe) {
    throw new Error(toe);
  }
}
 
//定時等待
public int await(long timeout, TimeUnit unit) throws InterruptedException, BrokenBarrierException, TimeoutException {
  return dowait(true, unit.toNanos(timeout));
}

可以看到不管是定時等待還是非定時等待,它們都調用了dowait方法,只不過是傳入的參數不同而已。下面我們就來看看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()) {
      //如果當前線程被中斷會做以下三件事
      //1.打翻當前柵欄
      //2.喚醒攔截的所有線程
      //3.拋出中斷異常
      breakBarrier();
      throw new InterruptedException();
    }
    //每次都將計數器的值減1
    int index = --count;
    //計數器的值減爲0則需喚醒所有線程並轉換到下一代
    if (index == 0) {
      boolean ranAction = false;
      try {
        //喚醒所有線程前先執行指定的任務
        final Runnable command = barrierCommand;
        if (command != null) {
          command.run();
        }
        ranAction = true;
        //喚醒所有線程並轉到下一代
        nextGeneration();
        return 0;
      } finally {
        //確保在任務未成功執行時能將所有線程喚醒
        if (!ranAction) {
          breakBarrier();
        }
      }
    }
 
    //如果計數器不爲0則執行此循環
    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();
  }
}

上面貼出的代碼中註釋都比較詳細,我們只挑一些重要的來講。可以看到在dowait方法中每次都將count減1,減完後立馬進行判斷看看是否等於0,如果等於0的話就會先去執行之前指定好的任務,執行完之後再調用nextGeneration方法將柵欄轉到下一代,在該方法中會將所有線程喚醒,將計數器的值重新設爲parties,最後會重新設置柵欄代次,在執行完nextGeneration方法之後就意味着遊戲進入下一局。如果計數器此時還不等於0的話就進入for循環,根據參數來決定是調用trip.awaitNanos(nanos)還是trip.await()方法,這兩方法對應着定時和非定時等待。如果在等待過程中當前線程被中斷就會執行breakBarrier方法,該方法叫做打破柵欄,意味着遊戲在中途被掐斷,設置generation的broken狀態爲true並喚醒所有線程。同時這也說明在等待過程中有一個線程被中斷整盤遊戲就結束,所有之前被阻塞的線程都會被喚醒。線程醒來後會執行下面三個判斷,看看是否因爲調用breakBarrier方法而被喚醒,如果是則拋出異常;看看是否是正常的換代操作而被喚醒,如果是則返回計數器的值;看看是否因爲超時而被喚醒,如果是的話就調用breakBarrier打破柵欄並拋出異常。這裏還需要注意的是,如果其中有一個線程因爲等待超時而退出,那麼整盤遊戲也會結束,其他線程都會被喚醒。下面貼出nextGeneration方法和breakBarrier方法的具體代碼。

//切換柵欄到下一代
private void nextGeneration() {
  //喚醒條件隊列所有線程
  trip.signalAll();
  //設置計數器的值爲需要攔截的線程數
  count = parties;
  //重新設置柵欄代次
  generation = new Generation();
}
 
//打翻當前柵欄
private void breakBarrier() {
  //將當前柵欄狀態設置爲打翻
  generation.broken = true;
  //設置計數器的值爲需要攔截的線程數
  count = parties;
  //喚醒所有線程
  trip.signalAll();
}

trip.await()具體代碼:

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();
            int savedState = fullyRelease(node);
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {
                LockSupport.park(this);
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
}

   final int fullyRelease(Node node) {
        boolean failed = true;
        try {
            int savedState = getState();
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }
public final boolean release(int arg) {
        // 將鎖釋放同時State值設置爲0
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                // 頭結點ws值爲0,並喚醒下個節點
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
 protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

最後,我們來看看怎麼重置一個柵欄:

public void reset() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        breakBarrier();   // break the current generation
        nextGeneration(); // start a new generation
    } finally {
        lock.unlock();
    }
}

我們設想一下,如果初始化時,指定了線程 parties = 4,前面有 3 個線程調用了 await 等待,在第 4 個線程調用 await 之前,我們調用 reset 方法,那麼會發生什麼?首先,打破柵欄,那意味着所有等待的線程(3個等待的線程)會喚醒,await 方法會通過拋出 BrokenBarrierException 異常返回。然後開啓新的一代,重置了 count 和 generation,相當於一切歸零了。

線程喚醒後,則繼續執行下面的方法獲取鎖(執行順序可參考前篇文章:深入理解AQS),最後執行await下面的代碼。

final boolean acquireQueued(final Node node, int arg) {
 boolean failed = true;
 try {
    boolean interrupted = false;
    for (;;) {
       final Node p = node.predecessor();//獲取當前節點的 prev 節點
       if (p == head && tryAcquire(arg)) {//如果是 head 節點,說明有資格去爭搶鎖
          setHead(node);//獲取鎖成功,也就是ThreadA 已經釋放了鎖,然後設置 head 爲 ThreadB 獲得執行權限
          p.next = null; //把原 head 節點從鏈表中移除
          failed = false;
          return interrupted;
      }
      //ThreadA 可能還沒釋放鎖,使得 ThreadB 在執行 tryAcquire 時會返回 false
      if (shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt())
          interrupted = true; //並且返回當前線程在等待過程中有沒有中斷過。
   }
 } finally {
    if (failed)
      cancelAcquire(node);
    }
}

文章參考:

https://blog.csdn.net/qq_39241239/article/details/87030142

https://www.jianshu.com/p/4ef4bbf01811

https://www.jianshu.com/p/3b92bd4b430a

 

 

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