點贊再看,養成習慣,搜一搜【一角錢技術】關注更多原創技術文章。本文 GitHub org_hejianhui/JavaStudy 已收錄,有我的系列文章。
前言
控制并發流程的工具類,作用就是幫助我們程序員更容易的讓線程之間合作,讓線程之間相互配合來滿足業務邏輯。比如讓線程A等待線程B執行完畢后再執行等合作策略。
控制并發流程的工具類主要有:
類 | 作用 | 說明 |
---|---|---|
Semaphore | 信號量,可以通過控制“許可證”的數量,來保證線程之間的配合 | 線程只有拿到“許可證”后才能繼續運行,相比于其它的同步器,更靈活 |
CyclicBarrier | 線程會等待,直到足夠多線程達到了事先規定的數目。一旦達到觸發條件,就可以進行下一步的動作 | 適用于線程之間相互等待處理結果的就緒場景 |
Phaser | 和CyclicBarrier類似,但是計數可變 | Java7加入的 |
CountDownLatch | 和CyclicBarrier類似,數量遞減到0時,觸發動作 | 不可重復使用 |
Exchanger | 讓兩個線程在合適時交換對象 | 適用場景:當兩個線程工作在同一個類的不同實例上時,用于交換數據 |
Condition | 可以控制線程的“等待”和“喚醒” | 是Object.wait() 的升級版 |
簡介
背景
- CountDownLatch是在Java1.5被引入,跟它一起被引入的工具類還有CyclicBarrier、Semaphore、ConcurrenthashMap和BlockingQueue。
- 在java.util.cucurrent包下。
概念
- CountDownLatch是一個同步計數器,他允許一個或者多個線程在另外一組線程執行完成之前一直等待,基于AQS共享模式實現的。
- 是通過一個計數器來實現的,計數器的初始值是線程的數量。每當一個線程執行完畢后,計數器的值就-1,當計數器的值為0時,表示所有線程都執行完畢,然后在閉鎖上等待的線程就可以恢復工作來。
關于 AQS,可以查看《并發編程之抽象隊列同步器AQS應用ReentrantLock》
應用場景
Zookeeper分布式鎖,Jmeter模擬高并發等
場景1 讓多個線程等待:模擬并發,讓并發線程一起執行
為了模擬高并發,讓一組線程在指定時刻(秒殺時間)執行搶購,這些線程在準備就緒后,進行等待(CountDownLatch.await()),直到秒殺時刻的到來,然后一擁而上。這也是本地測試接口并發的一個簡易實現。
在這個場景中,CountDownLatch充當的是一個發令槍
的角色;就像田徑賽跑時,運動員會在起跑線做準備動作,等到發令槍一聲響,運動員就會奮力奔跑。和上面的秒殺場景類似。
代碼實現如下:
package com.niuh.tools;
import java.util.concurrent.CountDownLatch;
/**
* <p>
* CountDownLatch示例
* 場景1 讓多個線程等待:模擬并發,讓并發線程一起執行
* </p>
*/
public class CountDownLatchRunner1 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(1);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try {
//準備完畢……運動員都阻塞在這,等待號令
countDownLatch.await();
String parter = "【" + Thread.currentThread().getName() + "】";
System.out.println(parter + "開始執行……");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(2000);// 裁判準備發令
countDownLatch.countDown();// 發令槍:執行發令
}
}
運行結果:
【Thread-2】開始執行……
【Thread-4】開始執行……
【Thread-3】開始執行……
【Thread-0】開始執行……
【Thread-1】開始執行……
我們通過CountDownLatch.await(),讓多個參與者線程啟動后阻塞等待,然后在主線程 調用CountDownLatch.countdown(1) 將計數減為0,讓所有線程一起往下執行;以此實現了多個線程在同一時刻并發執行,來模擬并發請求的目的。
場景2 讓單個線程等待:多個線程(任務)完成后,進行匯總合并
很多時候,我們的并發任務,存在前后依賴關系;比如數據詳情頁需要同時調用多個接口獲取數據,并發請求獲取到數據后、需要進行結果合并;或者多個數據操作完成后,需要數據check;這其實都是:在多個線程(任務)完成后,進行匯總合并的場景。
代碼實現如下:
package com.niuh.tools;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ThreadLocalRandom;
/**
* <p>
* CountDownLatch示例
* 場景2 讓單個線程等待:多個線程(任務)完成后,進行匯總合并
* </p>
*/
public class CountDownLatchRunner2 {
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(5);
for (int i = 0; i < 5; i++) {
final int index = i;
new Thread(() -> {
try {
Thread.sleep(1000 + ThreadLocalRandom.current().nextInt(1000));
System.out.println("finish" + index + Thread.currentThread().getName());
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
countDownLatch.await();// 主線程在阻塞,當計數器==0,就喚醒主線程往下執行。
System.out.println("主線程:在所有任務運行完成后,進行結果匯總");
}
}
運行結果:
finish3Thread-3
finish0Thread-0
finish1Thread-1
finish4Thread-4
finish2Thread-2
主線程:在所有任務運行完成后,進行結果匯總
在每個線程(任務) 完成的最后一行加上CountDownLatch.countDown(),讓計數器-1;當所有線程完成-1,計數器減到0后,主線程往下執行匯總任務。
源碼分析
本文基于JDK1.8
CountDownLatch 類圖
從圖中可以看出CountDownLatch是基于Sync類實現的,而Sync繼承AQS,使用的是AQS共享模式。
其內部主要變量和方法如下:
在我們方法中調用 awit()
和 countDown()
的時候,發生了幾個關鍵的調用關系,如下圖所示:
其與AQS交互原理如下:
構造函數
CountDownLatch類中只提供了一個構造器,參數count為計數器的大小
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
這里需要注意,設置state的數量只有在初始化CountDownLatch的時候,如果該state被減成了0,就無法繼續使用這個CountDownLatch了,需要重新new一個,這就是這個類不可重用的原因,有另一個類也實現了類似的功能,但是可以重用,就是CyclicBarrier。
內部同步器
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
//初始化,設置資源個數
Sync(int count) {
setState(count);
}
//獲取共享資源個數
int getCount() {
return getState();
}
//嘗試獲取共享鎖,只有當共享資源個數為0的時候,才會返回1,否則為-1
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
//釋放共享資源,通過CAS每次對state減1
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;
}
}
}
主要方法
類中有三個方法是最重要的
// 調用await()方法的線程會被掛起,它會等待直到count值為0才繼續執行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
//和await()方法類似,只不過等待一定的時間后count值還沒變為0的化就會繼續執行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
//將count值減1
public void countDown() {
sync.releaseShared(1);
}
await()方法
// 調用await()方法的線程會被掛起,它會等待直到count值為0才繼續執行
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
進入AbstractQueuedSynchronizer #acquireSharedInterruptibly()方法.
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//等待過程不可中斷
if (Thread.interrupted())
throw new InterruptedException();
//這里的tryAcquireShared在AbstractQueuedSynchronizer中沒有實現,在上面介紹的Sync中實現的
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
在上面介紹Sync類的時候#tryAcquireShared(),當AQS的state = 0的時候才會返回1,否則一直返回-1,如果返回-1,要執行#doAcquireSharedInterruptibly(),進入該方法
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//這里就把主線程加入隊列,隊列中有兩個節點,第一個是虛擬節點,第二個就是主線程節點
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
//總共只有兩個節點,主線程前一個就是首節點
final Node p = node.predecessor();
if (p == head) {
//這里又執行到CountDownLatch中Sync類中實現的方法,判斷state是否為0
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//如果state不為0,這里會把主線程掛起阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
這里使用AQS很神奇,在阻塞隊列中就只加入了一個主線程,但是呢,只要其他線程沒有執行完,那state就不為0,那主線程就在這里阻塞著,那問題了,誰來喚醒這個主線程呢?就是 countDown() 這個方法。
await(long timeout, TimeUnit unit)方法
該方法就是指定等待時間,如果在規定的等待時間中沒有完成,就直接返回false,在主線程中可以根據這個狀態進行后續的處理。
//和await()方法類似,只不過等待一定的時間后count值還沒變為0的化就會繼續執行
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
countDown() 方法
//將count值減1
public void countDown() {
sync.releaseShared(1);
}
進入AbstractQueuedSynchronizer #releaseShared方法
public final boolean releaseShared(int arg) {
//該方法同樣在AbstractQueuedSynchronizer中沒有實現,在CountDownLatch中實現
if (tryReleaseShared(arg)) {
//喚醒主線程
doReleaseShared();
return true;
}
return false;
}
在分析Sync類的時候,介紹了tryReleaseShared(),該方法會把AQS的state減1,如果減1操作成功,執行喚醒主線程操作,進入AbstractQueuedSynchronizer#tryReleaseShared()方法
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//首節點狀態為SIGNAL = -1
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//喚醒主線程,也就是隊列中的第二個節點,如果線程沒有執行完成,主線程被喚醒之后,發現state依然不為零,會再次阻塞
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
總結
CountDownLatch 和 Semaphore 一樣都是共享模式下資源問題,這些源碼實現AQS的模版方法,然后使用CAS+循環重試實現自己的功能。在RT多個資源調用,或者執行某種操作依賴其他操作完成下可以發揮這個計數器的作用。
CountDownLatch就只在隊列中放入一個主線程,然后不停的喚醒,喚醒之后發現state還是不為0,就繼續等待。每個子線程執行完都會對state進行減1操作,當所有子線程都執行完了,那state也就為0,這時候主線程被喚醒之后才可以繼續執行。而這也正是CountDownLatch不可重用的原因,如果想要重用,需要重新new一個,因為只有在new的時候才可以設置資源的數量。
CountDownLatch與Thread.join
CountDownLatch的作用就是允許一個或多個線程等待其他線程完成操作,看起來有點類似join() 方法,但其提供了比 join() 更加靈活的API。
CountDownLatch可以手動控制在n個線程里調用n次countDown()方法使計數器進行減一操作,也可以在一個線程里調用n次執行減一操作。
而 join() 的實現原理是不停檢查join線程是否存活,如果 join 線程存活則讓當前線程永遠等待。所以兩者之間相對來說還是CountDownLatch使用起來較為靈活。
CountDownLatch與CyclicBarrier
CountDownLatch和CyclicBarrier都能夠實現線程之間的等待,只不過它們側重點不同:
- CountDownLatch一般用于一個或多個線程,等待其他線程執行完任務后,再才執行;
- CyclicBarrier一般用于一組線程互相等待至某個狀態,然后這一組線程再同時執行;
- CountDownLatch 是一次性的,CyclicBarrier 是可循環利用的;
- CountDownLathch是一個計數器,線程完成一個記錄一個,計數器遞減,只能用一次。如下圖:
- CyclicBarrier的計數器更像一個閥門,需要所有線程都到達,然后繼續執行,計數器遞減,提供reset功能,可以多次使用。如下圖:
PS:以上代碼提交在 Github :https://github.com/Niuh-Study/niuh-juc-final.git
文章持續更新,可以搜一搜「 一角錢技術 」第一時間閱讀, 本文 GitHub org_hejianhui/JavaStudy 已經收錄,歡迎 Star。