手撕同步工具類源碼(一)

在瞭解了 AQS 再來學習下,它在 Java 源碼中的應用。先從同步工具類開始吧;關於同步工具類如何使用,有一篇很好的文章可以給你:點擊這裏


CountDownLatch

允許一個或多個線程等待,直到在其它線程中執行的一組操作完成。

在瞭解了 AQS 之後,我們知道它的獨佔鎖式和共享鎖模式。所以,我們第一步需要弄明白 ,CountDownLatch 使用的是什麼模式?參考它的用法知道,它需要一個或多個線程阻塞,並在計數降爲 0 後,喚醒所有阻塞的線程,所以,它是一個共享鎖模式。

AQS 也提到過,大多數使用它的地方都不是直接繼承,而是維護一個內部的私有類來繼承,在這裏也一樣:

	private static final class Sync extends AbstractQueuedSynchronizer {
        private static final long serialVersionUID = 4982264981922014374L;
		// 初始化計數,使用 AQS 中的 state
        Sync(int count) {
            setState(count);
        }

        int getCount() {
            return getState();
        }
		/**
		* 使用共享鎖模式需要實現的方法,在這裏,state 爲 0 表示獲取到鎖,如果獲取鎖失敗,將入 CLH 隊		  * 列並阻塞線程;
		*/
        protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
        }
		/**
		* 遞減 state
		*/
        protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                if (c == 0)
                    return false;
                int nextc = c-1;
                if (compareAndSetState(c, nextc))
                    return nextc == 0;
            }
        }
    }

CountDownLatch 有着以下幾個方法,由於每個方法的源碼比較簡單,這裏就不在列出:

  1. 構造函數:構造函數接受一個 int ,用於創建上面的 Sync 對象,這是 state 初始的值;更改 state 的值必須使用父類的方法,爲了線程安全;

  2. await:該方法調用了 AQSacquireSharedInterruptibly 方法,熟悉 AQS 的朋友都知道,該方法會回調子類實現的 tryAcquireShared 方法(也就是上面的這個方法)。如果這個方法返回的數小於 0,則將入隊列,並阻塞,然後等待被喚醒;

  3. countDwon:該方法調用了 AQSreleaseShared 方法,每次狀態值減一,該方法會回調上面的 tryReleaseShared 方法。如果該方法返回 true,將去喚醒隊列中阻塞的線程。可以看見,當狀態值爲已經 0 ,將直接返回 false,只有在狀態值第一次等於 0 時,纔會返回 true,這時候就會嘗試去喚醒隊列中的阻塞線程。

    注意:並不是等於 0 就直接去喚醒,而是隻有在第一次等於 0 纔會去這樣做,也就是後續再調用這個方法並不會再去隊列上喚醒線程了,儘管這個隊列上沒有線程。

  4. getCount:返回狀態值,在這裏的語義可以理解爲:等待線程還需再等待多少其它線程完成操作就可以執行。

總結:state 值很重要,只應該通過 AQS 提供的方法來操作它。鎖與 state 值只存在一種表示關係,你可以理解爲 state 爲 0 才表示獲得鎖,也可以理解爲 state 爲 1 才表示獲得鎖,這取決於你自己。只要明白 tryAcquireShared 方法返回一個大於 0 的鎖就是鎖獲取成功,返回小於 1 的數,表示獲取鎖失敗,並且會阻塞等待,其它的完全取決於你的想象力。


CyclicBarrier

允許一組線程彼此等待到達一個共同的障礙點

CyclicBarrier 有一個代的思想,它指的是當一組線程均到達了屏障,則完成了一代,然後開始下一代;但也有特殊情況,那就是在各線程均到達屏障點以後要執行的那個任務失敗了,則將損壞當前代(屏障被打破),其它還未獲得執行許可的線程在阻塞超時以後,將得到當前代已經被損壞的信息,並拋出 BrokenBarrierException 異常。

CyclicBarrier 底層是通過 ReentrantLock ,並結合 Condition 來實現的,主要的代碼是以下這個方法:

	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;
            // 代表所有線程均到達屏障
            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 {
                        // 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;
				
                if (timed && nanos <= 0L) {
                    breakBarrier();
                    throw new TimeoutException();
                }
            }
        } finally {
            lock.unlock();
        }
    }

從上面可以知道,如果在屏障處等待的線程被中斷或者超時,將打破屏障(如果還處在當前代的話),所有在已到達屏障處的線程將被喚醒,並拋出 BrokenBarrierException;還未到達屏障處的線程在到達以後,將立即得到該異常。

CyclicBarrier 的方法,都是基於 ReentrantLock 的加鎖和解鎖來操作的,所以,在調用其它的方法時,也需要注意鎖的競爭;

總結

在屏障處等待的一個線程,有三種結束狀態:

  1. 正常的結束:此時,循環屏障已經開啓下一代,可重複使用;
  2. 被中斷,捕獲到中斷異常:這表明屏障已經被打破,其它在屏障處等待的線程將捕獲到 BrokenBarrierException 異常。
  3. 捕獲到 BrokenBarrierException 異常:這可能是由於屏障任務執行失敗,其它線程被中斷。

想要重用循環屏障,需要調用 reset 方法,不過,參考該方法的實現發現,該方法會先去打破屏障,再開始下一代。該方法的文檔介紹說並不適用於重置因某種原因破損的屏障,此時,推薦重新創建循環屏障。


推薦博文


手撕 AQS 之獨佔鎖模式

手撕AQS之共享鎖模式

手撕AQS之Condition


我與風來


認認真真學習,做思想的產出者,而不是文字的搬運工。錯誤之處,還望指出!

小新


如果覺得這篇文章對你有所幫助,動動小手,點點贊,這將使我能夠爲你帶來更好的文章。

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