併發——深入分析CountDownLatch的實現原理
一、前言
最近在研究java.util.concurrent
包下的一些的常用類,之前寫了AQS
、ReentrantLock
、ArrayBlockingQueue
以及LinkedBlockingQueue
的相關部落格,今天這篇部落格就來寫一寫併發包下的另一個常用類——CountDownLatch
。這裡首先要說明一點,CountDownLatch
是基於AQS
實現的,AQS
才是真正實現了執行緒同步的元件,CountDownLatch
只是它的使用者,所以如果想要學習CountDownLatch,請一定先要弄懂AQS的實現原理。我以下的描述均建立在已經瞭解AQS
的基礎之上。我之前寫過一篇AQS
實現原理的分析部落格,感興趣可以看一看:併發——抽象佇列同步器AQS的實現原理。
二、正文
2.1 抽象佇列同步器AQS
在說CountDownLatch
前,必須要先提一下AQS
。AQS
全稱抽象佇列同步器(AbstractQuenedSynchronizer),它是一個可以用來實現執行緒同步的基礎框架。當然,它不是我們理解的Spring
這種框架,它是一個類,類名就是AbstractQuenedSynchronizer
,如果我們想要實現一個能夠完成執行緒同步的鎖或者類似的同步元件,就可以在使用AQS
來實現,因為它封裝了執行緒同步的方式,我們在自己的類中使用它,就可以很方便的實現一個我們自己的鎖。
AQS
的實現相對複雜,無法通過短短的幾句話將其說清楚,我之前專門寫過一篇分析AQS
在閱讀下面的內容前,請一定要先學習AQS的實現原理,因為CountDownLatch
的實現非常簡單,完全就是依賴於AQS
的,所以我以下的描述均建立在已經理解AQS
的基礎之上。可以閱讀上面推薦部落格,也可以自己去查閱相關資料。
2.2 CountDownLatch的實現原理
既然已經開始學習CountDownLatch
的實現原理了,那一定已經知道了它的作用,我這裡就不詳細展示了,簡單介紹一下:CountDownLatch
的被稱為門栓,可以將它看成是門上的鎖,它會給門上多把鎖,只有每一把鎖都解開,才能通過。對於執行緒來說,CountDownLatch
CountDownLatc
內部記錄的值減小為0
,執行緒才能繼續向前執行。
CountDownLatch
底層通過AQS
實現,AQS
的一般使用方式就是以內部類的形式繼承它,CountDownLatch
就是這麼使用它的。在CountDownLatch
內部有一個內部類Sync
,繼承自AQS
,並重寫了AQS
加鎖解鎖的方法,並通過Sync
的物件,呼叫AQS
的方法,阻塞執行緒的執行。我們知道,建立一個CountDownLatch
物件時,需要傳入一個整數值count
,只有當count
被減小為0
時執行緒才能通過await
方法,否則將被await
阻塞。這裡實際上是這樣的:當執行緒執行到await方法時,需要去獲取鎖(鎖由AQS實現),若count不為0,則執行緒就會獲取鎖失敗,被阻塞;若count為0,則就能順利通過。CountDownLatch
是一次性的,因為沒有方法可以增加count
的值,也就是說,一旦count
被減小為0
,則之後就一直是0
了,也就再也不能阻塞執行緒了。下面我們就從原始碼的角度來分析CountDownLatch
。
2.3 CountDownLatch的內部類
前面我們說過,CountDownLatch
內部定義了一個內部類Sync
,繼承自AQS
,通過這個內部類來實現執行緒阻塞,下面我們就來看一看這個內部類的實現:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
/** 構造方法,接收count值,只有count減小為0時,執行緒才不會被await方法阻塞 */
Sync(int count) {
// CountDownLatch利用AQS的方式就是直接讓count作為AQS的同步變數state
// 所以直接用state記錄count值
setState(count);
}
/** 獲取當前的count值 */
int getCount() {
return getState();
}
/**
* 這是AQS的模板方法acquireShared、acquireSharedInterruptibly等方法內部將會呼叫的方法,
* 由子類實現,這個方法的作用是嘗試獲取一次共享鎖,對於AQS來說,
* 此方法返回值大於等於0,表示獲取共享鎖成功,反之則獲取共享鎖失敗,
* 而在這裡,實際上就是判斷count是否等於0,執行緒能否向下執行
*/
protected int tryAcquireShared(int acquires) {
// 此處判斷state的值是否為0,也就是判斷count是否為0,
// 若count為0,返回1,表示獲取鎖成功,此時執行緒將不會阻塞,正常執行
// 若count不為0,則返回-1,表示獲取鎖失敗,執行緒將會被阻塞
// 從這裡我們已經可以看出CountDownLatch的實現方式了
return (getState() == 0) ? 1 : -1;
}
/**
* 此方法的作用是用來是否AQS的共享鎖,返回true表示釋放成功,反之則失敗
* 此方法將會在AQS的模板方法releaseShared中被呼叫,
* 在CountDownLatch中,這個方法用來減小count值
*/
protected boolean tryReleaseShared(int releases) {
// 使用死迴圈不斷嘗試釋放鎖
for (;;) {
// 首先獲取當前state的值,也就是count值
int c = getState();
// 若count值已經等於0,則不能繼續減小了,於是直接返回false
// 為什麼返回的是false,因為等於0表示之前等待的那些執行緒已經被喚醒了,
// 若返回true,AQS會嘗試喚醒執行緒,若返回false,則直接結束,所以
// 在沒有執行緒等待的情況下,返回false直接結束是正確的
if (c == 0)
return false;
// 若count不等於0,則將其-1
int nextc = c-1;
// compareAndSetState的作用是將count值從c,修改為新的nextc
// 此方法基於CAS實現,保證了操作的原子性
if (compareAndSetState(c, nextc))
// 若nextc == 0,則返回的是true,表示已經沒有鎖了,執行緒可以運行了,
// 若nextc > 0,則表示執行緒還需要繼續阻塞,此處將返回false
return nextc == 0;
}
}
}
可以看到,內部類Sync的實現非常簡單,它只實現了AQS
中的兩個方法,即tryAcquireShared以及tryReleaseShared,這兩個方法是AQS
提供的使用共享鎖的介面。這也就表明,CountDownLatch
實際上是一種共享鎖機制,即鎖可以同時被多個執行緒獲取,這個不難理解,因為一旦count
被減小為0,則所有執行緒通過await
方法時,都能夠順利通過,不會因為獲取不到鎖而阻塞。而且從上面的實現中我們可以看到,Sync
直接將count
值作為AQS
的state
的值,只有state
的值為0,執行緒才能獲取鎖,也就是獲得執行許可權。
2.4 CountDownLatch的成員變數和構造方法
下面來看一看CountDownLatch
的屬性和構造方法:
/**
* 只有一個成員變數,就是內部類Sync的一個物件,通過此物件呼叫AQS的方法,實現執行緒阻塞和喚醒
*/
private final Sync sync;
/**
* 只有一個構造方法,接收一個count值
*/
public CountDownLatch(int count) {
// count值不能小於0
if (count < 0) throw new IllegalArgumentException("count < 0");
// 直接建立一個Sync物件,並傳入count值,Sync內部將會執行setState(count)
this.sync = new Sync(count);
}
2.5 await方法分析
CountDownLatch
類最最核心的兩個方法就是await
以及ountDown
,我們先來看一看await
方法的實現:
// 此方法用來讓當前執行緒阻塞,直到count減小為0才恢復執行
public void await() throws InterruptedException {
// 這裡直接呼叫sync的acquireSharedInterruptibly方法,這個方法定義在AQS中
// 方法的作用是嘗試獲取共享鎖,若獲取失敗,則執行緒將會被加入到AQS的同步佇列中等待
// 直到獲取成功為止。且這個方法是會響應中斷的,執行緒在阻塞的過程中,若被其他執行緒中斷,
// 則此方法會通過丟擲異常的方式結束等待。
sync.acquireSharedInterruptibly(1);
}
await
的實現異常簡單,只有短短一行程式碼,呼叫了AQS
中已經封裝好的方法。這就是AQS
的好處,AQS
已經實現了執行緒的阻塞和喚醒機制,將實現的複雜性隱藏,而其他類只需要簡單的使用它即可。為了方便理解,我們還是來看看acquireSharedInterruptibly
方法吧:
/** 此方法是AQS中提供的一個模板方法,用以獲取共享鎖,並且會響應中斷 */
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 首先判斷當前執行緒釋放被中斷,若被中斷,則直接丟擲異常結束
if (Thread.interrupted())
throw new InterruptedException();
// 呼叫tryAcquireShared方法嘗試獲取鎖,這個方法被Sycn類重寫了,
// 若count == 0,則這個方法會返回1,表示獲取鎖成功,則這裡會直接返回,執行緒不會被阻塞
// 若count < 0,將會執行下面的doAcquireSharedInterruptibly方法,
// 此處請去檢視Sync中tryAcquireShared方法的實現
if (tryAcquireShared(arg) < 0)
// 下面這個方法的作用是,執行緒獲取鎖失敗,將會加入到AQS的同步佇列中阻塞等待,
// 直到成功獲取到鎖,而此處成功獲取到鎖的條件就是count == 0,若當前執行緒在等待的過程中,
// 成功地獲取了鎖,則它會繼續喚醒在它後面等待的執行緒,也嘗試獲取鎖,
// 這也就是說,只要count == 0了,則所有被阻塞的執行緒都能恢復執行
doAcquireSharedInterruptibly(arg);
}
相信看到這裡,對CountDownLatch
的實現原理已經有一個比較清晰的理解了。CountDownLatch
的實現完全就是依賴於AQS
的,所有再次提醒,如果以上內容理解不了,請先去學習AQS
。
2.6 countDown方法分析
下面我們來分析CountDownLatch
中另一個核心的方法——countDown
,
/**
* 此方法的作用就是將count的值-1,如果count等於0了,就喚醒等待的執行緒
*/
public void countDown() {
// 這裡直接呼叫sync的releaseShared方法,這個方法的實現在AQS中,也是AQS提供的模板方法,
// 這個方法的作用是當前執行緒釋放鎖,若釋放失敗,返回false,若釋放成功,則返回false,
// 若鎖被釋放成功,則當前執行緒會喚醒AQS同步佇列中第一個被阻塞的執行緒,讓他嘗試獲取鎖
// 對於CountDownLatch來說,釋放鎖實際上就是讓count - 1,只有當count被減小為0,
// 鎖才是真正被釋放,執行緒才能繼續向下執行
sync.releaseShared(1);
}
為了方便理解,我們還是來看一看AQS
中releaseShared
方法的實現:
public final boolean releaseShared(int arg) {
// 呼叫tryReleaseShared嘗試釋放鎖,這個方法已經由Sycn重寫,請回顧上面對此方法的分析
// 若tryReleaseShared返回true,表示count經過這次釋放後,等於0了,於是執行doReleaseShared
if (tryReleaseShared(arg)) {
// 這個方法的作用是喚醒AQS的同步佇列中,正在等待的第一個執行緒
// 而我們分析acquireSharedInterruptibly方法時已經說過,
// 若一個執行緒被喚醒,檢測到count == 0,會繼續喚醒下一個等待的執行緒
// 也就是說,這個方法的作用是,在count == 0時,喚醒所有等待的執行緒
doReleaseShared();
return true;
}
return false;
}
三、總結
如果直接去看CountDownLatch
的原始碼會發現,它的實現真的非常簡單,包括註釋在內,總共300
行程式碼,除去註釋,連100
行程式碼都不到。因為它所作的工作,除了重寫AQS
的兩個方法外,其餘的基本上就是呼叫AQS
提供的模板方法而已。所以,理解CountDownLatch
的過程,實際上是理解AQS
的過程,只要理解了AQS
,看懂CountDownLatch
的原理,不需要5
分鐘。AQS
真的是Java
併發中非常重要的一個元件,很多類都是基於它實現的,比如還有ReentrantLock
,同時AQS
也是面試中的常考點,所以一定要好好研究。最後再次推薦我之前編寫的有關AQS
的原始碼分析部落格:併發——抽象佇列同步器AQS的實現原理。
四、參考
- JDK1.8原始碼