前言
前面我們介紹了併發容器和隊列,今天我們來介紹幾個非常有用的併發工具類,今天主要講CountDownLatch和Cyclicbarrier這兩個工具類,通過講解並對比兩個類的區別,OK,讓我們開始今天的併發之旅吧。
什麼是CountDownLatch?
CountDownLatch用於監聽某些初始化操作,等待初始化執行完畢,通知主線程繼續工作,允許一個或者多個線程等待其他線程完成操作。之前我們知道要實現線程等待還有一個方法就是jion方法,先讓我們來回憶什麼是Join方法:
Join用於讓當前執行線程等待Join線程執行結束,實現原理是,不停的檢查Join線程是否存活,如果存活則讓當前線程永遠等待下去,如果Join線程終止,則調用this.notifyAll方法喚醒等待的線程;
CountDownLatch其實也是來做這件事的,而且比Join更強大,使用起來也很輕便。
如何使用CountDownLatch?
我們看下面這個demo,看看如何使用CountDownLatch:
public static void main(String[] args) {
// CountDownLatch接收一個int類型的計算器,此處是2代表計數器爲2,意思是需要等待2個線程喚醒
final CountDownLatch countDown = new CountDownLatch(2);
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("進入線程t1" + "等待其他線程處理完成...");
// countDown.await()方法會阻塞當前線程即t1,沒執行一次countDown()方法計數器就會-1
// 直到計數器=0,則當前阻塞的線程t1被喚醒,繼續執行
countDown.await();
System.out.println("t1線程繼續執行...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
},"t1");
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("t2線程進行初始化操作...");
Thread.sleep(3000);
System.out.println("t2線程初始化完畢,通知t1線程繼續...");
// 計數器-1
countDown.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
try {
System.out.println("t3線程進行初始化操作...");
Thread.sleep(4000);
System.out.println("t3線程初始化完畢,通知t1線程繼續...");
// 計數器再-1,喚醒t1,t1繼續執行
countDown.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t3.start();
}
執行結果:
猜想:假設t1或者t2由於某某原因發生異常未能執行countDown.countDown()那麼,t1線程豈不是要一直處於等待狀態嗎?當然JDK的設計大佬們纔不會給你留下這麼明顯的問題呢,所以countDown還提供了一個
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
,這個方法會在特定時間後,結束阻塞的線程。
CountDownLatch底層分析
我們主要看下CountDownLatch的await方法和countDown方法的源碼,首先看看await源碼:await內部採用公平鎖來實現等待
public void await() throws InterruptedException {
// 採用公平鎖機制
sync.acquireSharedInterruptibly(1);
}
public boolean await(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
再看下acquireSharedInterruptibly,這裏只分析await,超時await原理也差不多:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
// 判斷是否發生中斷
if (Thread.interrupted())
throw new InterruptedException();
// -1表示獲取到了共享鎖,1表示沒有獲取共享鎖
if (tryAcquireShared(arg) < 0)
// 獲取共享鎖,繼續執行
doAcquireSharedInterruptibly(arg);
}
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) {
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 void countDown() {
// 每次釋放一個計數器
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//嘗試釋放共享鎖
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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;
// 循環檢查
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
// loop on failed CAS
}
if (h == head)
// loop if head changed
break;
}
}
什麼是Cyclicbarrier?
Cyclicbarrier指的是可循環使用的屏障,主要是讓一組線程到達一個屏障之後被阻塞,當最後一個線程到達時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活。
如何使用Cyclicbarrier?
static class Runner implements Runnable {
private CyclicBarrier barrier;
private String name;
public Runner(CyclicBarrier barrier, String name) {
this.barrier = barrier;
this.name = name;
}
@Override
public void run() {
try {
// 因爲是先打印後阻塞,所以這裏getNumberWaiting的+1
int numberWaiting = barrier.getNumberWaiting();
int count = numberWaiting + 1 ;
System.out.println(name + " 進入賽道,簽到完畢,當前人數"+count);
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(name + " Go!!");
}
}
public static void main(String[] args) throws IOException, InterruptedException {
CyclicBarrier barrier = new CyclicBarrier(10);
// Executors是我們後續會講的線程池
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 100; i < 110; i++) {
Thread.sleep(1000);
executor.submit(new Thread(new Runner(barrier, i+"號選手進場")));
}
executor.shutdown();
}
執行結果:
某些情況下,我們需要讓阻塞屏障解除的時候,某些線程需要先執行,例如某個運動員買通了裁判,比賽開始時,比別的選手提前開跑,當然這在現實比賽中是不允許的,此處我只是打個比方,對於這樣的場景,Cyclicbarrier提供了:
public CyclicBarrier(int parties, Runnable barrierAction) {
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
this.barrierCommand = barrierAction;
}
用於在線程到達屏障時,優先執行barrierAction線程;
Cyclicbarrier底層實現
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen;
}
}
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 使用重入鎖,同步進行wait操作,計數器+1
final ReentrantLock lock = this.lock;
lock.lock();
try {
final Generation g = generation;
// 當前Generation處於打破狀態,拋出異常
if (g.broken)
throw new BrokenBarrierException();
// 當前Generation處於中斷狀態,拋出異常,並重置計數器,喚醒所有等待線程,可見見下面源碼
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
int index = --count;
// 當最後一個線程也到達了,就從調用中返回
if (index == 0) {
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();
return 0;
} finally {
// 如果運行command失敗也會導致當前屏障被打破
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();
}
}
private void breakBarrier() {
generation.broken = true;
count = parties;
trip.signalAll();
}
CountDownLatch和Cyclicbarrier比較
CountDownLatch就像一場跑步比賽,假設這場比賽有10個運動員,那麼計數器初始值就爲10,裁判員喊下比賽開始,就await阻塞在那,當每個運動員跑到終點就countDown一次,計數器-1,知道最後一個運動員到達終點即計數器爲0,此時裁判員被喚醒,統計比賽結果,完成比賽。
Cyclicbarrier就像這場比賽時,裁判員首先準備好10條賽道,準備完畢就拿個小本子在那等着,每當以爲選手到達賽道就簽到一次,當10個選手全部簽到完畢,裁判員就宣佈比賽正式開始,繼續執行下面的比賽。如果中間因爲某某原因,某個選手未能到場或者天氣原因,比賽推遲,簽到信息就重置,比賽恢復之後選手需要重新簽到;
區別總結:CountDownLatch的計數器只能使用一次。而CyclicBarrier的計數器可以使用reset() 方法重置。所以CyclicBarrier能處理更爲複雜的業務場景,比如如果計算髮生錯誤,可以重置計數器,並讓線程們重新執行一次。