CountDownLatch 介紹
CountDownLatch是一個同步協(xié)助類已亥,允許一個或多個線程等待,直到其他線程完成操作集挺智。
CountDownLatch使用給定的計數(shù)值(count)初始化航缀。await方法會阻塞直到當(dāng)前的計數(shù)值(count)由于countDown方法的調(diào)用達(dá)到0芳誓,在這之后(即,count為0之后)所有等待的線程都會被釋放兔簇,并且隨后對await方法的調(diào)用都會立即返回发绢。這是一個一次性現(xiàn)象 ———— count不會被重置硬耍。如果你需要一個重置count的版本,那么請考慮使用CyclicBarrier边酒。
CountDownLatch是一個通用的同步工具经柴,它能用于許多用途。一個使用’1’計數(shù)值初始化的CountDownLatch服務(wù)作為一個簡單的開關(guān)門:所有執(zhí)行await的線程等待在門口墩朦,直到某個執(zhí)行countDown方法的線程將門打開坯认。一個使用‘N(count)’初始化的CountDownLatch能被用于使一個線程等待,直到N個線程完成某些動作氓涣,或者某些動作已經(jīng)完成N次牛哺。
CountDownLatch一個很有用的性質(zhì)是,它不要求你在可以繼續(xù)進(jìn)行之前調(diào)用countDown方法等待count到達(dá)0劳吠,它只是簡單的防止任何線程超過await方法直到所有的線程都可以通過引润。
也就是說,你可以在任意時刻調(diào)用await痒玩,如果當(dāng)前的count值非0淳附,那么線程會等待直到count為0時才會繼續(xù)往下執(zhí)行,否則如果count值為0凰荚,await方法會立即返回燃观,你可以不被阻塞的繼續(xù)往下執(zhí)行。
內(nèi)存一致性作用:直到count到達(dá)0便瑟,一個線程調(diào)用countDown()方法之前的動作 happen-before 從另一個線程相應(yīng)的await()方法返回之后的動作缆毁。
比如,threadB.await()到涂、threadA.countDown()脊框,那么threadA執(zhí)行countDown()之前的動作,對于threadB的await()方法之后的動作都可見(當(dāng)count為0時践啄,threadB會從await()方法的阻塞中結(jié)束而繼續(xù)往下執(zhí)行)浇雹。
AbstractQueuedSynchronizer
因?yàn)镃ountDownLatch是使用AbstractQueuedSynchronizer(AQS)的state來實(shí)現(xiàn)其同步控制的。CountDownLatch使用的是共享鎖模式屿讽,由于AQS除了共享鎖模式還有排他鎖模式昭灵,本文僅對CountDownLatch涉及到的共享鎖模式部分的內(nèi)容進(jìn)行介紹,關(guān)于排他鎖模式的部分會在 ReentrantLock 源碼淺析一文中介紹伐谈。
AQS提供一個框架用于實(shí)現(xiàn)依賴于先進(jìn)先出(FIFO)等待隊列的阻塞鎖和同步器(信號量烂完,事件等)。這個類被設(shè)計與作為一個有用的基類诵棵,一個依賴單一原子值為代表狀態(tài)的多種同步器的基類抠蚣。子類必須將修改這個狀態(tài)值的方法定義為受保護(hù)的方法,并且該方法會根據(jù)對象(即履澳,AbstractQueuedSynchronizer子類)被獲取和釋放的方式來定義這個狀態(tài)嘶窄。根據(jù)這些怀跛,這個類的其他方法實(shí)現(xiàn)所有排隊和阻塞的機(jī)制。子類能夠維護(hù)其他的狀態(tài)屬性柄冲,但是只有使用『getState』方法吻谋、『setState』方法以及『compareAndSetState』方法來原子性的修改 int 狀態(tài)值的操作才能遵循相關(guān)同步性。
等待隊列節(jié)點(diǎn)類 ——— Node
等待隊列是一個CLH鎖隊列的變體羊初。CLH通常被用于自旋鎖(CLH鎖是一種基于鏈表的可擴(kuò)展滨溉、高性能、公平的自旋鎖长赞,申請線程只在本地變量上自旋晦攒,它不斷輪詢前驅(qū)的狀態(tài),如果發(fā)現(xiàn)前驅(qū)釋放了鎖就結(jié)束自旋得哆。)脯颜。我們用它來代替阻塞同步器,但是使用相同的基本策略贩据,該策略是持有一些關(guān)于一個線程在它前驅(qū)節(jié)點(diǎn)的控制信息栋操。一個“status”字段在每個節(jié)點(diǎn)中用于保持追蹤是否一個線程需要被阻塞。一個節(jié)點(diǎn)會得到通知當(dāng)它的前驅(qū)節(jié)點(diǎn)被釋放時饱亮。隊列中的每一個節(jié)點(diǎn)都作為一個持有單一等待線程的特定通知風(fēng)格的監(jiān)視器矾芙。狀態(tài)字段不會控制線程是否被授予鎖等。一個線程可能嘗試去獲取鎖如果它在隊列的第一個近上。但是首先這并不保證成功剔宪,它只是給與了競爭的權(quán)力(也就是說,隊列中第一個線程嘗試獲取鎖時壹无,并不保證一定能得到鎖葱绒,它只是有競爭鎖的權(quán)力而已)。所以當(dāng)前被釋放的競爭者線程可能需要重新等待獲取鎖斗锭。
(這里說的"隊列中的第一個的線程"指的時地淀,從隊列頭開始往下的節(jié)點(diǎn)中,第一個node.thread != null的線程岖是。因?yàn)榘锘伲珹QS隊列的head節(jié)點(diǎn)是一個虛節(jié)點(diǎn),不是有個有效的等待節(jié)點(diǎn)豺撑,因此head節(jié)點(diǎn)的thread是為null的作箍。)
為了排隊進(jìn)入一個CLH鎖,你可以原子性的拼接節(jié)點(diǎn)到隊列中作為一個新的隊尾前硫;對于出隊,你只要設(shè)置頭字段荧止。(即屹电,入隊操作時新的節(jié)點(diǎn)會排在CLH鎖隊列的隊尾阶剑,而出隊操作就是將待出隊的node設(shè)置為head。由此可見危号,在AQS中維護(hù)的這個等待隊列牧愁,head是一個無效的節(jié)點(diǎn)。初始化時head是一個new Node()節(jié)點(diǎn)外莲;在后期的操作中猪半,需要出隊的節(jié)點(diǎn)就會設(shè)置到head中。)
+------+ prev +-----+ +-----+
head | | <---- | | <---- | | tail
+------+ +-----+ +-----+
插入到一個CLH隊列的請求只是一個對“tail”的單個原子操作偷线,所以有一個簡單的從未入隊到入隊的原子分割點(diǎn)磨确。類似的,出隊調(diào)用只需要修改“head”声邦。然而乏奥,節(jié)點(diǎn)需要更多的工作來確定他們的后繼者是誰,部分是為了處理由于超時和中斷而導(dǎo)致的可能的取消亥曹。
(也就是說邓了,一個node的后繼節(jié)點(diǎn)不一定就是node.next,因?yàn)殛犃兄械墓?jié)點(diǎn)可能因?yàn)槌瑫r或中斷而取消了媳瞪,而這些取消的節(jié)點(diǎn)此時還沒被移除隊列(也許正在移除隊列的過程中)骗炉,而一個node的后繼節(jié)點(diǎn)指的是一個未被取消的有效節(jié)點(diǎn),因此在下面的操作中你就會發(fā)現(xiàn)蛇受,在尋找后繼節(jié)點(diǎn)時句葵,尋找的都是當(dāng)前節(jié)點(diǎn)后面第一個有效節(jié)點(diǎn),即非取消節(jié)點(diǎn)龙巨。)
“prev”(前驅(qū))連接(原始的CLH鎖是不使用前驅(qū)連接的)笼呆,主要用于處理取消。如果一個節(jié)點(diǎn)被取消了旨别,它的后驅(qū)(通常)會重連接到一個未被取消的前驅(qū)诗赌。
另外我們使用“next”連接去實(shí)現(xiàn)阻塞機(jī)制。每個節(jié)點(diǎn)的線程ID被它們自己的節(jié)點(diǎn)所持有秸弛,所以前驅(qū)節(jié)點(diǎn)通知下一個節(jié)點(diǎn)可以被喚醒铭若,這是通過遍歷下一個鏈接(即,next字段)來確定需要喚醒的線程递览。后繼節(jié)點(diǎn)的決定必須同‘新入隊的節(jié)點(diǎn)在設(shè)置它的前驅(qū)節(jié)點(diǎn)的“next”屬性操作(即叼屠,新入隊節(jié)點(diǎn)為newNode,在newNode的前驅(qū)節(jié)點(diǎn)preNewNode進(jìn)行preNewNode.next = newNode操作)’產(chǎn)生競爭绞铃。一個解決方法是必要的話當(dāng)一個節(jié)點(diǎn)的后繼看起來是空的時候镜雨,從原子更新“tail”向前檢測儿捧。(或者換句話說荚坞,next鏈接是一個優(yōu)化挑宠,所以我們通常不需要反向掃描。)
取消引入了對基本算法的一些保守性颓影。當(dāng)我們必須為其他節(jié)點(diǎn)的取消輪詢時各淀,我們不需要留意一個取消的節(jié)點(diǎn)是在我們節(jié)點(diǎn)的前面還是后面。它的處理方式是總是根據(jù)取消的節(jié)點(diǎn)喚醒其后繼節(jié)點(diǎn)诡挂,允許它們?nèi)ミB接到一個新的前驅(qū)節(jié)點(diǎn)碎浇,除非我們能夠標(biāo)識一個未被取消的前驅(qū)節(jié)點(diǎn)來完成這個責(zé)任。
- waitStatus
volatile int waitStatus;
狀態(tài)屬性璃俗,只有如下值:
① SIGNAL:
static final int SIGNAL = -1;
這個節(jié)點(diǎn)的后繼(或者即將被阻塞)被阻塞(通過park阻塞)了奴璃,所以當(dāng)前節(jié)點(diǎn)需要喚醒它的后繼當(dāng)它被釋放或者取消時。為了避免競爭旧找,獲取方法必須首先表示他們需要一個通知信號溺健,然后再原子性的嘗試獲取鎖,如果失敗钮蛛,則阻塞鞭缭。
也就是說,在獲取鎖的操作中魏颓,需要確保當(dāng)前node的preNode的waitStatus狀態(tài)值為’SIGNAL’岭辣,才可以被阻塞,當(dāng)獲取鎖失敗時甸饱。(『shouldParkAfterFailedAcquire』方法的用意就是這)
② CANCELLED:
static final int CANCELLED = 1;
這個節(jié)點(diǎn)由于超時或中斷被取消了沦童。節(jié)點(diǎn)不會離開(改變)這個狀態(tài)。尤其叹话,一個被取消的線程不再會被阻塞了偷遗。
③ CONDITION:
static final int CONDITION = -2;
這個節(jié)點(diǎn)當(dāng)前在一個條件隊列中。它將不會被用于當(dāng)做一個同步隊列的節(jié)點(diǎn)直到它被轉(zhuǎn)移到同步隊列中驼壶,轉(zhuǎn)移的同時狀態(tài)值(waitStatus)將會被設(shè)置為0氏豌。(這里使用這個值將不會做任何事情與該字段其他值對比,只是為了簡化機(jī)制)热凹。
④ PROPAGATE:
static final int PROPAGATE = -3;
一個releaseShared操作必須被廣播給其他節(jié)點(diǎn)泵喘。(只有頭節(jié)點(diǎn)的)該值會在doReleaseShared方法中被設(shè)置去確保持續(xù)的廣播,即便其他操作的介入般妙。
⑤ 0:不是上面的值的情況纪铺。
這個值使用數(shù)值排列以簡化使用。非負(fù)的值表示該節(jié)點(diǎn)不需要信號(通知)碟渺。因此鲜锚,大部分代碼不需要去檢查這個特殊的值,只是為了標(biāo)識。
對于常規(guī)的節(jié)點(diǎn)該字段會被初始化為0芜繁,競爭節(jié)點(diǎn)該值為CONDITION攒霹。這個值使用CAS修改(或者可能的話,無競爭的volatile寫)浆洗。
- prev
volatile Node prev
連接到前驅(qū)節(jié)點(diǎn),當(dāng)前節(jié)點(diǎn)/線程依賴與這個節(jié)點(diǎn)waitStatus的檢測集峦。分配發(fā)生在入隊時伏社,并在出隊時清空(為了GC)。并且塔淤,一個前驅(qū)的取消摘昌,我們將短路當(dāng)發(fā)現(xiàn)一個未被取消的節(jié)點(diǎn)時,未被取消的節(jié)點(diǎn)總是存在因?yàn)轭^節(jié)點(diǎn)不能被取消:只有在獲取鎖操作成功的情況下一個節(jié)點(diǎn)才會成為頭節(jié)點(diǎn)高蜂。一個被取消的線程絕不會獲取成功聪黎,一個線程只能被它自己取消,不能被其他線程取消备恤。
- next
volatile Node next
連接到后繼的節(jié)點(diǎn)稿饰,該節(jié)點(diǎn)是當(dāng)前的節(jié)點(diǎn)/線程釋放喚醒的節(jié)點(diǎn)。分配發(fā)生在入隊時露泊,在繞過取消的前驅(qū)節(jié)點(diǎn)時進(jìn)行調(diào)整喉镰,并在出隊列時清空(為了GC的緣故)。一個入隊操作(enq)不會被分配到前驅(qū)節(jié)點(diǎn)的next字段惭笑,直到tail成功指向當(dāng)前節(jié)點(diǎn)之后(通過CAS來將tail指向當(dāng)前節(jié)點(diǎn)侣姆。『enq』方法實(shí)現(xiàn)中沉噩,會先將node.prev = oldTailNode;在需要在CAS成功之后捺宗,即tail = node之后,再將oldTailNode.next = node;)川蒙,所以當(dāng)看到next字段為null時并不意味著當(dāng)前節(jié)點(diǎn)是隊列的尾部了蚜厉。無論如何,如果一個next字段顯示為null派歌,我們能夠從隊列尾向前掃描進(jìn)行復(fù)核弯囊。被取消的節(jié)點(diǎn)的next字段會被設(shè)置為它自己,而不是一個null胶果,這使得isOnSyncQueue方法更簡單匾嘱。
- thread
volatile Thread thread
這個節(jié)點(diǎn)的入隊線程。在構(gòu)建時初始化早抠,在使用完后清除霎烙。
- nextWaiter
Node nextWaiter
鏈接下一個等待條件的節(jié)點(diǎn),或者一個指定的SHARED值。因?yàn)橹挥谐钟信潘i時能訪問條件隊列悬垃,所以我們只需要一個簡單的單鏈表來維持正在等待條件的節(jié)點(diǎn)游昼。它們接下來會被轉(zhuǎn)換到隊列中以去重新獲取鎖。因?yàn)橹挥信潘i才有conditions尝蠕,所以我們使用給一個特殊值保存的字段來表示共享模式烘豌。
也就是說,nextWaiter用于在排他鎖模式下表示正在等待條件的下一個節(jié)點(diǎn)看彼,因?yàn)橹挥信潘i模式有conditions廊佩;所以在共享鎖模式下,我們使用’SHARED’這個特殊值來表示該字段靖榕。
源碼分析
初始化
CountDownLatch doneSignal = new CountDownLatch(N);
CountDownLatch 使用了共享鎖模式标锄。CountDownLatch 使用了一個內(nèi)部類 Sync來實(shí)現(xiàn)CountDownLatch的同步控制,而Sync是AQS的一個實(shí)現(xiàn)類茁计,它使用AQS的狀態(tài)(state)來表示count料皇。
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
構(gòu)造一個CountDownLatch使用給定的count值進(jìn)行初始化。
count值最終是設(shè)置到sync(AbstractQueuedSynchronizer)里的state字段星压。
阻塞的流程分析
『await()』
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
導(dǎo)致當(dāng)前的線程等待直到latch被倒數(shù)到0践剂,或者線程被中斷了。
如果當(dāng)前的count是0租幕,那么方法會立即返回舷手,并且返回值為true。
如果當(dāng)前的count大于0劲绪,則當(dāng)前線程因?yàn)榫€程調(diào)度而變得不可用男窟,并且處于休眠狀態(tài),直到發(fā)生下面二件事之一:
① 由于countDown方法的調(diào)用當(dāng)前的count達(dá)到0贾富;
如果count達(dá)到0歉眷,那么這個方法將返回true。
② 其他線程中斷了當(dāng)前的線程颤枪;
如果當(dāng)前線程在進(jìn)入這個方法時設(shè)置了中斷狀態(tài)汗捡;或者當(dāng)前線程在等待時被設(shè)置了中斷狀態(tài),那么“InterruptedException”異常將會拋出畏纲,并且當(dāng)前的線程的中斷狀態(tài)會被清除扇住。
『acquireSharedInterruptibly』
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
獲取一個共享模式鎖,如果發(fā)生中斷則異常終止盗胀。實(shí)現(xiàn)首先會檢查中斷的狀態(tài)艘蹋,然后執(zhí)行至少一次的tryAcquireShared,成功的話返回票灰。否則女阀,線程將會入隊宅荤,可能會重復(fù)的阻塞和解阻塞,執(zhí)行tryAcquireShared直到成功或者線程被中斷浸策。
① 首先判斷當(dāng)前的線程是否被標(biāo)志為了中斷冯键,如果被標(biāo)志位了中斷,則拋出“InterruptedException”異常庸汗,并清除中斷標(biāo)志惫确;否則到第②步;
② 執(zhí)行『tryAcquireShared』來嘗試獲取鎖蚯舱,如果成功(即雕薪,返回>=0)。則返回true退出方法晓淀;否則到第③步
③ 執(zhí)行doAcquireSharedInterruptibly。
『doAcquireSharedInterruptibly』
② 獲取新創(chuàng)建好節(jié)點(diǎn)的前驅(qū)節(jié)點(diǎn)。如果前驅(qū)節(jié)點(diǎn)是head節(jié)點(diǎn)蜈亩,則說明當(dāng)前節(jié)點(diǎn)是隊列中第一個等待獲取鎖的節(jié)點(diǎn)懦窘,那么就執(zhí)行『tryAcquireShared』方法嘗試獲取共享鎖。tryAcquireShared是由CountDownLatch重寫的方法稚配。具體實(shí)現(xiàn)下面會詳細(xì)說明畅涂。這里先給出結(jié)果就是tryAcquireShared方法的返回值會小于0.也就說獲取共享鎖失敗。進(jìn)入步驟③
③ 如果前驅(qū)節(jié)點(diǎn)不是head節(jié)點(diǎn)道川,或者當(dāng)前節(jié)點(diǎn)獲取共享鎖失斘缢ァ(即,步驟②)冒萄。那么執(zhí)行『shouldParkAfterFailedAcquire』方法臊岸,該方法返回true則說明本次獲取共享鎖失敗需要阻塞(掛起)當(dāng)前線程。接著執(zhí)行『parkAndCheckInterrupt』方法尊流,該方法會將當(dāng)前線程掛起帅戒,直到被喚醒。
這就是阻塞情況下的一個主流程崖技,可以知道的是逻住,在這個邏輯過程中使用了大量的CAS來進(jìn)行原子性的修改,當(dāng)修改失敗的時候迎献,是會通過for(;;)來重新循環(huán)的瞎访,也就是說『doAcquireSharedInterruptibly』使用自旋鎖(自旋+CAS)來保證在多線程并發(fā)的情況下,隊列節(jié)點(diǎn)狀態(tài)也是正確的以及在等待隊列的正確性忿晕,最終使得當(dāng)前節(jié)點(diǎn)要么獲取共享鎖成功装诡,要么被掛起等待喚醒银受。
下面我們來對阻塞情況下,涉及的方法進(jìn)行進(jìn)一步的展開鸦采。
『addWaiter』
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
根據(jù)給定的模式創(chuàng)建當(dāng)前線程的節(jié)點(diǎn)宾巍,并將創(chuàng)建好的節(jié)點(diǎn)入隊(加入等待隊列尾部)。
首先在隊列非空的情況下會嘗試一次快速入隊渔伯,也就是通過嘗試一次CAS操作入隊顶霞,如果CAS操作失敗,則調(diào)用enq方法進(jìn)行“自旋+CAS”方法將創(chuàng)建好的節(jié)點(diǎn)加入隊列尾锣吼。
在共享模式下选浑,Node的mode(即,waitStatus)為’SHARED’玄叠。waitStatus是用于在排他鎖模式下當(dāng)節(jié)點(diǎn)處于條件隊列時表示下一個等待條件的節(jié)點(diǎn)古徒,所以在共享鎖模式下,我們使用’SHARED’這個特殊值來表示該字段读恃。
『enq』
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
使用自旋鎖的方式(自旋+CAS)插入節(jié)點(diǎn)到等待隊列隧膘,如果等待隊列為空則初始化隊列。
初始化隊列:創(chuàng)建一個空節(jié)點(diǎn)(即寺惫,new Node())疹吃,將head和tail都指向這個節(jié)點(diǎn)。
然后才是將我們待插入的節(jié)點(diǎn)插入西雀,即:emptyNode -> newNode. head指向emptyNode萨驶,tail指向newNode。
『tryAcquireShared』
在共享模式下嘗試獲取艇肴。這個方法需要查詢是否對象的狀態(tài)允許在共享模式下被獲取腔呜,如果允許則去獲取它。
這個方法總是被線程執(zhí)行獲取共享鎖時被調(diào)用再悼。如果這個方法報告失敗育谬,那么獲取方法可能會使線程排隊等待,如果它(即帮哈,線程)還沒入隊的話膛檀,直到其他的線程發(fā)出釋放的信號。
默認(rèn)實(shí)現(xiàn)拋出一個“UnsupportedOperationException”
返回:
a)< 0 : 一個負(fù)數(shù)的返回表示失斈锸獭咖刃;
b) 0 : 0表示在共享模式下獲取鎖成功,但是后續(xù)的獲取共享鎖將不會成功
c)> 0 : 大于0表示共享模式下獲取鎖成功憾筏,并且后續(xù)的獲取共享鎖可能也會成功嚎杨,在這種情況下后續(xù)等待的線程必須檢查是否有效。
CountDownLatch對該方法進(jìn)行了重寫:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
如果當(dāng)前的狀態(tài)值為0(即氧腰,count為0)枫浙,則表示獲取成功(返回’1’)刨肃;否則表示獲取失敿粮(返回’-1’)
『shouldParkAfterFailedAcquire』
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
檢查并修改一個節(jié)點(diǎn)的狀態(tài)黑滴,當(dāng)該節(jié)點(diǎn)獲取鎖失敗時。返回true如果線程需要阻塞挪鹏。這是主要的信號通知控制在所有的獲取鎖循環(huán)中紧帕。要求’pred’ == ‘node.prev’
① 如果pred.waitStatus == Node.SIGNAL盔然。則說明node的前驅(qū)節(jié)點(diǎn)已經(jīng)被要求去通知釋放它的后繼節(jié)點(diǎn),所以node可以安全的被掛起(park)是嗜。然后愈案,退出方法,返回true鹅搪。
② 如果pred.waitStatus > 0站绪。則說明node的前驅(qū)節(jié)點(diǎn)被取消了。那么跳過這個前驅(qū)節(jié)點(diǎn)并重新標(biāo)志一個有效的前驅(qū)節(jié)點(diǎn)(即丽柿,waitStatus <= 0 的節(jié)點(diǎn)可作為有效的前驅(qū)節(jié)點(diǎn))崇众,然后,退出方法航厚,返回false。
③ 其他情況下锰蓬,即pred.waitStatus為’0’或’PROPAGATE’幔睬。表示我們需要一個通知信號(即,當(dāng)前的node需要喚醒的通知)芹扭,但是當(dāng)前還不能掛起node麻顶。調(diào)用『compareAndSetWaitStatus(pred, ws, Node.SIGNAL)』方法通過CAS的方式來修改前驅(qū)節(jié)點(diǎn)的waitStatus為“SIGNAL”。退出方法舱卡,返回false辅肾。
我們需要一個通知信號,主要是因?yàn)楫?dāng)前線程要被掛起了(park)轮锥。而如果waitStatus已經(jīng)是’SIGNAL’的話就無需修改矫钓,直接掛起就好,而如果waitStatus是’CANCELLED’的話舍杜,說明prev已經(jīng)被取消了新娜,是個無效節(jié)點(diǎn)了,那么無需修改這個無效節(jié)點(diǎn)的waitStatus既绩,而是需要先找到一個有效的prev概龄。因此,剩下的情況就只有當(dāng)waitStatus為’0’和’PROPAGAET’了(注意饲握,waitStatus為’CONDITION’是節(jié)點(diǎn)不在等待隊列中私杜,所以當(dāng)下情況waitStatus不可能為’CONDITION’)蚕键,這是我們需要將prev的waitStatus使用CAS的方式修改為’SIGNAL’,而且只有修改成功的情況下衰粹,當(dāng)前的線程才能安全被掛起锣光。
還值得注意的時,因此該方法的CAS操作都是沒有自旋的寄猩,所以當(dāng)它操作完CAS后都會返回false嫉晶,在外層的方法中會使用自旋,當(dāng)發(fā)現(xiàn)返回的是false時田篇,會再次調(diào)用該方法替废,以檢查保證有當(dāng)前node有一個有效的prev,并且其waitStatus為’SIGNAL’泊柬,在此情況下當(dāng)前的線程才會被掛起(park)椎镣。
釋放的流程分析
『countDown』
public void countDown() {
sync.releaseShared(1);
}
減小latch的count,如果count達(dá)到0則釋放所有正在等待的線程兽赁。
如果當(dāng)前的count大于0状答,那么減少count。如果減少后的count值為0刀崖,那么所有正在等待的線程因?yàn)榫€程調(diào)度的原因被重新啟用惊科。
如果當(dāng)前的count值已經(jīng)是0了,那么什么都不會發(fā)生亮钦。
『releaseShared』
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
共享模式下的釋放馆截。如果『tryReleaseShared』返回true的話,會使一個或多個線程重新啟動蜂莉。
『tryReleaseShared』
在共享模式下蜡娶,嘗試去設(shè)置狀態(tài)來反映一個釋放。
這個方法總是在線程執(zhí)行釋放時被調(diào)用映穗。
默認(rèn)實(shí)現(xiàn)拋出一個UnsupportedOperationException異常窖张。
返回:如果當(dāng)前共享模式可能允許一個正在等待的獲取成功(正在等待的獲取可能是共享模式的,也可能是排他模式的)蚁滋,則返回true宿接;否則,返回false辕录。
CountDownLatch對該方法進(jìn)行了重寫:
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;
}
}
減少count的值澄阳,如果count為0則發(fā)出釋放信號。
這里使用了"自旋+CAS”的方式來原子性的將state的值減少1踏拜,如果在此過程中state已經(jīng)為0了(在并發(fā)情況下碎赢,可能已經(jīng)被其他線程修改為了0),則返回false速梗。否則根據(jù)修改后state的值是否等于0來返回boolean值肮塞。
『doReleaseShared』
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
共享模式下的釋放動作 ———— 用信號通知后繼節(jié)點(diǎn)并且確保廣播襟齿。(注意:在排他鎖模式下,釋放只是相當(dāng)于調(diào)用head的unparkSuccessor方法如果它需要通知喚醒的話枕赵。)
確保一個釋放的廣播猜欺,即使有其他線程正在進(jìn)行獲取/釋放鎖。這個過程通常的方式是嘗試head的unparkSuccessor操作如果需要通知釋放的話拷窜。如果沒這么做开皿,狀態(tài)會被設(shè)置為‘PROPAGATE’以確保在釋放,廣播繼續(xù)篮昧。此外赋荆,當(dāng)我們正在做這個操作的時候如果新的節(jié)點(diǎn)被添加的話,我們需要重新循環(huán)再進(jìn)行一次該操作懊昨。另外窄潭,不同于unparkSuccessor的其他用途,我們需要知道CAS重置狀態(tài)是否失敗酵颁,如果失敗則重新檢查嫉你。
在隊列非空的時候,該方法會釋放head的后繼節(jié)點(diǎn)躏惋,如果該節(jié)點(diǎn)可以被釋放的話幽污。『(h != null && h != tail)』表示隊列非空簿姨,即有等待獲取鎖的節(jié)點(diǎn)距误;『(h == head)』表示,已經(jīng)操作完釋放后繼節(jié)點(diǎn)款熬,或者隊列已經(jīng)空了(即,『(h == null || h == tail)』)攘乒,那么就退出循環(huán)贤牛。否則如果循環(huán)過程中(即,『h != head』)则酝,頭結(jié)點(diǎn)發(fā)生了變化殉簸,則重新循環(huán)。
如果『if (h != null && h != tail)』為true沽讹,那么:
① 如果head的waitStatus為’SIGNAL’般卑,則說明head的后繼節(jié)點(diǎn)可被通知釋放,那么執(zhí)行CAS操作將head.waitStatus修改為’0’爽雄,如果成功蝠检,則執(zhí)行『unparkSuccessor』對head的后繼節(jié)點(diǎn)進(jìn)行釋放操作,如果CAS操作失敗挚瘟,則說明發(fā)送了多線程競爭(即叹谁,此時有其他線程也在修改head的waitStatus狀態(tài)值)饲梭,那么重新循環(huán)檢查。
② 如果head的waitStatus為’0’焰檩,則使用CAS的方式將其修改為’PROPAGATE’憔涉。如果CAS操作失敗,則說明發(fā)生了多線程競爭析苫,那么重新循環(huán)檢查兜叨。
③ 如果上面的兩個操作中有一個成功了,就會走到“if (h == head)”這一步衩侥,并且此時head節(jié)點(diǎn)沒有發(fā)生變化国旷,則退出循環(huán),操作結(jié)束顿乒。否則议街,說明head節(jié)點(diǎn)發(fā)生變化了,那么重新循環(huán)檢查璧榄。
『if (h != null && h != tail)』為false特漩,那么:
說明隊列中沒有等到獲取鎖的節(jié)點(diǎn)。會直接到“if (h == head)”骨杂,如果此時head節(jié)點(diǎn)沒有發(fā)生變化涂身,則直接退出循環(huán),操作結(jié)束搓蚪。如果此時head節(jié)點(diǎn)發(fā)生了變化蛤售,那么重新循環(huán)檢查。
也就是說妒潭,該方法在等待隊列非空時(即悴能,存在一個有效的等待節(jié)點(diǎn),頭結(jié)點(diǎn)不是有效節(jié)點(diǎn))雳灾,會根據(jù)head的waitStatus進(jìn)行后續(xù)的操作漠酿。
a) 如果『ws == Node.SIGNAL』,則說明需要釋放head后繼節(jié)點(diǎn)谎亩,如果此時CAS操作『compareAndSetWaitStatus(h, Node.SIGNAL, 0)』也成功的話(說明炒嘲,此時沒有其他線程在修改head的waitStatus),那么就會執(zhí)行『unparkSuccessor(h);』來釋放head的后繼節(jié)點(diǎn)匈庭。
b) 如果『ws != Node.SIGNAL』并且『ws == 0』夫凸,則通過CAS操作將head的waitStatus修改為’PROPAGATE’。
以上兩步阱持,當(dāng)CAS失敗夭拌,也就是有其他線程也在修改head的waitStatus狀態(tài)時,需要繼續(xù)循環(huán)進(jìn)行重新檢測,如果head節(jié)點(diǎn)改變了也需要繼續(xù)循環(huán)重新檢測啼止。
Q:關(guān)于node的waitStatus為’0’的情況道逗?
A:當(dāng)節(jié)點(diǎn)不屬于任何waitStatus的話,就會是0献烦。比如滓窍,創(chuàng)建好的節(jié)點(diǎn)。比如巩那,原來是SIGNAL狀態(tài)吏夯,在執(zhí)行完unparkSuccessor操作后(邏輯上說是執(zhí)行完unparkSuccessor后,但實(shí)際的代碼實(shí)現(xiàn)必須先將node的waitStatus通過CAS成功從SINGAL修改為0后即横,才可執(zhí)行unparkSuccessor操作噪生,以保證多線程競爭情況下的正確性)。比如东囚,將節(jié)點(diǎn)從條件隊列轉(zhuǎn)移到等待隊列的時候跺嗽,會通過CAS將node的waitStatus從’CONDITION’修改為0。
Q:’PROPAGATE’狀態(tài)與釋放之間的關(guān)系页藻?
A:當(dāng)head的waitStatus為’PROPAGATE’的話桨嫁,在釋放操作時,這個釋放會被廣播下去份帐,也就是說璃吧,第一個線程被釋放完后,會繼續(xù)釋放第二個被阻塞的線程废境。畜挨。。
『unparkSuccessor』
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
喚醒后繼節(jié)點(diǎn)噩凹,如果存在的話
① 如果狀態(tài)值是負(fù)數(shù)巴元,則在預(yù)期發(fā)信號通知時清除這個負(fù)數(shù)狀態(tài)值。如果狀態(tài)被等待的線程修改了或者清除負(fù)數(shù)狀態(tài)值失敗是允許驮宴。
② 后繼節(jié)點(diǎn)的線程被喚醒逮刨,后繼節(jié)點(diǎn)通常就是下一個節(jié)點(diǎn)。但是如果下一個節(jié)點(diǎn)被取消了或者下一個節(jié)點(diǎn)為null幻赚,則從隊列尾(tail)往前遍歷去找真實(shí)的未取消的后繼節(jié)點(diǎn)禀忆。
『(s == null || s.waitStatus > 0)』:說明下一個節(jié)點(diǎn)為null或被取消了(waitStatus允許的狀態(tài)值中臊旭,只有’CANCELLED’是>0的)落恼。那么,就從隊列尾(tail)開始向前遍歷离熏,獲取第一個非空且未被取消的節(jié)點(diǎn)佳谦。如果存在這樣的一個后繼節(jié)點(diǎn)的話(即,“s != null”)滋戳,則執(zhí)行『LockSupport.unpark(s.thread);』操作來喚醒這個節(jié)點(diǎn)的線程钻蔑。
Q:關(guān)于node的waitStatus為’CANCELLED’的情況啥刻?
A:關(guān)于node的waitStatus為’CANCELLED’的情況:比如,當(dāng)這個node被中斷了咪笑,或者設(shè)置的超時時間到了可帽,那么說明這個線程獲取鎖失敗,那么此時就應(yīng)該將其設(shè)置為cancelled窗怒,因?yàn)槿绻摼€程還需要獲取鎖的話映跟,會重新調(diào)用獲取鎖的方法,而獲取鎖的方法就是創(chuàng)建一個新的node的扬虚。所以努隙,那么線程獲取鎖失敗的時候就會將這個node的waitStatus設(shè)置為’CANCELLED’,一個被取消的線程絕不會獲取鎖成功辜昵,一個線程只能被它自己取消荸镊,不能被其他線程取消。
Q:關(guān)于node為null的情況堪置?
A:關(guān)于node為null的情況:比如躬存,一個入隊操作(enq)不會被分配到前驅(qū)節(jié)點(diǎn)的next字段,直到tail成功指向當(dāng)前節(jié)點(diǎn)之后(通過CAS來將tail指向當(dāng)前節(jié)點(diǎn)晋柱∮殴梗『enq』方法實(shí)現(xiàn)中,會先將node.prev = oldTailNode;在需要在CAS成功之后雁竞,即tail = node之后钦椭,再將oldTailNode.next = node;),所以當(dāng)看到next字段為null時并不意味著當(dāng)前節(jié)點(diǎn)是隊列的尾部了碑诉。無論如何彪腔,如果一個next字段顯示為null,我們能夠從隊列尾向前掃描進(jìn)行復(fù)核进栽。
當(dāng)調(diào)用了『LockSupport.unpark(s.thread);』操作后德挣,等待隊列中第一個等待的線程就會重新啟動。流程回到『doAcquireSharedInterruptibly』方法中快毛,線程從阻塞中恢復(fù):
『setHeadAndPropagate』
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
設(shè)置’node’節(jié)點(diǎn)為頭結(jié)點(diǎn)劳澄,并且檢查’node’節(jié)點(diǎn)的后繼是否正在等待獲取共享鎖,如果是的話秒拔,并且'propagate > 0'或者’node’的waitStatus被設(shè)置成了’PROPAGATE’莫矗,則廣播。
① 設(shè)置’node’為head節(jié)點(diǎn)
② 嘗試通知隊列中的下一個節(jié)點(diǎn)砂缩,如果:
??[1]
???a) 調(diào)用者標(biāo)識了廣播(即作谚,propagate > 0),
???b) 或者waitStatus被前面的操作重新記錄了(’h.waitStatus’可能在setHead之前或之后被重新記錄)(注意庵芭,這里使用waitStatus的符號檢查妹懒,因?yàn)镻ROPAGATE狀態(tài)可能被轉(zhuǎn)換為SIGNAL)。
??并且[2]隊列中下一個等待的節(jié)點(diǎn)是共享模式的双吆,或者下一個節(jié)點(diǎn)為null眨唬。
這兩次檢查的保守性可能導(dǎo)致不必要的喚醒,但是只有當(dāng)多線程競爭獲取/釋放鎖時好乐,所以大多數(shù)情況下現(xiàn)在或即將需要通知(signal)喚醒匾竿。(因?yàn)樵趀nq新節(jié)點(diǎn)入隊過程中,可能出現(xiàn)next為null的短暫現(xiàn)象蔚万,這是發(fā)現(xiàn)在節(jié)點(diǎn)入隊的過程中岭妖,隨后節(jié)點(diǎn)就會入隊成功,next字段就不會為null了反璃。所以這里將next為null的情況也考慮了昵慌,在廣播釋放時,會將這個正在入隊的節(jié)點(diǎn)對應(yīng)的線程也進(jìn)行釋放)淮蜈。
如果符合??[1]斋攀、[2]個條件則執(zhí)行『doReleaseShared()』來釋放后繼的節(jié)點(diǎn)。
可設(shè)置超時時間的await
『await(long timeout, TimeUnit unit)』同『await()』方法大體是相同的礁芦,主要多了在獲取共享鎖時對時間的控制蜻韭。
在嘗試獲取鎖時的區(qū)別:
① 如果傳入的給定的超時納秒數(shù)是否小于等于0,如果是則直接返回false柿扣,獲取共享鎖失敗肖方。
② 如果在使用自旋的方式獲取共享鎖的過程中,發(fā)現(xiàn)已經(jīng)過了設(shè)置的超時時間未状,那么直接返回false俯画,獲取共享鎖失敗。
③ 如果當(dāng)前線程無法獲取當(dāng)共享鎖司草,并且『shouldParkAfterFailedAcquire』方法返回true(則說明本次獲取共享鎖失敗需要阻塞/掛起當(dāng)前線程)艰垂。但當(dāng)『nanosTimeout <= spinForTimeoutThreshold』說明設(shè)置的超時時間 <= 自旋超時的閾值。這里spinForTimeoutThreshold的值為1000納秒埋虹,表示當(dāng)設(shè)置的超時時間小于1000納秒時猜憎,使用自旋比使用線程掛起更快。粗略估算這足以去提升響應(yīng)在一個很短的超時時間內(nèi)搔课。否則也是使用『LockSupport.parkNanos(this, nanosTimeout);』將當(dāng)前線程掛起胰柑,直到被喚醒或者超時時間到。
取消節(jié)點(diǎn)
當(dāng)嘗試獲取鎖的節(jié)點(diǎn)爬泥,因?yàn)槌瑫r或中斷而結(jié)束時柬讨,說明本次獲取鎖操作失敗,因?yàn)楸敬尾僮鞯膎ode就應(yīng)該被取消袍啡。如果線程還需要獲取鎖的話踩官,會再次嘗試獲取鎖操作,此時如果需要的話是會生成一個新的node的境输。
『cancelAcquire』
private void cancelAcquire(Node node) {
// Ignore if node doesn't exist
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
① 如果待取消節(jié)點(diǎn)(node)為null蔗牡,則直接返回。
② 將node的thread置為null嗅剖;
③ 將node的prev屬性指向一個在它之前的有效的節(jié)點(diǎn)(即蛋逾,waitStatus <= 0的節(jié)點(diǎn)都為有效節(jié)點(diǎn))。 也就是跳過被取消的前驅(qū)節(jié)點(diǎn)窗悯。
④ 『Node predNext = pred.next;』取pred的下一個節(jié)點(diǎn)区匣。這個predNext是pred表面上的下一個連接的節(jié)點(diǎn)(即,無需考慮該節(jié)點(diǎn)是否被取消了)蒋院。下面的CAS操作將會失斂鞴场(『compareAndSetNext(pred, predNext, null);』or『compareAndSetNext(pred, predNext, next);』),如果和其他的取消或通知操作發(fā)生競爭時欺旧,這時不需要進(jìn)一步的操作姑丑。因?yàn)槿绻a(chǎn)生競爭,說明pred的next已經(jīng)被修改了辞友,并且是最新的值了栅哀,而我們的操作也就沒有要執(zhí)行的必要了震肮。
⑤ 將node的waitStatus設(shè)置為’CANCELLED’。這里可以使用無條件的寫代替CAS(注意留拾,node的waitStatus是volatile的)戳晌。在這個原子操作之后,其他節(jié)點(diǎn)會跳過我們(即痴柔,跳過waitStatus被置位CANCELLED的節(jié)點(diǎn))沦偎,在這個原子操作之前,我們不受其他線程的干擾咳蔚。也就是說豪嚎,無論其他線程對node的waitStatus是否有在操作,在當(dāng)前的情況下我們都需要將這個node的waitStatus置為’CANCELLED’谈火。
⑥ 如果待取消的node節(jié)點(diǎn)是隊列尾節(jié)點(diǎn)的話(即侈询,『node == tail』),那么刪除node自己即可糯耍。使用CAS將tail節(jié)點(diǎn)設(shè)置成前面得到的第一個有效前驅(qū)節(jié)點(diǎn)(即妄荔,『compareAndSetTail(node, pred)』)。并且CAS操作成功的話谍肤,執(zhí)行『compareAndSetNext(pred, predNext, null);』也就是將tail的next置為null的意思啦租。如果該CAS操作失敗的話,沒關(guān)系荒揣。說明此時tail已經(jīng)被修改了篷角。
⑦ 如果待取消的node節(jié)點(diǎn)不是隊尾節(jié)點(diǎn)。并且:
a)pred(即系任,node的有效前驅(qū)節(jié)點(diǎn))不是head節(jié)點(diǎn)恳蹲;并且
b)“pred.waitStatus為SIGNAL” 或者 “pred.waitStatus <= 0”時通過CAS將pred.waitStatus設(shè)置為SIGNAL”成功;并且
c) pred的thread非空
那么俩滥,當(dāng)node的next節(jié)點(diǎn)非空嘉蕾,且next節(jié)點(diǎn)的waitStatus<=0(說明next節(jié)點(diǎn)未被取消)時,通過CAS將pred的next執(zhí)行node的next(即霜旧,pred.next = node.next)错忱。同時,如果該CAS操作失敗是沒關(guān)系的挂据,說明有其他線程操作已經(jīng)修改了該pre的next值以清。
⑧ 如果待取消的node節(jié)點(diǎn)不是隊尾節(jié)點(diǎn),并且步驟[7]條件不成立崎逃。那么執(zhí)行『unparkSuccessor(node);』來釋放當(dāng)前這個待取消節(jié)點(diǎn)的下一個節(jié)點(diǎn)掷倔。(也就是說,當(dāng)prev是head節(jié)點(diǎn)个绍,或者prev也被取消的話勒葱,會執(zhí)行『unparkSuccessor(node);』來釋放node的下一個節(jié)點(diǎn)浪汪,其實(shí)也就是pred的下一個節(jié)點(diǎn))
從上面的分析我們可以知道,其實(shí)CountDownLatch中線程的釋放其實(shí)是有順序的凛虽,根據(jù)節(jié)點(diǎn)入隊的順序依次被釋放死遭,先入隊的節(jié)點(diǎn)的線程會先被釋放。
后記
如果文章有錯不吝指教 :)