一、簡介
本文主要講解併發編程中常用的三個工具,他們分別是CountDownLatch(閉鎖)、CycleBarrier(循環欄柵)、Semaphore(信號量),三個工具都是在JUC併發包下提供的多線程開發工具,各自有各自的使用場景,在多線程開發中可以根據業務場景來選擇合適的工具。三個工具是以AQS以及以AQS爲基礎的Lock來構成的,所以最底層還是AQS,關於AQS可以通過文章《JUC-AQS框架解析》來學習瞭解。
二、解析
2.1 CountDownLatch(閉鎖)
使用場景?CountDownLatch也被稱爲閉鎖,併發編程中的一個工具。當某一個線程執行到某一個位置時,需要等待其他線程完成後,再繼續往下執行,這樣的場景適合使用此工具。
使用方式?首先創建CountDownLatch的一個實例對象,指定等待鎖數量k,一般等於剩餘等待的線程數量,然後等待線程執行await()方法阻塞等待,其他線程執行countDown(),當k個countDown()執行後,則等待線程被喚醒,繼續執行,簡單的使用邏輯如下:
public class CountDownLatchTest {
private static CountDownLatch countDownLatch = new CountDownLatch(2);
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "beginning do");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "end");
countDownLatch.countDown();
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "beginning do");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "end");
countDownLatch.countDown();
}).start();
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
首先我們看下CountDownLatch的構造器方法,入參是一個int類型的大於等於0的count,在構造器內部創建了內部類Sync的一個實例,如下所示:
public CountDownLatch(int count) {
if (count < 0) throw new IllegalArgumentException("count < 0");
this.sync = new Sync(count);
}
CountDownLatch的內部類Sync是繼承了AQS《JUC-AQS框架解析》的一個子類,剛剛構造器傳入的int類型的count參數,就是AQS中共享變量state的初始化數值,並且還可以看到Sync實現了AQS中的共享模式下的獲取和釋放共享資源的方法,也就是重寫了tryAcquireShared()和tryReleaseShared()方法,源碼如下:
private static final class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 4982264981922014374L;
//初始化AQS的state爲count
Sync(int count) {
setState(count);
}
//獲取state
int getCount() {
return getState();
}
//共享模式下重寫的獲取資源方法
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
//共享模式下重寫的釋放資源方法
protected boolean tryReleaseShared(int releases) {
//cas+自旋釋放資源
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
}
下面我們看下等待線程調用的await()方法,此方法功能是使調用線程暫時阻塞,直到countDown()方法釋放完畢共享資源(state=0),纔會喚醒等待線程,繼續向下執行,過程如下:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
可以看到CountDownLatch的await()方法調用的是AQS的acquireSharedInterruptibly()方法,AQS內此方法邏輯如下:
//*************AQS**************//
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
Sync內部重寫了tryAcquireShared()方法,重寫之後的邏輯是:(getState() == 0) ? 1 : -1;結果是大於0和小於0;
1.如果大於0,意味着共享資源釋放完畢,整個方法返回,執行結束;
2.如果小於0,則意味着還有線程佔用着共享資源沒有釋放,則進入執行AQS內的doAcquireSharedInterruptibly()方法,該方法和doAcquireShared()區別在於後者在過程中是不響應中斷的,具體邏輯可以參考AQS《JUC-AQS框架解析》文章瞭解。如果countDown()方法沒有執行count數量,則此時等待線程執行到此方法內部,會處於等待阻塞的狀態。
下面我們看下其他線程的countDown()方法,具體邏輯如下:
public void countDown() {
sync.releaseShared(1);
}
直接調用的是Sync的父類AQS的releaseShared()方法,邏輯如下:
//*************AQS**************//
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
首先調用自定義同步器重寫的tryReleaseShared(),也就是Sync重寫的方法邏輯,如下邏輯:
protected boolean tryReleaseShared(int releases) {
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
可以看到邏輯是cas+自旋的方式,設置state值減一,然後返回state是否等於0,如果等於0,則進入doReleaseShared()邏輯,則去喚醒隊列節點去獲取鎖等操作;如果不等於0,則不去喚醒隊列節點。
總結過程:以上大概就是CountDownLatch工具的執行過程,我們總結過程如下:
1.創建CountDownLatch的實例對象,設定入參count值,初始化AQS對的state,主線程也就是等待線程去執行await()方法;
2.主線程執行到await()方法時,調用AQS的acquireSharedInterruptibly()方法,如果此時state!=0,則阻塞此方法;
3.如果此時state=0,則等待線程繼續執行,直到完畢;
3.其他線程執行countDown()方法時,使用cas+自旋的方式,設置state值減一,如果state=0,則喚醒隊列的後續節點;
4.如果state!=0,則執行完畢;
注意!CountDownLatch工具在AQS構建的隊列,只會有兩個節點,一個是空的頭結點(AQS初始化),另一個就是阻塞的等待線程節點。
2.2 CycleBarrier(循環欄柵)
功能?CycleBarrier與CountDownLatch有着類似的作用,但是二者區別又很明顯,CycleBarrier是可以循環使用的,且當全部線程都到達某一個Barrier屏障後,然後全部繼續向下執行。
簡單的使用方式如下:
public class CyclicBarrierTest {
private static CyclicBarrier cycleBarrier = new CyclicBarrier(2, () -> System.out.println("-last task over-"));
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " beginning do");
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " sleep end");
cycleBarrier.await();
System.out.println(Thread.currentThread().getName() + " do over");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " beginning do");
try {
cycleBarrier.await();
System.out.println(Thread.currentThread().getName() + " do over");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
}).start();
cycleBarrier.reset();
}
}
首先看一下他的主要成員變量,包括鎖、數量控制以及鎖的條件:
//執行的await()方法的加鎖工具,避免併發造成count不準
private final ReentrantLock lock = new ReentrantLock();
//該鎖的條件,用於阻塞線程知道滿足條件
private final Condition trip = lock.newCondition();
//構造函數傳入參數,指定參與的線程數量
private final int parties;
//構造參數傳入,用於默認的執行任務
private final Runnable barrierCommand;
//作爲代標識
private Generation generation = new Generation();
//已經執行await()的數量,初始化值爲parties
private int count;
下面我們看下CycleBarrier的執行過程,首先我們看下他的創建入口:
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;
this.barrierCommand = barrierAction;
}
可以看到實際上是調用的兩個參數的構造方法,入口有兩個參數,一個是parties,代表着必須執行await()方法的線程數量,纔可以到達柵欄繼續往下執行,另外一個是Runnable類型的barrierAction,當parties數量的線程執行await()方法之後,則運行此配置的任務。
下面我看下核心方法await(),方法邏輯如下:
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
還有一個await()方法支持傳入等待時間的,底層都是同樣調用doAwait()方法,下面我看下此方法:
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();
}
//執行count減一
int index = --count;
if (index == 0) { //如果全部執行完畢,count=0,則喚醒所有線程,執行入參的任務
boolean ranAction = false;
try {
final Runnable command = barrierCommand;
if (command != null)
command.run();
ranAction = true;
nextGeneration();//開始下一代
return 0;
} finally {
if (!ranAction)
breakBarrier();
}
}
//如果count!=0,則循環開始,直到喚醒、本代結束(broken=0)、中斷、或者超時
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 {
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();
}
}
首先我們看到進入方法先進行lock加鎖操作,後續進行count減一操作,如果count=0,則喚醒所有線程執行後續邏輯,如果count!=0則使線程進入等待狀態,具體邏輯如下:
1.lock加鎖,判斷broken是否爲true,如果是則拋出異常;繼續判斷線程是否中斷,如果中斷則拋出異常;
2.lock內執行--count,然後判斷count是否爲0;
3.如果count=0,表示parties數量的線程執行await()方法完畢,則執行默認任務,喚醒其他線程,歸置count和generation;
4.如果count!=0,則表示還有線程沒有執行await()方法,則走入後邊的自旋,按照傳入的時間參數進入等待狀態(等待時間依據配置);
5.如果沒有配置等待時間,則會持續等待直到被喚醒;如果配置了等待時間,則在等待配置時間內沒有被喚醒,則拋出timeout異常,結束;
剛剛上面流程提到,如果count=0則進入nextGeneration()方法,過程如下:
private void nextGeneration() {
//喚醒所有線程
trip.signalAll();
//重置count和generation(可重用)
count = parties;
generation = new Generation();
}
喚醒所有等待線程繼續執行,初始化count爲parties值,新建一個generation對象,表示新的一代開始。
當發生異常情況時候,會執行breakBarrier()方法,過程如下:
private void breakBarrier() {
//本代標識中斷標識true
generation.broken = true;
//重置count
count = parties;
//通知所有線程(broken=true都會拋出異常)
trip.signalAll();
}
可以看到broken中斷標識的值變成true,初始化count,通知所有線程,此時其他線程被喚醒後,執行至broken的判斷,都會拋出BrokenBarrierException的異常,從而停止。
總結過程!CyclicBarrier的核心基本都在doAwait()方法中,所以理解上面分析的此方法的邏輯,便可以理解此工具的工作邏輯。
2.3 Semaphore(信號量)
信號量Semaphore可以控制同時執行代碼體的線程數量,比如說,在Semaphore控制代碼間,只能有k個線程同時執行,類似於lock加鎖間只允許一個線程去執行,而Semaphore是可控的線程數量,簡單的使用方式如下:
public class SemaphoreTest {
private static SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
private static Semaphore semaphore = new Semaphore(2);
public static void main(String[] args) {
SemaphoreTest semaphoreTest = new SemaphoreTest();
for (int i = 0; i < 10; i++) {
new Thread(semaphoreTest::doThing).start();
}
}
private void doThing() {
try {
semaphore.acquire();
System.out.println(Thread.currentThread().getName() + "-start : " + getFormatTimeStr());
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + "-end : " + getFormatTimeStr());
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static String getFormatTimeStr() {
return sf.format(new Date());
}
}
執行結果參考如下:
可以大致的看出同時執行的線程是兩個,當一個線程結束的時候,另一個線程可以開始,始終維持這最多兩個線程執行這段期間的代碼邏輯,下面我們分析一下它的執行邏輯。
構造函數!首先我們看下他的程序入口:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
Semaphore分爲公平和非公平兩種方式,構造函數有兩個入參,其中一個是int類型的permits參數,也就是可以並行的線程數量,另外是boolean類型的fair參數,可以指定是否是公平模式的,默認是非公平方式的。
Sync是Semaphore內部類,他繼承自AQS,可以看到我們傳進去的permits,作爲了AQS中的state的初始值。
Sync(int permits) {
setState(permits);
}
獲取資源!下面我們以默認的非公平的方式分析,首先我們看下acquire()方法,邏輯如下:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
Sync是Semaphore的一個內部類,類似於我們上邊講解的CountDownLatch工具那樣,使用內部類的形式繼承AQS來實現響應功能,acquireSharedInterruptibly()方法是AQS內的共享模式的頂層入口,邏輯如下(類似於CountDownLatch工具):
//*************AQS**************//
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
在看Semaphore重寫的tryAcquireShared()方法邏輯:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
可以看到調用的是父類Sync的nonfairTryAcquireShared()方法,邏輯如下:
final int nonfairTryAcquireShared(int acquires) {
for (;;) {
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
總體思想是自旋+cas,獲取state值,然後減去本次將要佔用的資源數量,判斷剩餘值remaining。如果remaining<0,則證明本次無法獲取足夠資源,此時返回負數,則進入剛剛上面的判斷,進入AQS的doAcquireSharedInterruptibly()方法,線程進入等待狀態,等待喚醒執行。如果remaining>=0,則證明有足夠資源,則進行cas設置,直到成功,整個方法返回。
獲取鎖的方式在於自定義同步器的tryAcquireShared()方法的重寫邏輯,作爲對比,我們看下公平模式下獲取鎖的實現:
protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())//判斷是否是最靠前節點(公平)
return -1;
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
不同點在於公平模式下多了一個執行步驟,也就是hasQueuedPredecessors()方法的執行邏輯,前面文章中提到了此方法是用於判斷本線程節點是否有資格獲取鎖,包括空隊列、當前線程剛好是頭節點的下一個節點等情況下才有資格去獲取共享資源。
釋放資源!下面我們看下釋放資源的方法release(),邏輯如下:
public void release(int permits) {
if (permits < 0) throw new IllegalArgumentException();
sync.releaseShared(permits);
}
接着我們看AQS的releaseShared()方法:
//*************AQS**************//
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
繼續我們看下自定義同步器實現的共享模式的獲取資源的邏輯:
protected final boolean tryReleaseShared(int releases) {
for (;;) {
int current = getState();
int next = current + releases;
if (next < current)
throw new Error("Maximum permit count exceeded");
if (compareAndSetState(current, next))
return true;
}
}
邏輯很簡單,自旋+cas方式釋放共享資源,其中會校驗一下共享資源是否釋放溢出。當釋放資源成功後,則執行doReleaseShared()方法,喚醒後續節點去嘗試獲取鎖。
整體過程!Semphore整體的工作流程:
1.首先通過構造函數創建Semaphore實例,指定併發數量以及按照需要指定默認的執行任務;
2.線程開始執行,調用Semaphore實例的acquire()方法,底層調用AQS共享模式下的獲取鎖的方式,重寫獲取鎖的方法。cas+自旋去判斷當前state值是否小於0;
3.如果state減去此次佔用的資源後,是小於0的,則表示本線程需要去隊列中等待;
4.如果state大於等於0,則cas更新state的值,如果成功則返回,如果失敗則下一次循環;
注意Semaphore是區分公平模式和非公平模式的,默認是以非公平方式進行的。
四、資源地址
文檔:《Thinking in java》
jdk1.8版本源碼