CountDownLatch 分析

參考 一行一行源碼分析清楚AQS
AQS 獨(dú)占鎖:只能有一個(gè)線程持有鎖狞膘,獲取鎖失敗的線程進(jìn)入阻塞隊(duì)列,持有鎖的線程釋放鎖之后會喚醒等待隊(duì)列中的第一個(gè)線程,讓其來占有鎖
AQS 共享鎖:允許多個(gè)線程同時(shí)持有鎖被辑,當(dāng)隊(duì)列中的等待線程被喚醒以后就重新嘗試獲取鎖資源燎悍,如果成功則喚醒后面還在等待的共享節(jié)點(diǎn)并把該喚醒事件傳遞下去,
即會依次喚醒在該節(jié)點(diǎn)后面的所有共享節(jié)點(diǎn)

waitStatus 值為0盼理,代表初始化狀態(tài)谈山,值為-1 代表后續(xù)一個(gè)節(jié)點(diǎn)需要被喚醒。
AQS在判斷狀態(tài)時(shí)宏怔,通過用waitStatus>0表示取消狀態(tài)奏路,而waitStatus<0表示有效狀態(tài)。

CountDownLatch就是利用AQS的共享鎖機(jī)制來實(shí)現(xiàn)的臊诊。

假設(shè)有4個(gè)線程鸽粉,t1,t2,t3,t4. t1,t2調(diào)用countdown()方法,t3,t4調(diào)用await()方法

public void await() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {

    if (Thread.interrupted())
        throw new InterruptedException();
    //  調(diào)用 await 的時(shí)候抓艳,state都是小于 0触机。
    // 也就是說,這個(gè) if 返回 true玷或,然后往里看
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
// 只有當(dāng) state == 0 的時(shí)候儡首,這個(gè)方法才會返回 1  state==0 代表可以喚醒阻塞的線程了
protected int tryAcquireShared(int acquires) {
    return (getState() == 0) ? 1 : -1;
}

// 獲取共享鎖
private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 1. 入隊(duì)
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
            final Node p = node.predecessor();
            if (p == head) {
                // head == p 代表當(dāng)前節(jié)點(diǎn)是阻塞隊(duì)列的第一個(gè)節(jié)點(diǎn) 
                //只要 state 不等于 0,那么這個(gè)方法返回 -1庐椒,也就是下面這個(gè)if判斷進(jìn)不去
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    failed = false;
                    return;
                }
            }
            // 到 shouldParkAfterFailedAcquire 的時(shí)候椒舵,t3 將 head 的 waitStatus 值設(shè)置為 -1,代表之后的一個(gè)線程需要被喚醒
            if (shouldParkAfterFailedAcquire(p, node) &&
                //然后進(jìn)入到 parkAndCheckInterrupt 的時(shí)候约谈,t3 掛起笔宿。
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

t4 入隊(duì),t4 會將前驅(qū)節(jié)點(diǎn) t3 所在節(jié)點(diǎn)的 waitStatus 設(shè)置為 -1,然后 t4 也掛起棱诱。為什么需要把前面節(jié)點(diǎn)waitStatus 設(shè)置為 -1泼橘,我覺得是給定一種信號,共享鎖模式下迈勋,每個(gè)共享節(jié)點(diǎn)入隊(duì)都會把waitStatus 設(shè)置為 -1炬灭,代表自己后面要被喚醒。接下來靡菇,t3 和 t4 就等待喚醒了重归。

public void countDown() {
    sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
    // 只有當(dāng) state 減為 0 的時(shí)候,tryReleaseShared 才返回 true厦凤,然后執(zhí)行喚醒操作
    // 否則只是將 state = state - 1 那么 countDown 方法就結(jié)束了
    if (tryReleaseShared(arg)) {
        // 喚醒 await 的線程
        doReleaseShared();
        return true;
    }
    return false;
}

//用CAS自旋的方法實(shí)現(xiàn) state 減 1
protected boolean tryReleaseShared(int releases) {
    for (;;) {
        int c = getState();
        if (c == 0)
            return false;
        int nextc = c-1;
        if (compareAndSetState(c, nextc))
            return nextc == 0;
    }
}

// 開始釋放鎖鼻吮,然后執(zhí)行喚醒操作
private void doReleaseShared() {
    for (;;) {
        Node h = head;
        // 1. h == null: 說明阻塞隊(duì)列為空
        // 2. h == tail: 說明頭結(jié)點(diǎn)可能是剛剛初始化的頭節(jié)點(diǎn),
        // 或者是普通線程節(jié)點(diǎn)较鼓,但是此節(jié)點(diǎn)既然是頭節(jié)點(diǎn)了椎木,那么代表已經(jīng)被喚醒了违柏,阻塞隊(duì)列沒有其他節(jié)點(diǎn)了
        // 所以這兩種情況不需要進(jìn)行喚醒后繼節(jié)點(diǎn)
        if (h != null && h != tail) {
            int ws = h.waitStatus;
            // t3 入隊(duì)的時(shí)候,已經(jīng)將頭節(jié)點(diǎn)的 waitStatus 設(shè)置為 Node.SIGNAL(-1) 了
            if (ws == Node.SIGNAL) {
               // 利用CAS將前驅(qū)節(jié)點(diǎn)的waitStatus值置為0香椎,清空當(dāng)前節(jié)點(diǎn)即t3需要被喚醒的信號漱竖,
                // 因?yàn)轳R上就要喚醒他了,避免重復(fù)喚醒 這里可能CAS失敗畜伐,下面有解釋
                if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                    continue;            // loop to recheck cases
                // 喚醒 head 的后繼節(jié)點(diǎn)馍惹,也就是阻塞隊(duì)列中的第一個(gè)節(jié)點(diǎn) 在這里,也就是喚醒 t3
                unparkSuccessor(h);
            }
            else if (ws == 0 &&
                // 這個(gè) CAS 失敗的場景是:執(zhí)行到這里的時(shí)候烤礁,剛好有一個(gè)節(jié)點(diǎn)入隊(duì)讼积,入隊(duì)會將這個(gè) ws 設(shè)置為 -1
                     !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // todo
                continue;                // loop on failed CAS
        }
        // 如果到這里的時(shí)候,前面喚醒的線程已經(jīng)占領(lǐng)了 head脚仔,那么再循環(huán)
        // 否則,就是 head 沒變舆绎,那么退出循環(huán)鲤脏,
        // 退出循環(huán)之后喚醒的線程之后還是會調(diào)用這個(gè)方法的
        if (h == head)                   // loop if head changed
            break;
    }
}

一旦 t3 被喚醒后,回到 await 的這段代碼吕朵,parkAndCheckInterrupt 返回猎醇,接下來,t3 會進(jìn)到 setHeadAndPropagate(node, r) 這個(gè)方法努溃,先把 自己設(shè)置為head 硫嘶,然后喚醒隊(duì)列中后續(xù)線程:

private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; // Record old head for check below
    setHead(node);

     
    // 下面說的是,喚醒當(dāng)前 node 之后的節(jié)點(diǎn)梧税,即t3醒了沦疾,喚醒t4,t4喚醒t5等等
    //  也就是共享鎖的思路
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
        (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        if (s == null || s.isShared())
            // 現(xiàn)在的 head 已經(jīng)不是原來的空節(jié)點(diǎn)了,是 t3 的節(jié)點(diǎn)
            doReleaseShared();
    }
}

解釋下為什么第一個(gè)CAS可能會失數诙印:
for 循環(huán)第一輪的時(shí)候會喚醒 t4哮塞,t4 醒后會將自己設(shè)置為頭節(jié)點(diǎn),如果在 t4 設(shè)置頭節(jié)點(diǎn)后凳谦,for 循環(huán)才跑到
if (h == head)忆畅,那么此時(shí)會返回 false,for 循環(huán)會進(jìn)入下一輪尸执。t4 喚醒后也會進(jìn)入到這個(gè)方法里面家凯,
那么 for 循環(huán)第二輪和 t4 就有可能在這個(gè) CAS 相遇,那么就只會有一個(gè)成功了如失。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末绊诲,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子岖常,更是在濱河造成了極大的恐慌驯镊,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,542評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異板惑,居然都是意外死亡橄镜,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,822評論 3 394
  • 文/潘曉璐 我一進(jìn)店門冯乘,熙熙樓的掌柜王于貴愁眉苦臉地迎上來洽胶,“玉大人,你說我怎么就攤上這事裆馒℃⒚ィ” “怎么了?”我有些...
    開封第一講書人閱讀 163,912評論 0 354
  • 文/不壞的土叔 我叫張陵喷好,是天一觀的道長翔横。 經(jīng)常有香客問我,道長梗搅,這世上最難降的妖魔是什么禾唁? 我笑而不...
    開封第一講書人閱讀 58,449評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮无切,結(jié)果婚禮上荡短,老公的妹妹穿的比我還像新娘。我一直安慰自己哆键,他們只是感情好掘托,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,500評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著籍嘹,像睡著了一般闪盔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上噩峦,一...
    開封第一講書人閱讀 51,370評論 1 302
  • 那天锭沟,我揣著相機(jī)與錄音,去河邊找鬼识补。 笑死族淮,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的凭涂。 我是一名探鬼主播祝辣,決...
    沈念sama閱讀 40,193評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼切油!你這毒婦竟也來了蝙斜?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,074評論 0 276
  • 序言:老撾萬榮一對情侶失蹤澎胡,失蹤者是張志新(化名)和其女友劉穎孕荠,沒想到半個(gè)月后娩鹉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,505評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡稚伍,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,722評論 3 335
  • 正文 我和宋清朗相戀三年弯予,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片个曙。...
    茶點(diǎn)故事閱讀 39,841評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡锈嫩,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出垦搬,到底是詐尸還是另有隱情呼寸,我是刑警寧澤,帶...
    沈念sama閱讀 35,569評論 5 345
  • 正文 年R本政府宣布猴贰,位于F島的核電站对雪,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏米绕。R本人自食惡果不足惜慌植,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,168評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望义郑。 院中可真熱鬧,春花似錦丈钙、人聲如沸非驮。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,783評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽劫笙。三九已至,卻和暖如春星岗,著一層夾襖步出監(jiān)牢的瞬間填大,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,918評論 1 269
  • 我被黑心中介騙來泰國打工俏橘, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留允华,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,962評論 2 370
  • 正文 我出身青樓寥掐,卻偏偏與公主長得像靴寂,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子召耘,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,781評論 2 354