歡迎來到《併發王者課》,本文是該系列文章中的第22篇,鉑金中的第9篇。
在前面的文章中,我們已經介紹了ReentrantLock,CountDownLatch,CyclicBarrier,Semaphore等同步工具。在本文中,將為你介紹最後一個同步工具,即Exchanger.
Exchanger用於兩個執行緒在某個節點時進行資料交換。在用法上,Exchanger並不複雜,但是實現上會稍微有點費解。所以,考慮到Exchanger在平時使用的場景並不多,況且多數讀者對一些“枯燥”的原始碼的耐受度有限(可能引起不適或煩躁等不良情緒,阻礙學習),本文將側重講它的使用和思想,對於原始碼不會過多展開,點到為止。
一、Exchanger的使用場景
在峽谷中,鎧和蘭陵王都是擅長打野的英雄,各自對野怪的偏好也不完全相同。所以,為了能得到自己想要的野怪,他們經常會在峽谷的交易中心交換各自的獵物。
這一天,鎧打到了一隻棕熊,而蘭陵王則收穫了一隻野狼,並且彼此都想要對方的野怪。於是,他們約定在峽谷交易中心交換雙方的野怪,誰先到了就先等會。這個過程,可以用下面這幅圖來表示:
在鎧和蘭陵王交換獵物的過程中,有三個點需要你留意:
- 交換的雙方有明確的交易地點(峽谷交易中心);
- 交換的雙方具有明確的交易物件(比如棕熊和野狼);
- 誰先到了就等會兒(他們中總會有先來後到)。
如果用程式碼來實現的話,也是有多種方式可以選擇,比如前面所學過的同步方法等。不過,雖然做也是可以做的,只是沒那麼方便。所以,接下來我們就用Exchanger來實現這一過程。
在下面的程式碼中,我們定義了一個exchanger
,它就類似於峽谷交易中心,而它的型別Exchanger<WildMonster>
則明確表示交換的物件是野怪。
接著,我們再定義兩個執行緒,分別代表鎧和蘭陵王。在其執行緒的內部,會通過前面定義的exchanger
物件來和對方進行交換資料。交換完成後,他們彼此將獲得對方的物品。
public static void main(String[] args) {
Exchanger<WildMonster> exchanger = new Exchanger<> (); // 定義交換地點和交換型別
Thread 鎧 = newThread("鎧", () -> {
try {
WildMonster wildMonster = new Bear("棕熊");
say("我手裡有一隻:" + wildMonster.getName());
WildMonster exchanged = exchanger.exchange(wildMonster); // 交換後將獲得對方的物品
say("交易完成,我獲得了:", wildMonster.getName(), "->", exchanged.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
Thread 蘭陵王 = newThread("蘭陵王", () -> {
try {
WildMonster wildMonster = new Wolf("野狼");
say("我手裡有一隻:" + wildMonster.getName());
WildMonster exchanged = exchanger.exchange(wildMonster);
say("交易完成,我獲得了:", wildMonster.getName(), "->", exchanged.getName());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
鎧.start();
蘭陵王.start();
}
下面是上面程式碼用到的內部類:
@Data
private static class WildMonster {
protected String name;
}
private static class Wolf extends WildMonster {
public Wolf(String name) {
this.name = name;
}
}
private static class Bear extends WildMonster {
public Bear(String name) {
this.name = name;
}
}
示例程式碼執行結果如下:
鎧:我手裡有一隻:棕熊
蘭陵王:我手裡有一隻:野狼
蘭陵王:交易完成,我獲得了:野狼->棕熊
鎧:交易完成,我獲得了:棕熊->野狼
Process finished with exit code 0
從結果中可以看到,鎧用棕熊換到了野狼,而蘭陵王則用野狼換到了棕熊,他們完成了交換。
以上就是Exchanger的用法,看起來還是非常簡單的,事實上也確實很簡單。在使用Exchanger的時候要注意下面幾點:
- 定義Exchanger物件,各執行緒通過這個物件完成交換;
- 在Exchanger物件中要定義型別,也就是這兩個執行緒要交換什麼;
- 執行緒在呼叫Exchanger進行交換時,要特別注意的是,先到的那個執行緒會原地等待另外一個執行緒的出現。比如,鎧先到交換地點,可這時候蘭陵王還沒有到,那麼鎧會等待蘭陵王的出現,除非超過設定的時間限制,比如蘭陵王中途被妲己蹲了草叢。反之亦然,蘭陵王先到也到等鎧的出現。
二、Exchanger的原始碼與實現
雖然理解Exchanger的思想很容易,瞭解其用法也很簡單,但是若要理清它幾百餘行的原始碼卻並非易事。其原因在於,槽是Exchanger中的核心概念和屬性,Exchanger中的資料交換分為單槽交換和多槽交換,其中單槽交換原始碼簡單,但多槽交換卻很複雜。所以,下文對Exchanger原始碼的闡述以概括為主,不會對原始碼深究。如果你有興趣,可以參考閱讀這篇文章,作者對其原始碼的解讀較為詳細。
1. 核心構造
與其他同步工具不同的是,Exchanger有且僅有一個建構函式。在這個構造中,也只初始化了一個物件participant
.
public Exchanger() {
participant = new Participant();
}
從繼承關係看,Participant本質上是一個ThreadLocal,而其中的Node則是執行緒的本地變數。
static final class Participant extends ThreadLocal<Node> {
public Node initialValue() {
return new Node();
}
}
2. 核心屬性
Exchanger有四個核心變數,如下所示。當然,除此之外,還有一些用以計算的其他變數。不過,為避免引入不必要的複雜度,本文暫不提及。
//ThreadLocal變數,每個執行緒都有自己的一個副本
private final Participant participant;
//多槽位,高併發下使用,儲存待匹配的Node例項
private volatile Node[] arena;
//單槽位,arena未初始化時使用的儲存待匹配的Node例項
private volatile Node slot;
//初始值為0,當建立arena後會被賦值成SEQ,用來記錄arena陣列的可用最大索引,會隨著併發的增大而增大直到等於最大值FULL,會隨著並行的執行緒逐一匹配成功而減少恢復成初始值
private volatile int bound;
Node的具體細節,注意其中的item和match.
@sun.misc.Contended static final class Node {
int index; //arena的下標,多個槽位的時候使用
int bound; // 上一次記錄的Exchanger.bound
int collides; // 記錄的 CAS 失敗數
int hash; // 用於自旋
Object item; // 這個執行緒的資料項
volatile Object match; // 交換的資料
volatile Thread parked; // 當阻塞時,設定此執行緒,不阻塞的話會自旋
}
3. 核心方法
// 交換資料
// 如果一個執行緒達到後,會等待其他執行緒的到達(除非自己被中斷)。然後,該執行緒會和到達的執行緒交換資料。
// 如果執行緒在到達後,已經有其他執行緒在等待。那麼,將會喚起該執行緒並交換資料。
public V exchange(V x) throws InterruptedException {...}
//帶有超時限制的交換
public V exchange(V x, long timeout, TimeUnit unit) throws InterruptedException, TimeoutException {...}
所以,從原始碼上看上文的示例,那麼鎧和蘭陵王交換資料的過程應該是下面這樣的:
小結
以上就是關於Exchanger的全部內容。在學習Exchanger時,要側重理解它所要解決的問題場景,以及它的基本用法。對於其原始碼,當前階段可以選擇“不求甚解”,以降維的方式降低學習難度,日後再循序漸進理解。我在寫本文時,也曾多次考慮是否要講清楚原始碼,最終還是決定暫緩,畢竟現階段理解它、學會它才是重點。
正文到此結束,恭喜你又上了一顆星✨
夫子的試煉
- 使用Exchanger實現生產者與消費者。
延伸閱讀與參考資料
關於作者
關注【技術八點半】,及時獲取文章更新。傳遞有品質的技術文章,記錄平凡人的成長故事,偶爾也聊聊生活和理想。早晨8:30推送作者品質原創,晚上20:30推送行業深度好文。
如果本文對你有幫助,歡迎點贊、關注、監督,我們一起從青銅到王者。