原創申明:本文由公衆號【猿燈塔】原創,轉載請說明出處標註
本文將介紹 java.util.concurrent.Phaser,一個經常被你們忽略的併發工具。它和 CyclicBarrier 以及 CountDownLatch 很像,可是使用上更加的靈活,本文會進行一些對比介紹。
和以前的文章不一樣,本文不寫源碼分析了,就只是從各個角度介紹下它是怎麼用的。本文比較簡單,我以爲對於初學者大概須要 20 分鐘左右吧。java
其實我對這個須要多少時間很沒概念,有沒有讀者願意記錄下所花費的時間,在評論區反饋一下。
咱們來實現一個小需求,啓動 10 個線程執行任務,因爲啓動時間有前後,咱們但願等到全部的線程都啓動成功之後再開始執行,讓每一個線程在同一個起跑線上開始執行業務操做。面試
下面,分別介紹 CountDownLatch、CyclicBarrier 和 Phaser 怎麼實現該需求。微信
一、這種 case 最容易使用的就是 CountDownLatch,代碼很簡單:併發
`// 1. 設置 count 爲 1
CountDownLatch latch = new CountDownLatch(1);函數
for (int i = 0; i < 10; i++) {工具
new Thread(() -> { try { // 2. 每一個線程都等在柵欄這裏,等待放開柵欄,不會由於有些線程先啓動就先跑路了 latch.await(); // doWork(); } catch (InterruptedException ignore) { } }).start();
}`源碼分析
doSomethingELse(); // 確保在下面的代碼執行以前,上面每一個線程都到了 await() 上。性能
// 3. 放開柵欄
latch.countDown();this
簡單回顧一下 CountDownLatch 的原理:AQS 共享模式的典型使用,構造函數中的 1 是設置給 AQS 的 state 的。latch.await() 方法會阻塞,而 latch.countDown() 方法就是用來將 state-- 的,減到 0 之後,喚醒全部的阻塞在 await() 方法上的線程。
二、這種 case 用 CyclicBarrier 來實現更簡單:spa
`// 1. 構造函數中指定了 10 個 parties
CyclicBarrier barrier = new CyclicBarrier(10);
for (int i = 0; i < 10; i++) {
executorService.submit(() -> { try { // 2. 每一個線程"報告"本身到了, // 當第10個線程到的時候,也就是全部的線程都到齊了,一塊兒經過 barrier.await(); // doWork() } catch (InterruptedException | BrokenBarrierException ex) { ex.printStackTrace(); } });
}
`
CyclicBarrier 的原理不是 AQS 的共享模式,是 AQS Condition 和 ReentrantLock 的結合使用
CyclicBarrier 能夠被重複使用,咱們這裏只使用了一個週期,當第十個線程到了之後,全部的線程一塊兒經過,此時開啓了新的一個週期,在 CyclicBarrier 中,週期用 generation 表示。
三、咱們來介紹今天的主角 Phaser,用 Phaser 實現這個需求也很簡單:
`Phaser phaser = new Phaser();
// 1. 註冊一個 party
phaser.register();
for (int i = 0; i < 10; i++) {
phaser.register(); executorService.submit(() -> { // 2. 每一個線程到這裏進行阻塞,等待全部線程到達柵欄 phaser.arriveAndAwaitAdvance(); // doWork() });
}
phaser.arriveAndAwaitAdvance();`
Phaser 比較靈活,它不須要在構造的時候指定固定數目的 parties,而 CountDownLatch 和 CyclicBarrier 須要在構造函數中明確指定一個數字。
咱們能夠看到,上面的代碼總共執行了 11 次 phaser.register() ,能夠把 11 理解爲 CountDownLatch 中的 count 和 CyclicBarrier 中的 parties。
這樣讀者應該很容易理解 phaser.arriveAndAwaitAdvance() 了,這是一個阻塞方法,直到該方法被調用 11 次,全部的線程才能同時經過。
這裏和 CyclicBarrier 是一個意思,湊齊了全部的線程,一塊兒經過柵欄。 Phaser 也有周期的概念,一個週期定義爲一個 phase,從 0 開始。
上面咱們介紹了 Phaser 中的兩個很重要的接口,register() 和 arriveAndAwaitAdvance(),這節咱們來看它的其餘的一些重要的接口使用。
畫一張圖壓着:
Phaser 仍是有 parties 概念的,可是它不須要在構造函數中指定,而是能夠很靈活地動態增減。
咱們來看 3 個代碼片斷,看看 parties 是怎麼來的。
`一、首先是 Phaser 有一個帶 parties 參數的構造方法:
public Phaser(int parties) {
this(null, parties);
}
二、register() 方法:
public int register() {
return doRegister(1);
}
這個方法會使得 parties 加 1
三、bulkRegister(int parties) 方法:
public int bulkRegister(int parties) {
if (parties < 0) throw new IllegalArgumentException(); if (parties == 0) return getPhase(); return doRegister(parties);
}`
一次註冊多個,這個方法會使得 parties 增長相應數值
parties 也能夠減小,由於有些線程可能在執行過程當中,不和你們玩了,會進行退出,調用 arriveAndDeregister() 便可,這個方法的名字已經說明了它的用途了。
再看一下這個圖,phase-1 結束的時候,黑色的線程離開了你們,此時就只有 3 個 parties 了。
這裏說一下 Phaser 的另外一個概念 phase,它表明 Phaser 中的週期或者叫階段,phase 從 0 開始,一直往上遞增。
經過調用 arrive() 或 arriveAndDeregister() 來標記有一個成員到達了一個 phase 的柵欄,當全部的成員都到達柵欄之後,開啓一個新的 phase。
這裏咱們來看看和 phase 相關的幾個方法:
一、arrive()
這個方法標記當前線程已經到達柵欄,可是該方法不會阻塞,注意,它不會阻塞。
你們要理解一點,party 本和線程是沒有關係的,不能說一個線程表明一個 party,由於咱們徹底能夠在一個線程中重複調用 arrive() 方法。這麼表達純粹是方便理解用。
二、arriveAndDeregister()
和上面的方法同樣,當前線程經過柵欄,非阻塞,可是它執行了 deregister 操做,意味着總的 parties 減 1。
三、arriveAndAwaitAdvance()
這個方法應該一目瞭然,就是等其餘線程都到了柵欄上再一塊兒經過,進入下一個 phase。
四、awaitAdvance(int phase)
這個方法須要指定 phase 參數,也就是說,當前線程會進行阻塞,直到指定的 phase 打開。
五、protected boolean onAdvance(int phase, registeredParties)
這個方法是 protected 的,因此它不是 phaser 提供的 API,從方法名字上也能夠看出,它會在一個 phase 結束的時候被調用。
它的返回值表明是否應該終結(terminate)一個 phaser,之因此拿出來講,是由於咱們常常會見到有人經過覆寫該方法來自定義 phaser 的終結邏輯,如:
`protected boolean onAdvance(int phase, int registeredParties) {
return phase >= N || registeredParties == 0;
}`
一、咱們能夠經過 phaser.isTerminated() 來檢測一個 phaser 實例是否已經終結了 二、當一個 phaser 實例被終結之後,register()、arrive() 等這些方法都沒有什麼意義了,你們能夠玩一玩,觀察它們的返回值,本來應該返回 phase 值的,可是這個時候會返回一個負數。
介紹下幾個用於返回當前 phaser 狀態的方法:
getPhase():返回當前的 phase,前面說了,phase 從 0 開始計算,最大值是 Integer.MAX_VALUE,超過又從 0 開始
getRegisteredParties():當前有多少 parties,隨着不斷地有 register 和 deregister,這個值會發生變化
getArrivedParties():有多少個 party 已經到達當前 phase 的柵欄
getUnarrivedParties():尚未到達當前柵欄的 party 數
Tiering 這個詞自己就很差翻譯,你們將就一下,要表達的意思就是,將多個 Phaser 實例構形成一棵樹。
一、第一個問題來了,爲何要把多個 Phaser 實例構形成一棵樹,解決什麼問題?有什麼優勢?
Phaser 內部用一個 state 來管理狀態變化,隨着 parties 的增長,併發問題帶來的性能影響會愈來愈嚴重。
/**
* 0-15: unarrived
* 16-31: parties, 因此一個 phaser 實例最大支持 2^16-1=65535 個 parties
* 32-62: phase, 31 位,那麼最大值是 Integer.MAX_VALUE,達到最大值後又從 0 開始
* 63: terminated
*/
private volatile long state;
一般咱們在說 0-15 位這種,說的都是從低位開始的
state 的各類操做依賴於 CAS,典型的無鎖操做,可是,在大量競爭的狀況下,可能會形成不少的自旋。
而構造一棵樹就是爲了下降每一個節點(每一個 Phaser 實例)的 parties 的數量,從而有效下降單個 state 值的競爭。
二、第二個問題,它的結構是怎樣的?
這裏咱們不講源碼,用通俗一點的語言表述一下。咱們先寫段代碼構造一棵樹:
`Phaser root = new Phaser(5);
Phaser n1 = new Phaser(root, 5);
Phaser n2 = new Phaser(root, 5);
Phaser m1 = new Phaser(n1, 5);
Phaser m2 = new Phaser(n1, 5);
Phaser m3 = new Phaser(n1, 5);
Phaser m4 = new Phaser(n2, 5);`
根據上面的代碼,咱們能夠畫出下面這個很簡單的圖:
這棵樹上有 7 個 phaser 實例,每一個 phaser 實例在構造的時候,都指定了 parties 爲 5,可是,對於每一個擁有子節點的節點來講,每一個子節點都是它的一個 party,咱們能夠經過 phaser.getRegisteredParties() 獲得每一個節點的 parties 數量:
結論應該很是容易理解,咱們來闡述一下過程。
在子節點註冊第一個 party 的時候,這個時候會在父節點註冊一個 party,注意這裏說的是子節點添加第一個 party 的時候,而不是說實例構造的時候。
在上面代碼的基礎上,你們能夠試一下下面的這個代碼:
`Phaser m5 = new Phaser(n2);
System.out.println("n2 parties: " + n2.getRegisteredParties());
m5.register();
System.out.println("n2 parties: " + n2.getRegisteredParties());`
第一行代碼中構造了 m5 實例,可是此時它的 parties == 0,因此對於父節點 n2 來講,它的 parties 依然是 6,因此第二行代碼輸出 6。第三行代碼註冊了 m5 的第一個 party,顯然,第四行代碼會輸出 7。
當子節點的 parties 降爲 0 的時候,會從父節點中"剝離",咱們在上面的基礎上,再加兩行代碼:
`m5.arriveAndDeregister();
System.out.println("n2 parties: " + n2.getRegisteredParties());`
因爲 m5 以前只有一個 parties,因此一次 arriveAndDeregister() 就會使得它的 parties 變爲 0,此時第二行代碼輸出父節點 n2 的 parties 爲 6。
還有一點有趣的是(其實也不必定有趣吧),在非樹的結構中,此時 m5 應該處於 terminated 狀態,由於它的 parties 降爲 0 了,不過在樹的結構中,這個狀態由 root 控制,因此咱們依然能夠執行 m5.register()...
三、每一個 phaser 實例的 phase 週期有快有慢,怎麼協調的?
在組織成樹的這種結構中,每一個 phaser 實例的 phase 已經不受本身控制了,由 root 來統一協調,也就是說,root 當前的 phase 是多少,每一個 phaser 的 phase 就是多少。
那又有個問題,若是子節點的一個週期很快就結束了,要進入下一個週期怎麼辦?須要等!這個時候其實要等全部的節點都結束當前 phase,由於只有這樣,root 節點纔有可能結束當前 phase。
我以爲 Phaser 中的樹結構咱們要這麼理解,咱們要把整棵樹當作一個 phaser 實例,每一個節點只是輔助用於下降併發而存在,整棵樹仍是須要知足 Phaser 語義的。
四、這種樹結構在什麼場景下會比較實用?設置每一個節點的 parties 爲多少比較合適?
這個問題留給讀者思考吧。
(全文完)
365天干貨不斷微信搜索「猿燈塔」第一時間閱讀,回覆【資料】【面試】【簡歷】有我準備的一線大廠面試資料和簡歷模板