AQS分析第四篇(藉助CountDownLatch探索AQS共享模式的實現原理)

上一篇我們通過ReentrantLock分析瞭如何AQS獨佔模式的實現原理。這一篇我們將根據CountDownLatch探索共享模式的實現原理,如果你認真看完上一篇,那該篇的內容將更容易理解。主要分析以下幾個問題?

一、思考

  • 問題一:CountDownLatch提供了怎樣的功能?其實現原理是什麼?AQS獨佔模式是怎樣實現的?
  • 問題二:共享鎖和獨佔鎖的區別是什麼?

二、源碼分析

因爲CountDownLatch是藉助AQS實現的,所以其必然有一個內部類繼承AQS並實現tryAcquireShared(獲取共享鎖)和tryReleaseShared(釋放共享鎖)方法,這是使用AQS的唯一方式。我們去論證這一點:

在介紹CountDownLatch實現之前,我們先了解一下CountDownLatch的一般使用方式:

CountDownLatch latch=new CountDownLatch(n);


//A線程中等待
Thread  a=new Thread(()->{
    latch.await();
    doSomeThing();
});

....可能有多個線程同時調用latch.await();

//其他線程countDown
Thread  b=new Thread(()->{
    doSomeThing();
    latch.countDown();
});



Thread  c=new Thread(()->{
    doSomeThing();
    latch.await();
});

......

Thread  n=new Thread(()->{
    doSomeThing();
    latch.await();
});

接下來我們將從CountDownLathc的定義,await方法,countDown方法取剖析其實現原理,以及AQS共享模式的實現原理。

2.1CountDownLatch的定義

Method:CountDownLatch(int)

//CountDownLatch初始化
public CountDownLatch(int count) {
        if (count < 0) throw new IllegalArgumentException("count < 0");
        //詳情請看下文Sync構造函數
        this.sync = new Sync(count);
}

Method:Sync(int)

//在CountDownLatch初始化時調用AQS中setState方法將AQS中state值初始化爲count
Sync(int count) {
            setState(count);
}

2.2CountDownLatch等待(這裏只分析不限時等待,後面會專門出一篇關於AQS中限時等待的博文)

Method:await()

//使該線程等待直到以下兩種情況之一發生:
//1.其他線程調用countDown方法使count變爲0
//2.其他線程調用Thread.interrupt方法中斷該線程
public void await() throws InterruptedException {
    //具體請看下文acquireSharedInterruptibly(int)方法 
    sync.acquireSharedInterruptibly(1);
}

Method:AbstractQueuedSynchronizer.acquireSharedInterruptibly(int)

//通過共享模式獲取,如果線程被中斷則停止。

//如果當前線程被中斷,將拋出InterruptedException。
public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        //因爲支持可中斷,所以先檢查線程是否中斷

        if (Thread.interrupted())
            throw new InterruptedException();
        //先調用子類的tryAcquireShared嘗試以共享模式獲取,如果獲取失敗(該方法返回值小於0),則進
        //阻塞獲取

        //詳情請看下文tryAcquireShared和doAcquireSharedInterruptibly方法
        if (tryAcquireShared(arg) < 0)
            doAcquireSharedInterruptibly(arg);
}

Method:Sync.tryAcquireShared(int)

//嘗試獲取,如果stage爲0,則獲取成功,否則獲取失敗

//注意state被初始化爲n,這裏的語義就是當n沒有減爲0時,認爲該線程是不可獲取成功的
protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;
}

Method:doAcquireSharedInterruptibly(arg)

//以共享可中斷模式進行獲取
private void doAcquireSharedInterruptibly(int arg)
        throws InterruptedException {
        //addWaiter方法請看【AQS分析第三篇】

        //創建新的代表當前線程的Node入隊

        //這裏與獨佔模式的區別在於Node.nextWaiter=Node.SHARED
        final Node node = addWaiter(Node.SHARED);
        //最終是否獲取失敗
        boolean failed = true;
        try {
            //自旋,當被喚醒之後,再次檢查之前被阻塞的原因現在是否滿足
            for (;;) {
                final Node p = node.predecessor();
                //head爲當前獲取成功的線程,會釋放其後繼線程(這裏後繼線程是自己,所以嘗試獲取)
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    //這裏r>==0,說明state爲0(已有n個線程調用了countDown方法)
                    if (r >= 0) {
                      
                        //如果獲取成功,將當前Node設置爲隊頭節點

                        //這裏與獨佔模式不同的是,獨佔模式只是調用setHead(請看上一篇)設 
                        //置了隊頭,請看下文setHeadAndPropagate分析其除了設置隊頭還需要幹什麼
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        failed = false;
                        return;
                    }
                }
                //下面這段阻塞前處理和線程阻塞的代碼同【AQS分析第三篇】,這裏不再贅述

                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    throw new InterruptedException();
            }
        } finally {
            //這裏同上一篇ReentrantLock分析一樣,如果tryAcquireShared方法異常,則取消等待
            if (failed)
                cancelAcquire(node);
        }
}

Method:AbstactQueuedSynchronizer.setHeadAndPropagate(Node,int)

//在分析該方法之前我們先總結一下:
//到目前爲止,當前線程嘗試獲取成功(tryAcquireShared方法返回值大於等於0,即state值爲0),那麼此時 
//我們肯定要做的是設置當前線程爲隊頭線程(這一點同獨佔鎖,因爲無論什麼模式head代表獲取成功的線程)
//那除了設置隊頭還需要做什麼。

//因爲可能有多個線程
//會調用latch.await方法進行等待,等待的條件是state不爲0。當能走到這個方法,說明tryAcquiredShared方法返回值大於等於0,說明此時state爲0,所
//以我們需要將所有在await方法上阻塞的線程全部喚醒。所以當前被喚醒的線程是有義務去喚醒其後繼的所有
//線程的(傳播機制)。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; // Record old head for check below
        setHead(node);
       
        //下面列舉了很多需要傳播的情況,這裏我們只看propagate>0的情況,該參數表明了是否需要傳播
        //大於0表示需要,在CountDownLatch中,當state爲0時,tryAcquireShared返回的是1,即需要傳播。
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            //如果當前節點的後繼節點是共享模式,喚醒當前節點的後繼節點(傳播),在這裏我們第一次
            //使用了nextWaiter屬性,請看下文isShared方法(判斷需不需要傳播喚醒後繼節點)
            Node s = node.next;
            if (s == null || s.isShared())
                //需要喚醒則調用doReleaseShared依次喚醒後繼節點
                //請看下文doReleaseShared方法
                doReleaseShared();
        }
}

Method:isShared()

//返回true則代表該節點是需要被傳播喚醒的
final boolean isShared() {
            return nextWaiter == SHARED;
}

Method:doReleaseShared()

//該方法在CountDownLatch的實現中會被調用多次:
//1.當某個線程調用latch.countDown之後,state變爲0,調用該方法喚醒head後繼線程
//2.當某個阻塞線程從park返回之後,發現tryAcquireShared方法返回true,會調用setHeadAndPropagate
//,這個方法中會調用該方法喚醒head後繼線程

//該方法將確保釋放操作在隊列中傳播,即使有其他acquire/release操作在執行中。

private void doReleaseShared() {
        //無限傳播
        for (;;) {
            Node h = head;
            //該if分支不滿足情況:
            //h==null或者h==tail:這代表隊列中無Node或者只有一個Node節點(當前線程),則無須繼 
            //續傳播。

            //當隊列中阻塞線程個數>1時,則需要嘗試喚醒head後繼節點
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                //表明需要喚醒後繼節點
                if (ws == Node.SIGNAL) {
                    //CAS判斷是否有其他操作已經喚醒head後繼節點
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        //如果確實有其他操作已經喚醒head後繼節點,則再次循環傳播釋放
                        continue;            // loop to recheck cases
                    //喚醒head後繼節點 
                    unparkSuccessor(h);
                }
                //ws==0表明無阻塞線程被添加到head之後
                //CAS設置ws==PROPAGATE:讓下一個調用acquireShared的線程可以被無條件傳播,因爲
                //這裏釋放了鎖,但是並無喚醒任何線程,所以下一個線程當然可以無條件傳播

                //CAS失敗,可能已經有其它線程加入head之後,循環傳播喚醒
                else if (ws == 0 &&
                         !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                // loop on failed CAS
            }
            

            //當h==head,說明被喚醒的線程再次嘗試獲取依然失敗。或者無後繼節點需要喚醒,則退出循 
            //環。
            if (h == head)                   // loop if head changed
                break;
        }
    }

2.3CountDownLatch.countDown

Method:CountDownLatch.countDown()

//將count值減1,當count=0時喚醒所有在latch.latch上等待的線程
public void countDown() {
        //具體請看下文AbstractQueuedSynchronizer.releaseShared(int)方法
        sync.releaseShared(1);
}

Method:AbstractQueuedSynchronizer.releaseShared(int)

public final boolean releaseShared(int arg) {
        //嘗試以共享模式進行釋放,當返回true時(count爲0),喚醒素有才latch.await上等待的線程,
        //請看下文tryReleaseShared方法。
        if (tryReleaseShared(arg)) {
           //以傳播方式喚醒所有阻塞線程,請看上文對該方法的解釋
            doReleaseShared();
            return true;
        }
        return false;
}

Method:Sync.tryReleaseShared()

//該方法會線程安全的對count(state)進行減一操作,當count爲0之後
//返回值:true代表需要喚醒隊列中再latch.await上等待的線程
protected boolean tryReleaseShared(int releases) {
            // Decrement count; signal when transition to zero
            for (;;) {
                int c = getState();
                //當state已經爲0,說明已經執行過喚醒操作返回false
                if (c == 0)
                    return false;
                int nextc = c-1;
                //CAS加失敗重試
                if (compareAndSetState(c, nextc))
                    //如果當前線程將count-1之後,count爲0,則喚醒所有等待線程
                    return nextc == 0;
            }
}

至此我們已經看完CountDownLatch的源碼實現。接下來我們先總結一下:

CountDownLatch初始化時,會將state設置爲n,所有在CountDownLatch上調用await方法的線程都將等待直到有n個線程調用countDown方法將state值減爲0。當state值爲0時,AQS會通過傳播機制依次喚醒這些在隊列中阻塞的線程。但是這個喚醒操作,不會等待當前線程執行完成再去操作,只要當前線程獲取成功,就先喚醒後繼節點,然後繼續執行自己的方法。

三、開篇解答

  • 問題一:CountDownLatch提供了怎樣的功能?其實現原理是什麼?

答:CountDownLatch可以讓一個或一組線程等待其他一些線程進行完某些操作後執行。原理上藉助AQS共享模式進行實現,詳情請看上文源碼分析。

  • 問題二:共享模式和獨佔模式的區別是什麼?

答:我們先分析一下CountDownLatch中共享模式體現在哪裏,當調用CountDownLatch.countDown()方法時,只是CAS加失敗重試的機制,這裏不存在對資源的獲取操作,所以不會體現共享模式。當調用CountDownLatch.await方法時,如果state不爲0,那麼線程將阻塞,其實這裏和獨佔的ReentrantLock認爲state不爲0類似,也並體現不出共享性。但是當state爲0時,CountDownLatch中所有在await方法上阻塞的線程都將獲取成功,而ReentrantLock中只有一個線程可以在lock上獲取成功,這裏變體現了獨佔和共享模式的區別。總結一下如下:

獨佔模式:當state爲0時,下一個獲取成功的線程將獨佔state狀態,其他在等待這個條件(state=0)的線程無法獲取成功。

共享模式:當state爲0時,所有在等待這個條件(state=0)的線程都將獲取成功。

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