1. 程式人生 > >Java併發學習筆記(四)-柵欄CyclicBarrier

Java併發學習筆記(四)-柵欄CyclicBarrier

閉鎖是一次性物件,一旦進入終止狀態,就不能被重置,它是用來啟動一組相關的操作,或者等待一組相關的操作結束。

柵欄跟閉鎖有點類似,它能阻塞一組執行緒直到某個時間發生,但是這裡有個很大的區別,在柵欄裡,只有這組執行緒都到達柵欄位置時,才能繼續執行

public class CyclicBarrierDemo {
	public static void main(String[] args) {
		CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {
			//柵欄動作,在計數器為0的時候執行
			@Override
			public void run() {
				System.out.println("我們都準備好了.");
			}
		});
		
		ExecutorService es = Executors.newCachedThreadPool();
		for (int i = 0; i < 5; i++) {
			es.execute(new Roommate(barrier));
		}
	}
}

class Roommate implements Runnable {
	private CyclicBarrier barrier;
	private static int Count = 1;
	private int id;

	public Roommate(CyclicBarrier barrier) {
		this.barrier = barrier;
		this.id = Count++;
	}

	@Override
	public void run() {
		System.out.println(id + " : 我到了");
		try {
			//通知barrier,已經完成動作,在等待
			barrier.await();
			System.out.println("Id " + id + " : 點菜吧!");
		} catch (InterruptedException e) {
			e.printStackTrace();
		} catch (BrokenBarrierException e) {
			e.printStackTrace();
		}
	}
}

在上面的例子裡,柵欄初始計數為5,說明需要5個執行緒達到柵欄,這5個執行緒才能繼續執行。當在一個執行緒裡,呼叫barrier.await相當於告訴柵欄,已經有一個執行緒達到柵欄。當barrier.await執行5次後,柵欄開啟,所有5個執行緒都可以繼續執行。此時,還會發生兩件事,柵欄會執行柵欄動作,也就是在初始化柵欄物件時產生的Runnable類物件需要執行的run方法,還有一件事,柵欄的計數會重置為5。

綜合以上,柵欄和閉鎖有以下區別

  • 柵欄可重複使用,在計數器為0的時候,會重置為原來的計數
  • 柵欄有柵欄動作,在計數器為0的時候,會執行柵欄動作

那麼柵欄的實現機制跟閉鎖有什麼區別?

跟閉鎖依靠Sync不同,柵欄是依靠可重入鎖實現的。

    /** The lock for guarding barrier entry */
    private final ReentrantLock lock = new ReentrantLock();
    /** Condition to wait on until tripped */
    private final Condition trip = lock.newCondition();

ReentrantLock是併發庫提供的可重入鎖,而trip是ReentrantLock的條件,一個可重入鎖可以有多個條件。(以後補充)

建立柵欄物件的時候,傳遞計數和柵欄動作給柵欄物件。
    public CyclicBarrier(int parties, Runnable barrierAction) {
        if (parties <= 0) throw new IllegalArgumentException();
        this.parties = parties;
        this.count = parties;
        this.barrierCommand = barrierAction;
    }
barrierCommand即柵欄動作,count儲存計數器。
    public int await() throws InterruptedException, BrokenBarrierException {
        try {
            return dowait(false, 0L);
        } catch (TimeoutException toe) {
            throw new Error(toe); // cannot happen;
        }
    }
	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();
				}
			}

			//計數器不為0,則迴圈直到被啟用或者打斷或者超時
			for (;;) {
				try {
					if (!timed)
						//trip條件阻塞,此時,lock鎖會釋放掉
						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方法的邏輯是,用可重入鎖鎖定後,計數器減1,並判斷此時計數器是否為0,不為0,會進入迴圈,在condition變數trip呼叫await方法時,進入阻塞狀態並釋放lock鎖。如果計數器為0,會執行柵欄動作,並呼叫nextGeneration開始下一個柵欄週期。
    private void nextGeneration() {
        // signal completion of last generation
        trip.signalAll();
        // set up next generation
        count = parties;
        generation = new Generation();
    }
可以看到,在nextGeneration方法裡,trip呼叫了signalAll,喚醒了所有在trip.await阻塞的執行緒。