併發王者課-鉑金9:互通有無-Exchanger如何完成執行緒間的資料交換

秦二爺發表於2021-07-08

歡迎來到《併發王者課》,本文是該系列文章中的第22篇,鉑金中的第9篇

在前面的文章中,我們已經介紹了ReentrantLockCountDownLatchCyclicBarrierSemaphore等同步工具。在本文中,將為你介紹最後一個同步工具,即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推送行業深度好文。

如果本文對你有幫助,歡迎點贊關注監督,我們一起從青銅到王者

相關文章