上一篇我們通過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)的線程都將獲取成功。