AQS的全稱 AbstractQueuedSynchronizer,抽象隊列同步器。這個類在 java.util.concurrent.locks 包下面。AQS 是一個用來構建鎖和同步器的框架,使用AQS能簡單且高效地構造出應用廣泛的大量的同步器,比如我們提到的ReentrantLock,Semaphore,其他的諸如 ReentrantReadWriteLock,SynchronousQueue,FutureTask 等等皆是基於AQS的。當然,我們自己也能利用AQS非常輕鬆容易地構造出符合我們自己需求的同步器。
AQS 原理
1. AQS 原理概覽
AQS核心思想是,如果被請求的共享資源空閒,則將當前請求資源的線程設置爲有效的工作線程,並且將共享資源設置爲鎖定狀態。如果被請求的共享資源被佔用,那麼就需要一套線程阻塞等待以及被喚醒時鎖分配的機制,這個機制AQS是用CLH隊列鎖實現的,即將暫時獲取不到鎖的線程加入到隊列中。
CLH(Craig,Landin,and Hagersten)隊列是一個虛擬的雙向隊列(虛擬的雙向隊列即不存在隊列實例,僅存在結點之間的關聯關係)。AQS
是將每條請求共享資源的線程封裝成一個CLH鎖隊列的一個結點(Node)來實現鎖的分配。
下圖爲 AQS(AbstractQueuedSynchronizer) 原理圖:
AQS使用一個int成員變量來表示同步狀態,通過內置的FIFO隊列來完成獲取資源線程的排隊工作。AQS使用CAS對該同步狀態進行原子操作實現對其值的修改。
private volatile int state;//共享變量,使用volatile修飾保證線程可見性
狀態信息通過procted類型的getState,setState,compareAndSetState進行操作
//返回同步狀態的當前值
protected final int getState() {
return state;
}
// 設置同步狀態的值
protected final void setState(int newState) {
state = newState;
}
//原子地(CAS操作)將同步狀態值設置爲給定值update如果當前同步狀態的值等於expect(期望值)
protected final boolean compareAndSetState(int expect, int update) {
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
2. AQS 對資源的共享方式
AQS定義兩種資源共享方式:
1. Exclusive(獨佔):只有一個線程能執行,如ReentrantLock。又可分爲公平鎖和非公平鎖:
- 公平鎖:按照線程在隊列中的排隊順序,先到者先拿到鎖
- 非公平鎖:當線程要獲取鎖時,無視隊列順序直接去搶鎖,誰搶到就是誰的
2. Share(共享):多個線程可同時執行,如Semaphore、CountDownLatch。
ReentrantReadWriteLock 可以看成是組合式,因爲ReentrantReadWriteLock也就是讀寫鎖允許多個線程同時對某一資源進行讀。不同的自定義同步器爭用共享資源的方式也不同。自定義同步器在實現時只需要實現共享資源 state 的獲取與釋放方式即可,至於具體線程等待隊列的維護(如獲取資源失敗入隊/喚醒出隊等),AQS已經在上層已經幫我們實現好了。
3. AQS底層使用了模板方法模式
同步器的設計是基於模板方法模式的,如果需要自定義同步器一般的方式是這樣:
- 使用者繼承AbstractQueuedSynchronizer並重寫指定的方法。(這些重寫方法很簡單,無非是對於共享資源state的獲取和釋放)
- 將AQS組合在自定義同步組件的實現中,並調用其模板方法,而這些模板方法會調用使用者重寫的方法。
AQS使用了模板方法模式,自定義同步器時需要重寫下面幾個AQS提供的模板方法:
isHeldExclusively()//該線程是否正在獨佔資源。只有用到condition才需要去實現它。
tryAcquire(int)//獨佔方式。嘗試獲取資源,成功則返回true,失敗則返回false。
tryRelease(int)//獨佔方式。嘗試釋放資源,成功則返回true,失敗則返回false。
tryAcquireShared(int)//共享方式。嘗試獲取資源。負數表示失敗;0表示成功,但沒有剩餘可用資源;正數表示成功,且有剩餘資源。
tryReleaseShared(int)//共享方式。嘗試釋放資源,成功則返回true,失敗則返回false。
默認情況下,每個方法都拋出 UnsupportedOperationException
。 這些方法的實現必須是內部線程安全的,並且通常應該簡短而不是阻塞。AQS類中的其他方法都是final ,所以無法被其他類使用,只有這幾個方法可以被其他類使用。
以ReentrantLock爲例,state初始化爲0,表示未鎖定狀態。A線程lock()時,會調用tryAcquire()獨佔該鎖並將state+1。此後,其他線程再tryAcquire()時就會失敗,直到A線程unlock()到state=0(即釋放鎖)爲止,其它線程纔有機會獲取該鎖。當然,釋放鎖之前,A線程自己是可以重複獲取此鎖的(state會累加),這就是可重入的概念。但要注意,獲取多少次就要釋放多麼次,這樣才能保證state是能回到零態的。
再以CountDownLatch以例,任務分爲N個子線程去執行,state也初始化爲N(注意N要與線程個數一致)。這N個子線程是並行執行的,每個子線程執行完後countDown()一次,state會CAS(Compare and Swap)減1。等到所有子線程都執行完後(即state=0),會unpark()主調用線程,然後主調用線程就會從await()函數返回,繼續後餘動作。
一般來說,自定義同步器要麼是獨佔方法,要麼是共享方式,他們也只需實現tryAcquire-tryRelease
、tryAcquireShared-tryReleaseShared
中的一種即可。但AQS也支持自定義同步器同時實現獨佔和共享兩種方式,如 ReentrantReadWriteLock
。
Semaphore(信號量)-允許多個線程同時訪問
synchronized 和 ReentrantLock 都是一次只允許一個線程訪問某個資源,Semaphore(信號量)可以指定多個線程同時訪問某個資源。示例代碼如下:
public class SemaphoreExample1 {
// 請求的數量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 創建一個具有固定線程數量的線程池對象(如果這裏線程池的線程數量給太少的話你會發現執行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
// 一次只能允許執行的線程數量。
final Semaphore semaphore = new Semaphore(20);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表達式的運用
try {
semaphore.acquire();// 獲取一個許可,所以可運行線程數量爲20/1=20
test(threadnum);
semaphore.release();// 釋放一個許可
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模擬請求的耗時操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模擬請求的耗時操作
}
}
執行 acquire
方法阻塞,直到有一個許可證可以獲得然後拿走一個許可證;每個 release
方法增加一個許可證,這可能會釋放一個阻塞的acquire方法。然而,其實並沒有實際的許可證這個對象,Semaphore只是維持了一個可獲得許可證的數量。 Semaphore經常用於限制獲取某種資源的線程數量。當然一次也可以一次拿取和釋放多個許可,不過一般沒有必要這樣做
除了 acquire
方法之外,另一個比較常用的與之對應的方法是tryAcquire
方法,該方法如果獲取不到許可就立即返回false。Semaphore 有兩種模式,公平模式和非公平模式。
- 公平模式: 調用acquire的順序就是獲取許可證的順序,遵循FIFO;
- 非公平模式: 搶佔式的。
Semaphore 對應的兩個構造方法如下:
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
這兩個構造方法,都必須提供許可的數量,第二個構造方法可以指定是公平模式還是非公平模式,默認非公平模式。
CountDownLatch (倒計時器)
CountDownLatch是一個同步工具類,用來協調多個線程之間的同步。這個工具通常用來控制線程等待,它可以讓某一個線程等待直到倒計時結束,再開始執行。
1. CountDownLatch 的兩種典型用法
- 某一線程在開始運行前等待n個線程執行完畢。將 CountDownLatch 的計數器初始化爲n :
new CountDownLatch(n)
,每當一個任務線程執行完畢,就將計數器減1countdownlatch.countDown()
,當計數器的值變爲0時,在CountDownLatch上 await()
的線程就會被喚醒。一個典型應用場景就是啓動一個服務時,主線程需要等待多個組件加載完畢,之後再繼續執行。 - 實現多個線程開始執行任務的最大並行性。注意是並行性,不是併發,強調的是多個線程在某一時刻同時開始執行。類似於賽跑,將多個線程放到起點,等待發令槍響,然後同時開跑。做法是初始化一個共享的
CountDownLatch
對象,將其計數器初始化爲 1 :new CountDownLatch(1)
,多個線程在開始執行任務前首先coundownlatch.await()
,當主線程調用 countDown() 時,計數器變爲0,多個線程同時被喚醒。
2. CountDownLatch 的使用示例
public class CountDownLatchExample1 {
// 請求的數量
private static final int threadCount = 550;
public static void main(String[] args) throws InterruptedException {
// 創建一個具有固定線程數量的線程池對象(如果這裏線程池的線程數量給太少的話你會發現執行的很慢)
ExecutorService threadPool = Executors.newFixedThreadPool(300);
final CountDownLatch countDownLatch = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadnum = i;
threadPool.execute(() -> {// Lambda 表達式的運用
try {
test(threadnum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
countDownLatch.countDown();// 表示一個請求已經被完成
}
});
}
countDownLatch.await();
threadPool.shutdown();
System.out.println("finish");
}
public static void test(int threadnum) throws InterruptedException {
Thread.sleep(1000);// 模擬請求的耗時操作
System.out.println("threadnum:" + threadnum);
Thread.sleep(1000);// 模擬請求的耗時操作
}
}
3. CountDownLatch 的不足
CountDownLatch是一次性的,計數器的值只能在構造方法中初始化一次,之後沒有任何機制再次對其設置值,當CountDownLatch使用完畢後,它不能再次被使用。
CyclicBarrier(循環柵欄)
CyclicBarrier 和 CountDownLatch 非常類似,它也可以實現線程間的技術等待,但是它的功能比 CountDownLatch 更加複雜和強大。主要應用場景和 CountDownLatch 類似。
CyclicBarrier 的字面意思是可循環使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組線程到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個線程到達屏障時,屏障纔會開門,所有被屏障攔截的線程纔會繼續幹活。CyclicBarrier默認的構造方法是 CyclicBarrier(int parties)
,其參數表示屏障攔截的線程數量,每個線程調用await
方法告訴 CyclicBarrier 我已經到達了屏障,然後當前線程被阻塞。
1. CyclicBarrier 的應用場景
CyclicBarrier 可以用於多線程計算數據,最後合併計算結果的應用場景。比如我們用一個Excel保存了用戶所有銀行流水,每個Sheet保存一個帳戶近一年的每筆銀行流水,現在需要統計用戶的日均銀行流水,先用多線程處理每個sheet裏的銀行流水,都執行完之後,得到每個sheet的日均銀行流水,最後,再用barrierAction用這些線程的計算結果,計算出整個Excel的日均銀行流水。
2. CyclicBarrier 的使用示例
示例1:
public class CyclicBarrierExample2 {
// 請求的數量
private static final int threadCount = 550;
// 需要同步的線程數量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
public static void main(String[] args) throws InterruptedException {
// 創建線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
try {
cyclicBarrier.await(2000, TimeUnit.MILLISECONDS);
} catch (Exception e) {
System.out.println("-----CyclicBarrierException------");
}
System.out.println("threadnum:" + threadnum + "is finish");
}
}
可以看到當線程數量也就是請求數量達到我們定義的 5 個的時候, await
方法之後的方法才被執行。另外,CyclicBarrier還提供一個更高級的構造函數 CyclicBarrier(int parties, Runnable barrierAction)
,用於在線程到達屏障時,優先執行barrierAction
,方便處理更復雜的業務場景。示例代碼如下:
public class CyclicBarrierExample3 {
// 請求的數量
private static final int threadCount = 550;
// 需要同步的線程數量
private static final CyclicBarrier cyclicBarrier = new CyclicBarrier(5, () -> {
System.out.println("------當線程數達到之後,優先執行------");
});
public static void main(String[] args) throws InterruptedException {
// 創建線程池
ExecutorService threadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < threadCount; i++) {
final int threadNum = i;
Thread.sleep(1000);
threadPool.execute(() -> {
try {
test(threadNum);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (BrokenBarrierException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
});
}
threadPool.shutdown();
}
public static void test(int threadnum) throws InterruptedException, BrokenBarrierException {
System.out.println("threadnum:" + threadnum + "is ready");
cyclicBarrier.await();
System.out.println("threadnum:" + threadnum + "is finish");
}
}
3. CyclicBarrier和CountDownLatch的區別
- CountDownLatch 是計數器,只能使用一次,而 CyclicBarrier 的計數器提供 reset 功能,可以多次使用。
- 對於 CountDownLatch 來說,重點是“一個線程(多個線程)等待”,而其他的 N 個線程在完成“某件事情”之後,可以終止,也可以等待。而對於 CyclicBarrier,重點是多個線程,在任意一個線程沒有完成,所有的線程都必須等待。CountDownLatch 是計數器,線程完成一個記錄一個,只不過計數不是遞增而是遞減,而 CyclicBarrier 更像是一個閥門,需要所有線程都到達,閥門才能打開,然後繼續執行。