從源碼看JDK8并發(fā)工具類CountDownLatch的實現(xiàn)原理

CountDownLatch,是幾個重要的并發(fā)編程工具類之一,字面意思就是門鎖的意思,內(nèi)部會維護(hù)一個計數(shù)器的常量,這個常量代表執(zhí)行的線程數(shù)。

在多線程協(xié)作完成業(yè)務(wù)功能時,有時候需要等待其他多個線程完成任務(wù)之后,主線程才能繼續(xù)往下執(zhí)行業(yè)務(wù)功能,在這種的業(yè)務(wù)場景下,通常可以使用Thread類的join方法,讓主線程等待被join的線程執(zhí)行完之后,主線程才能繼續(xù)往下執(zhí)行。當(dāng)然,使用線程間消息通信機制也可以完成。其實,java并發(fā)工具類中為我們提供了類似“倒計時”(CountDownLatch)這樣的工具類,可以十分方便的完成所說的這種業(yè)務(wù)場景。

CountDownLatch允許一個或多個線程等待其他線程完成操作,調(diào)用await()方法的線程回去判斷count的值來判斷是否會被掛起,它會等待直到count值為0才會繼續(xù)執(zhí)行。控制臺輸出count=0最后輸出,這個時候就看cpu切換到哪個線程上執(zhí)行了,在初始化的時候我們會設(shè)置好count的值,當(dāng)每調(diào)用一次countDown()方法,會使count的值減一也就是將AQS維護(hù)同步狀態(tài)的state值減一。

在我們閱讀源碼之前,如果你看過AQS源碼(http://www.lxweimin.com/p/e0066f9349cd)與跟可重入鎖(http://www.lxweimin.com/p/5d57573b09f5)相關(guān)的內(nèi)容,你會更加對CountDownLatch本身是如何實現(xiàn)的以及他的本質(zhì)有一個更透徹的理解。

說說我的理解之前看看他的類

可以看到他同樣運用了一個繼承了AQS同步器的靜態(tài)內(nèi)部類來重寫父類AQS里面的一些方法然后再調(diào)用該父類里面的獲取鎖的方法來實現(xiàn)具體的功能。

來看看構(gòu)造方法中:

//設(shè)置初始化count的值,并傳遞給Sync類
public CountDownLatch(int count) {
       if (count < 0) throw new IllegalArgumentException("count < 0");
       this.sync = new Sync(count);
   }

Sync類的源碼如下:CountDownLatch的實現(xiàn)依賴于AQS

先介紹下兩個方法 countDown()每執(zhí)行一次該方法,也就是將由AQS維護(hù)的同步狀態(tài)值state值減1,其一般是執(zhí)行任務(wù)的線程調(diào)用。
調(diào)用countDown()釋放同步狀態(tài),每次調(diào)用同步狀態(tài)值-1。

 public void countDown() {
        sync.releaseShared(1);
    }

//父類AQS中
public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {   //如果釋放同步狀態(tài)線程成功,如果返回false,則表示,獲取失敗同步狀態(tài)。
  //返回flase,以CountDownLatch的實現(xiàn)角度來講,此時還要等待N(N>0)個線程,因為state還沒減到等于0,如果返回true,表示此時已經(jīng)執(zhí)行N次了,此時state已經(jīng)減到0了,這時候會執(zhí)行doReleaseShared(),表示釋放其他處于等待的節(jié)點。
            doReleaseShared();   //喚醒后續(xù)處于等待的節(jié)點,看下面具體的解釋。
            return true;
        }
        return false;
    }

//在CountDownLatch的靜態(tài)內(nèi)部工具類Sync繼承了AQS重寫的tryReleaseShared
protected boolean tryReleaseShared(int releases) {
            // 自旋
            for (;;) {
                int c = getState();  //獲取AQS維護(hù)的state值
                if (c == 0)   //如果為0,表示沒有一個線程在運行返回false
                    return false;
                int nextc = c-1;     //如果不等于0,這里肯定會>0的,所以減去1
                if (compareAndSetState(c, nextc))  //CAS去直接修改內(nèi)存地址的偏移量去修改值,保證線程安全。
                 return nextc == 0;       //重點來了。這里的意思是如果共享式獲取同步狀態(tài)后,state還不是為0,則獲取失敗。返回false
            }
        }

下面這個類,在我的這篇文章也解析過了。


await(),當(dāng)執(zhí)行該方法是,內(nèi)部會檢查那個計數(shù)常量的值,如果不等于0,就會進(jìn)入等待(waiting)狀態(tài),直到執(zhí)行了countDown使內(nèi)部的值減到0的時候,就會恢復(fù)線程,同時執(zhí)行,我們來看看實現(xiàn):

public void await() throws InterruptedException {
        //共享式獲取
        sync.acquireSharedInterruptibly(1);
    }

public final void acquireSharedInterruptibly(int arg)
            throws InterruptedException {
        if (Thread.interrupted())  //響應(yīng)中斷
            throw new InterruptedException();
        if (tryAcquireShared(arg) < 0)   
      //tryAcquireShared(arg) 返回1,此時state=0不阻塞,返回的是-1,執(zhí)行doAcquireSharedInterruptibly(arg);
            doAcquireSharedInterruptibly(arg);
    }

protected int tryAcquireShared(int acquires) {
            return (getState() == 0) ? 1 : -1;   
        }

這里需要解釋下doAcquireSharedInterruptibly的主要作用:1、將當(dāng)前線程構(gòu)造成共享模式的節(jié)點,通過自旋的方式嘗試獲取同步狀態(tài)2、如果獲取同步狀態(tài)成功,則喚醒后續(xù)處于共享模式的節(jié)點;如果沒有獲取到同步狀態(tài),則對調(diào)用shouldParkAfterFailedAcquire(Node, Node)和parkAndCheckInterrupt()方法掛起當(dāng)前線程,這樣可以避免該線程無限循環(huán)而獲取不到共享鎖,從而造成資源浪費。這里需要注意的是:當(dāng)有多個線程調(diào)用await()方法時,這些線程都會通過addWaiter(Node.SHARED)方法被構(gòu)造成節(jié)點加入到等待隊列中。當(dāng)最后一個調(diào)用countDown()方法的線程執(zhí)行了countDown()后(這里有點拗口),會喚醒處于等待隊列中距離頭節(jié)點最近的一個節(jié)點,也就是說該線程被喚醒之后會繼續(xù)自旋嘗試獲取同步狀態(tài),此時執(zhí)行到tryAcquireShared(int)方法時,發(fā)現(xiàn)r大于0(因為state已經(jīng)被置為0了),該線程就會調(diào)用setHeadAndPropagate(Node, int)方法將喚醒傳遞下去,并且退出當(dāng)前循環(huán),開始執(zhí)行awat()方法之后的代碼。


然后說說CountDownLatch的兩種用法:

1.可以設(shè)置new CountDownLatch(1); 如果需要控制多個線程同時開始執(zhí)行的時候,可以每個線程剛開始執(zhí)行run的時候,先執(zhí)行await,
進(jìn)入等待狀態(tài)。當(dāng)最后所有線程都準(zhǔn)備好了,就調(diào)用countDown,減一,這時所有線程就會主動同時開始執(zhí)行。
2.假設(shè)可以設(shè)置new CountDownLatch(10),這時有10個線程,我們需要做的是等10個線程,依次執(zhí)行countDown(),等到所有線程都執(zhí)行好了,這時候再執(zhí)行await。所有線程都準(zhǔn)備就緒了。

await(long timeout,TimeUtil unit)
作用使線程在指定的最大時間內(nèi),處于await狀態(tài),超過這個時間就會自動喚醒了。
getCount()
能夠獲取當(dāng)前計數(shù)的值。

下面舉一個實現(xiàn)的例子:

默認(rèn)10個運動員進(jìn)行跑步比賽的全過程:

public class MyThread extends Thread{

    /**等待運動員到來*/
    private CountDownLatch comingTag;
    /**等待裁判說開始*/
    private CountDownLatch waitTag;
    /** 等待起跑*/
    private CountDownLatch waitRunTag;
    /**起跑*/
    private CountDownLatch beginTag;
    /** 所有運動員道終點*/
    private CountDownLatch endTag;

    public MyThread(CountDownLatch comingTag, CountDownLatch waitTag, CountDownLatch waitRunTag, CountDownLatch beginTag, CountDownLatch endTag) {
        super();
        this.comingTag = comingTag;
        this.waitTag = waitTag;
        this.waitRunTag = waitRunTag;
        this.beginTag = beginTag;
        this.endTag = endTag;
    }

    @Override
    public void run() {
        try {
            System.out.println("運動員正陸續(xù)入場");
            Thread.sleep((int)Math.random()*10000);
            System.out.println(Thread.currentThread().getName()+"到起跑點了");
            comingTag.countDown();
            System.out.println("等待裁判說準(zhǔn)備");
            waitTag.await();
            System.out.println("準(zhǔn)備。。。。。開始");
            waitRunTag.countDown();
            beginTag.await();
            System.out.println(Thread.currentThread().getName()+"開始跑,并且跑步過程不確定");
            Thread.sleep((int)Math.random()*10000);
            endTag.countDown();
            System.out.println(Thread.currentThread().getName()+"到達(dá)終點");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

測試類:

public class Run {

    public static void main(String[] args) {


        CountDownLatch comingTag = new CountDownLatch(10);
        CountDownLatch waitTag=new CountDownLatch(1);
        CountDownLatch waitRunTag = new CountDownLatch(10);
        CountDownLatch beginTag=new CountDownLatch(1);
        CountDownLatch endTag = new CountDownLatch(10);

        MyThread[] threads=new MyThread[10];

        for(int i=0;i<threads.length;i++){
            threads[i]=new MyThread(comingTag,waitTag,waitRunTag,beginTag,endTag);
            threads[i].setName("運動員"+(i+1));
            threads[i].start();
        }

        try {
            System.out.println("裁判正在等待選手的到來。。。。");
            comingTag.await();
            System.out.println("所有的運動員都到齊了,準(zhǔn)備開始,各就位。。。。預(yù)備");
            Thread.sleep(5000);
            waitTag.countDown();
            System.out.println("各就各位。。。。");
            waitRunTag.await();
            Thread.sleep(2000);
            System.out.println("命令槍,開!!!");
            beginTag.countDown();
            endTag.await();
            System.out.println("所有運動員都到得終點了。。。");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}
image.png

image.png

一般來說,都會把CountDownLatch與CyclicBarrier進(jìn)行比較?

CountDownLatch一般用于某個線程A等待若干個其他線程執(zhí)行完任務(wù)之后,它才執(zhí)行;而CyclicBarrier一般用于一組線程互相等待至某個狀態(tài),然后這一組線程再同時執(zhí)行;CountDownLatch強調(diào)一個線程等多個線程完成某件事情。CyclicBarrier是多個線程互等,等大家都完成,再攜手共進(jìn)。
調(diào)用CountDownLatch的countDown方法后,當(dāng)前線程并不會阻塞,會繼續(xù)往下執(zhí)行;而調(diào)用CyclicBarrier的await方法,會阻塞當(dāng)前線程,直到CyclicBarrier指定的線程全部都到達(dá)了指定點的時候,才能繼續(xù)往下執(zhí)行;
CountDownLatch方法比較少,操作比較簡單,而CyclicBarrier提供的方法更多,比如能夠通過getNumberWaiting(),isBroken()這些方法獲取當(dāng)前多個線程的狀態(tài),并且CyclicBarrier的構(gòu)造方法可以傳入barrierAction,指定當(dāng)所有線程都到達(dá)時執(zhí)行的業(yè)務(wù)功能;
CountDownLatch是不能復(fù)用的,而CyclicBarrier是可以復(fù)用的。就是說,當(dāng)CountDownLatch執(zhí)行countDown時如果此時countDown執(zhí)行的state的值減到0了,這時候再調(diào)用,不能循環(huán)執(zhí)行了,而CyclicBarrier是可以的,可以看一下這篇文章:
http://www.lxweimin.com/p/ff6c2ef5e8c2

整理不易,喜歡可以關(guān)注我

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內(nèi)容