CountDownLatch
作用
讓一些阻塞等待的線程,被一定數量的線程完成後完成喚醒.
但需要注意的是CountDownLatch只能使用一次,相當於說一旦用完了,就不會再阻塞了
原理步驟:
- 計數器由構造函數初始化,並用它來初始化AQS的states的值
- 當線程調用await方法時會檢查state的值是否爲0
- 如果是的話
- 表示資源池的資源已經被用光了,則不會被阻塞
- 如果不是的話
- 將該線程節點加入等待隊列
- 將自身進行阻塞
- 當其他線程調用countDown方法時
- 將計數器減一
- 判斷計數器是否爲0
- 爲0時喚醒隊列中的第一個節點
- 由於CountDownLatch使用了共享模式所以第一個節點被喚醒之後,又會觸發下一個節點的釋放(自旋),並且依此類推,使得所有節點都能被喚醒
- 爲0時喚醒隊列中的第一個節點
方法原理介紹
await
// 嘗試共享中斷
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 1. 將該節點加入到等待隊列
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 自旋
for (;;) {
// 獲取當前等待隊列中的前繼節點
final Node p = node.predecessor();
// 如果當前節點等於前繼節點
// 假設
// 1. 可能是第一個進入等待的,所以隊列中只有一個
// 2. 可能是等待隊列中的節點已經放棄光了,
if (p == head) {
// 嘗試獲取鎖,看是否已經資源沒有了
int r = tryAcquireShared(arg);
// 如果資源已經沒有了,說明可以釋放鎖了
if (r >= 0) {
// 重新設置頭部,並且釋放鎖
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 找到一個可靠的前繼節點
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞該節點,等待被喚醒
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
countDown
// 釋放共享鎖
public final boolean releaseShared(int arg) {
//1. 嘗試釋放共享鎖
if (tryReleaseShared(arg)) {
// 釋放鎖的操作
doReleaseShared();
return true;
}
return false;
}
// 1. 嘗試釋放共享鎖
protected boolean tryReleaseShared(int arg) {
// Decrement count; signal when transition to zero
// 自旋
for (; ; ) {
// 獲取當前資源數
int c = getState();
// 如果爲0的話,這裏的話表示不需要阻塞了
if (c == 0) {
return false;
}
// --------------------------如果資源數還有的話-------------------------------------
// 遞減1
int nextc = c - 1;
// 之後通過CAS自旋去獲取 , 這裏的c就是getState()的state是volatile修飾,其他線程改了這裏一定能看到 // 這裏通過CAS去判斷當前資源是否有競爭,沒有競爭的話會賦值成功
// 條件中會判斷是否爲0 , 這裏爲true的話,會觸發上面的釋放鎖的操作
if (compareAndSetState(c, nextc)) {
return nextc == 0;
}
}
}
// 這裏就是真正的釋放鎖的操作
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.
*/
// 自旋,通過頭結點進行釋放
/**
這裏需要先聲明一個細節點,不然很容易被繞進去,這裏的正常邏輯應該是
1. head的節點是Node.SIGNAL狀態
2. 單個線程釋放頭結點的時候肯定會經過unparkSuccessor方法,這個方法會將頭結點喚醒之後,會經過自旋迴到doAcquireSharedInterruptibly中的setHeadAndPropagate方法重新更換頭結點,一般是讓下一級節點頂上
3. h == head 一定是爲true的 (前提是單個線程的情況下)
而下面的代碼,除了正常情況,其他的都是搶佔併發資源的情況,如何去調整?都是通過自旋的方式,一遍一遍的去釋放,直道最終釋放完畢h == head 自旋結束
*/
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 // __ 這裏需要注意的是,如果h!=head說明已經被其他線程操作過一遍了,重新再來,又從頭結點開始釋放
break;
}
}
CyclicBarrier
作用
構造方法傳遞一個初始值,當線程執行到該對象的await方法時會先阻塞。
如果阻塞的線程的數量達到初始值,則會開始喚醒阻塞的線程。然後再繼續下一輪的線程統計。
方法原理介紹
CyclicBarrier 構造方法
public CyclicBarrier(int parties) {
this(parties, null);
}
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
// 構建一個初始變量,用於重複使用
this.parties = parties;
// 當前總數的變量,用來做計算
this.count = parties;
// 當count變量變成0的時候,會觸發這個線程中的方法
this.barrierCommand = barrierAction;
}
dowait
/**
* 主要的屏障代碼,涵蓋了各種策略
*/
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;
// 如果爲0的話,表示屏障已經使用完畢
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)
// 如果還沒有到達超時時間,則直接通過conditions的方法進行處理
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;// 也按照index處理
// 如果超時了,則按照線程中斷的處理方式去處理
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
//釋放鎖
lock.unlock();
}
}
/**
* Sets current barrier generation as broken and wakes up everyone.
* Called only while holding lock.
* 其實就是說當前屏障已經被打破了,直接喚醒所有屏障內的線程
*/
private void breakBarrier() {
// 當前標識對象設置爲true會被異常觸發
generation.broken = true;
// 重新回到初始值
count = parties;
// 喚醒所有線程
trip.signalAll();
}
/**
* Updates state on barrier trip and wakes up everyone.
* Called only while holding lock.
* 重置成一個新的屏障
*/
private void nextGeneration() {
// signal completion of last generation
// 喚醒所有線程
trip.signalAll();
// set up next generation
// 將統計總數重置爲初始值
count = parties;
// 重新構建一個生成對象
generation = new Generation();
}
執行流程
- 構建一個線程屏障個數範圍值
- 當一個線程開始阻塞的時候,會用ReentrantLock進行加鎖
- 判斷當前屏障範圍內的線程是否有效
- 如果其中一個線程無效了(中斷或者超時)
- 則喚醒該屏障內的所有線程
- 重新初始化屏障環境
- 如果其中一個線程無效了(中斷或者超時)
- 屏障範圍數遞減
- 判斷範圍數是否已經爲0
- 如果爲0 則表示範圍內的線程數量已經達到喚醒的條件了
- 重新初始化屏障環境
- 執行觸發線程(由使用者傳遞)
- 如果爲0 則表示範圍內的線程數量已經達到喚醒的條件了
- 如果沒有範圍數不爲0
- 通過ReentrantLock的
Condition
的條件組進行阻塞- 如果是超時情況的話通過
awaitNanos
方法進行阻塞 - 一旦超時,則該範圍內的線程都會被喚醒
- 屏障環境重置
- 如果是超時情況的話通過
- 通過ReentrantLock的
Semaphore
Semaphore是信號量,用於管理一組資源。其內部是基於AQS的共享模式,AQS的狀態表示許可證的數量,在許可證數量不夠時,線程將會被掛起;而一旦有一個線程釋放一個資源,那麼就有可能重新喚醒等待隊列中的線程繼續執行。
作用
可以用於資源保護機制,例如同一時間允許的最大併發量。
方法原理介紹
acquire:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 如果資源的數量小於0了
if (tryAcquireShared(arg) < 0)
// 阻塞當前還在搶佔的線程
doAcquireSharedInterruptibly(arg);
}
// 嘗試獲取共享鎖
protected int tryAcquireShared(int acquires) {
for (;;) {
// 如果等待隊列中還有線程等待,則說明資源已經被搶光了,直接排到後面等待吧
if (hasQueuedPredecessors())
return -1;
// 獲取狀態之後進行遞減得到的資源數是否爲0
int available = getState();
int remaining = available - acquires;
// 如果小於0,或者CAS成功之後,返回值
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
/**
* 能進入到這個方法的話說明資源數已經被用光了
* 這個方法只需要負責,將這些沒有搶佔到的資源給放到阻塞隊列中並阻塞即可
*/
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 將當前線程構建成一個新的節點,並且加入到阻塞隊列尾部
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
// 自旋
for (;;) {
// 獲取等待隊列中的最後一個節點
final Node p = node.predecessor();
// 如果這個節點是head節點的話
if (p == head) {
// 嘗試搶佔一下鎖
int r = tryAcquireShared(arg);
// 如果資源鎖還有的情況下
if (r >= 0) {
// 釋放當前正在阻塞的對象,如果有的對象正在阻塞中,則設置成PROPAGATE狀態
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 加入到阻塞隊列中,並且獲取一個有效的前繼節點
if (shouldParkAfterFailedAcquire(p, node) &&
// 阻塞該節點
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
執行流程:
-
判斷state的資源數是否小於0
1.1 不小於 --> 通過CAS將數值-1之後返回
1.2 進入2
- 將當前線程構建成一個新的Node節點
- 獲取新的節點的前繼節點,如果是head節點?
- 再次嘗試獲取資源數,如果大於0
- 則釋放該節點
- 將當前節點掛靠到一個可靠的前節點下,並加入到等待隊列中
- 開始進行自我阻塞,等待被喚醒
release:
public final boolean releaseShared(int arg) {
// 嘗試釋放鎖,資源數提升
if (tryReleaseShared(arg)) {
// 釋放鎖
doReleaseShared();
return true;
}
return false;
}
// 這裏就是簡單的通過CAS將資源鎖進行累加
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
private void doReleaseShared() {
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;
}
}
// 具體釋放鎖的方法
private void unparkSuccessor(Node node) {
// 獲取要釋放鎖的節點的等待狀態,一般是-1 阻塞狀態
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
// 如果該節點的下級節點爲空
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;
}
// 如果該node的下級節點不爲空,則直接喚醒
if (s != null)
LockSupport.unpark(s.thread);
}
運行流程
- 獲取當前資源鎖並且通過CAS累加
- 嘗試釋放鎖,從頭結點開始 - doReleaseShared
- 拿到頭結點之後,判斷頭節點的狀態
- 如果阻塞,則開始喚醒
- 如果狀態爲自由狀態,則設置成共享狀態標誌
- 爲什麼會這麼做,因爲多個線程同時操作的時候,頭節點可能會被操作不及時
- 頭節點一旦被多個線程操作,勢必會引起線程安全問題,所以這裏也是爲什麼要使用自旋去從頭結點釋放
- 如果h不等於頭結點?說明已經被其他線程操作過一遍了,這裏又要重新開始釋放一次
二者比較
CountDownLatch
- 不可重用
- countDown後可以繼續執行自己的任務
- 一般是阻塞主線程(await),子線程不會阻塞(countDown)
CyclicBarrier
- 可重用
- await後直接阻塞,但是如果出現線程中斷或者超時,則直接喚醒該範圍內的所有線程
- CyclicBarrier底層是用ReentrantLock的Condition去做的組喚醒