在瞭解了 AQS
再來學習下,它在 Java
源碼中的應用。先從同步工具類開始吧;關於同步工具類如何使用,有一篇很好的文章可以給你:點擊這裏。
CountDownLatch
允許一個或多個線程等待,直到在其它線程中執行的一組操作完成。
在瞭解了 AQS
之後,我們知道它的獨佔鎖式和共享鎖模式。所以,我們第一步需要弄明白 ,CountDownLatch
使用的是什麼模式?參考它的用法知道,它需要一個或多個線程阻塞,並在計數降爲 0 後,喚醒所有阻塞的線程,所以,它是一個共享鎖模式。
講 AQS
也提到過,大多數使用它的地方都不是直接繼承,而是維護一個內部的私有類來繼承,在這裏也一樣:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
// 初始化計數,使用 AQS 中的 state
Sync(int count) {
setState(count);
}
int getCount() {
return getState();
}
/**
* 使用共享鎖模式需要實現的方法,在這裏,state 爲 0 表示獲取到鎖,如果獲取鎖失敗,將入 CLH 隊 * 列並阻塞線程;
*/
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
/**
* 遞減 state
*/
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;
}
}
}
CountDownLatch
有着以下幾個方法,由於每個方法的源碼比較簡單,這裏就不在列出:
-
構造函數:構造函數接受一個 int ,用於創建上面的
Sync
對象,這是 state 初始的值;更改 state 的值必須使用父類的方法,爲了線程安全; -
await:該方法調用了
AQS
的acquireSharedInterruptibly
方法,熟悉AQS
的朋友都知道,該方法會回調子類實現的tryAcquireShared
方法(也就是上面的這個方法)。如果這個方法返回的數小於 0,則將入隊列,並阻塞,然後等待被喚醒; -
countDwon:該方法調用了
AQS
的releaseShared
方法,每次狀態值減一,該方法會回調上面的tryReleaseShared
方法。如果該方法返回 true,將去喚醒隊列中阻塞的線程。可以看見,當狀態值爲已經 0 ,將直接返回 false,只有在狀態值第一次等於 0 時,纔會返回 true,這時候就會嘗試去喚醒隊列中的阻塞線程。注意:並不是等於 0 就直接去喚醒,而是隻有在第一次等於 0 纔會去這樣做,也就是後續再調用這個方法並不會再去隊列上喚醒線程了,儘管這個隊列上沒有線程。
-
getCount:返回狀態值,在這裏的語義可以理解爲:等待線程還需再等待多少其它線程完成操作就可以執行。
總結:state 值很重要,只應該通過 AQS
提供的方法來操作它。鎖與 state 值只存在一種表示關係,你可以理解爲 state 爲 0 才表示獲得鎖,也可以理解爲 state 爲 1 才表示獲得鎖,這取決於你自己。只要明白 tryAcquireShared
方法返回一個大於 0 的鎖就是鎖獲取成功,返回小於 1 的數,表示獲取鎖失敗,並且會阻塞等待,其它的完全取決於你的想象力。
CyclicBarrier
允許一組線程彼此等待到達一個共同的障礙點。
CyclicBarrier
有一個代的思想,它指的是當一組線程均到達了屏障,則完成了一代,然後開始下一代;但也有特殊情況,那就是在各線程均到達屏障點以後要執行的那個任務失敗了,則將損壞當前代(屏障被打破),其它還未獲得執行許可的線程在阻塞超時以後,將得到當前代已經被損壞的信息,並拋出 BrokenBarrierException
異常。
CyclicBarrier
底層是通過 ReentrantLock
,並結合 Condition
來實現的,主要的代碼是以下這個方法:
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 代表當前代
final Generation g = generation;
// 檢查當前代是否已經損壞
if (g.broken)
throw new BrokenBarrierException();
// 如果當前線程已經被中斷,則直接打破屏障
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
// 代表所有線程均到達屏障
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 所有線程到達屏障後,可以執行的任務
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
// 正常開始下一代,喚醒所有阻塞線程,重置計數和代
nextGeneration();
return 0;
} finally {
// 屏障任務未正常執行完成,將打破屏障,所有阻塞的線程被喚醒,然後,將以異常結束
if (!ranAction)
breakBarrier();
}
}
// loop until tripped, broken, interrupted, or timed out
for (;;) {
try {
if (!timed)
trip.await();
else if (nanos > 0L)
// 到達超時時間也不能結束,需要被喚醒
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
if (g == generation && ! g.broken) {
breakBarrier();
throw ie;
} else {
// We're about to finish waiting even if we had not
// been interrupted, so this interrupt is deemed to
// "belong" to subsequent execution.
Thread.currentThread().interrupt();
}
}
// 阻塞結束後,需要檢驗的各種條件
if (g.broken)
throw new BrokenBarrierException();
// 當前代已經結束,已正常開始下一代
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
從上面可以知道,如果在屏障處等待的線程被中斷或者超時,將打破屏障(如果還處在當前代的話),所有在已到達屏障處的線程將被喚醒,並拋出 BrokenBarrierException
;還未到達屏障處的線程在到達以後,將立即得到該異常。
CyclicBarrier
的方法,都是基於 ReentrantLock
的加鎖和解鎖來操作的,所以,在調用其它的方法時,也需要注意鎖的競爭;
總結:
在屏障處等待的一個線程,有三種結束狀態:
- 正常的結束:此時,循環屏障已經開啓下一代,可重複使用;
- 被中斷,捕獲到中斷異常:這表明屏障已經被打破,其它在屏障處等待的線程將捕獲到
BrokenBarrierException
異常。 - 捕獲到
BrokenBarrierException
異常:這可能是由於屏障任務執行失敗,其它線程被中斷。
想要重用循環屏障,需要調用 reset
方法,不過,參考該方法的實現發現,該方法會先去打破屏障,再開始下一代。該方法的文檔介紹說並不適用於重置因某種原因破損的屏障,此時,推薦重新創建循環屏障。
推薦博文
我與風來
認認真真學習,做思想的產出者,而不是文字的搬運工。錯誤之處,還望指出!
如果覺得這篇文章對你有所幫助,動動小手,點點贊,這將使我能夠爲你帶來更好的文章。