併發程式設計之 Exchanger 原始碼分析

莫那·魯道發表於2019-03-04

前言

JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 還有一個重要的工具,只不過相對而言使用的不多,什麼呢? Exchange —— 交換器。用於在兩個執行緒之間交換資料,A 執行緒將 a 資料交給 B 執行緒,B 執行緒將 b 資料交給 a 執行緒。

具體使用例子參見 併發程式設計之 執行緒協作工具類。我們這篇文章就不再講述如何使用了。

而今天,我們將從原始碼處分析,Exchange 的實現原理。如果大家看過之前關於 SynchronousQueue 的文章 併發程式設計之 SynchronousQueue 核心原始碼分析,就能夠看的出來,Exchange 的原理和他很類似。

1. 原始碼

類 UML:

image.png

內部有 2 個內部類: Node , Participant 重寫了 ThreadLocal 的 initialValue 方法。

構造方法如下:

public Exchanger() {
    participant = new Participant();
}

static final class Participant extends ThreadLocal<Node> {
    public Node initialValue() { return new Node(); }
}
複製程式碼

就是建立了一個 ThreadLocal 物件,並設定了初始值,一個 Node 物件。

看看這個 node 物件:

@sun.misc.Contended 
static final class Node {
    int index;              //  node 在 arena 陣列下標
    int bound;              //  交換器的最後記錄值 
    int collides;           //  記錄的 CAS 失敗數
    int hash;               //  偽隨機的自旋數
    Object item;            //  這個執行緒的資料項
    volatile Object match;  //  別的執行緒提供的元素,也就是釋放他的執行緒提供的資料 item
    volatile Thread parked; //  當阻塞時,設定此執行緒,不阻塞的話就不必了(因為會自旋)
}
複製程式碼

這個 node 物件就是 A ,B 執行緒實際儲存資料的容器。A 執行緒存在 item 屬性上,B 執行緒儲存在 match 執行緒上,稱為匹配。同時,有個執行緒物件,你應該猜到做什麼用處的吧,對,掛起執行緒的。

和 SynchronousQueue 的區別在於, SynchronousQueue 使用了一個變數來儲存資料項,通過 isData 來區別 “存” 操作和 “取” 操作。而 Exchange 使用了 2 個變數,就不用使用 isData 來區分了。

我們再來看看 Exchange 的唯一重要方法 : exchange 方法。

2. exchange 方法原始碼分析

程式碼如下:

public V exchange(V x) throws InterruptedException {
    Object v;
    Object item = (x == null) ? NULL_ITEM : x; // translate null args
    // arena 不是 Null ,返回的卻是 null, 說明執行緒中斷了.
    // 如果 arena 是 null, 就執行後面的方法.反之,如果不是 null, 執行沒有意義.
    // 注意,當 slotExchange 有機會被執行,且返回的不是 null, 這個表示式整個就是 false, 下面的表示式就不會執行了.
    // 也就是說,當 slot 有效的時候, arena 是沒有必要執行的.
    if ((arena != null || (v = slotExchange(item, false, 0L)) == null) &&
        // 執行緒中斷了,或者返回的是 null. 說明執行緒中斷了
        // 如果執行緒沒有中斷 ,就執行後面的方法.
        ((Thread.interrupted() || (v = arenaExchange(item, false, 0L)) == null))){
        throw new InterruptedException();        
    }
    return (v == NULL_ITEM) ? null : (V)v;
}
複製程式碼

說一下方法的邏輯:

  1. 如果執行 slotExchange 有結果,就不再執行 arenaExchange.
  2. 如果 slot 被佔用了,就執行 arenaExchange.

返回值是什麼呢?返回值就是對方執行緒的資料項,如果 A 執行緒先呼叫,那麼 A 執行緒將資料項存在 item 中,B 執行緒後呼叫,則 B 執行緒將資料存在 match 屬性中。

A 返回的是 match 屬性,b 返回的是 item 屬性。

從該方法中,可以看到,有 2 個重要的方法: slotExchange, arenaExchange。先簡單說說這兩個方法。

當沒有多執行緒併發操作 Exchange 的時候,使用 slotExchange 就足夠了。 slot 是一個 node 物件。

當出現併發了,一個 slot 就不夠了,就需要使用一個 node 陣列 arena 操作了。

so,我們先看看 slotExchange 方法吧,兩個方法的邏輯類似。

3. slotExchange 方法原始碼分析

程式碼加註釋如下:

 private final Object slotExchange(Object item, boolean timed, long ns) {
        Node p = participant.get(); // 從 ThreadLocal 中取出 node 物件
        Thread t = Thread.currentThread();// 當前執行緒
        if (t.isInterrupted()) // preserve interrupt status so caller can recheck
            return null;

        for (Node q;;) {// 死迴圈
            // 另一個下執行緒進入這裡, 假設 slot 有值
            if ((q = slot) != null) {
                // 將  slot 修改為 null
                if (U.compareAndSwapObject(this, SLOT, q, null)) {
                    // 拿到 q 的 item
                    Object v = q.item;
                    // 自己的 item 賦值給 match,以讓對方執行緒獲取
                    q.match = item;
                    // q 執行緒
                    Thread w = q.parked;
                    // slot 的  parked 就是阻塞等待的執行緒物件.
                    if (w != null)
                        U.unpark(w);
                    // 返回了上一個執行緒放入的 item
                    return v;
                }
                // 如果使用 CAS 修改slot 失敗了,說明 slot 被使用了,那就需要建立 arena 陣列了
                if (NCPU > 1 && bound == 0 &&
                    U.compareAndSwapInt(this, BOUND, 0, SEQ)) // SEQ == 256; 預設 BOUND == 0
                    arena = new Node[(FULL + 2) << ASHIFT];// length = (2 + 2) << 7 == 512
            }
            // 如果 slot 是 null, 但 arena 有值了,說明有執行緒競爭 slot 了,返回 null, 執行 arenaExchange 邏輯
            else if (arena != null)
                return null; // caller must reroute to arenaExchange
            else {// 第一次迴圈,給 p node 的 item 賦值
                p.item = item;
                // 將 slot 賦值賦值為 p
                if (U.compareAndSwapObject(this, SLOT, null, p))
                    // 賦值成功跳出迴圈
                    break;
                // 如果 CAS 失敗,將 p 的值清空,重來
                p.item = null;
            }
        }
        // 當走到這裡的時候,說明 slot 是 null, 且 arena 不是 null(沒有多執行緒競爭使用 slot),並且成功將 item 放入了 slot 中.
        // 這個時候要做的就是阻塞自己,等待對方取出 slot 的資料項,然後重置 slot 的資料和池化物件的資料
        // 偽隨機數
        int h = p.hash;
        // 超時時間 
        long end = timed ? System.nanoTime() + ns : 0L;
        // 自旋,預設 1024
        int spins = (NCPU > 1) ? SPINS : 1;
        Object v;
        // 如果這個值不是 null, 說明資料被其他執行緒拿走了, 並且其他執行緒將資料賦值給 match 屬性,完成了一次交換
        while ((v = p.match) == null) {
            // 自旋
            if (spins > 0) {
                // 計算偽隨機數
                h ^= h << 1; h ^= h >>> 3; h ^= h << 10;
                // 如果算出來的是0,就使用執行緒 ID
                if (h == 0)
                    h = SPINS | (int)t.getId();
                // 如果不是0,就將自旋數減一,並且讓出 CPU 時間片
                else if (h < 0 && (--spins & ((SPINS >>> 1) - 1)) == 0)
                    Thread.yield();
            }
            // 如果自旋數不夠了,且 slot 還沒有得到,就重置自旋數
            else if (slot != p)
                spins = SPINS;
            // 如果 slot == p 了,說明對 slot 賦值成功
            // 如果執行緒沒有中斷 && 陣列不是 null && 沒有超時限制
            else if (!t.isInterrupted() && arena == null &&
                     (!timed || (ns = end - System.nanoTime()) > 0L)) {
                // 為執行緒中的 parkBlocker 屬性賦值為 Exchange 自己
                U.putObject(t, BLOCKER, this);
                // node 節點的阻塞執行緒為當前執行緒
                p.parked = t;
                // 如果這個資料還沒有被拿走,阻塞自己
                if (slot == p)
                    U.park(false, ns);
                // 執行緒甦醒後,將 p 的阻塞執行緒屬性清空
                p.parked = null;
                // 將當前執行緒的 parkBlocker 屬性設定成 null
                U.putObject(t, BLOCKER, null);
            }
            // 如果有超時限制,使用 CAS 將 slot 從 p 變成 null,取消這次交換
            else if (U.compareAndSwapObject(this, SLOT, p, null)) {
                // 如果CAS成功,如果時間到了 && 執行緒沒有中斷 : 返回 time_out 物件: 返回 null
                v = timed && ns <= 0L && !t.isInterrupted() ? TIMED_OUT : null;
                // 跳出內層迴圈
                break;
            }
        }
        // 將 p 的 match 屬性設定成 null, 表示初始化狀態,沒有任何匹配  >>>  putOrderedObject是putObjectVolatile的記憶體非立即可見版本.
        U.putOrderedObject(p, MATCH, null);
        // 重置 item
        p.item = null;
        // 保留偽隨機數,供下次種子數字
        p.hash = h;
        // 返回
        return v;
    }
複製程式碼

原始碼還是有點小長的。簡單說說邏輯。

Exchange 使用了物件池的技術,將物件儲存在 ThreadLocal 中,這個物件(Node)封裝了資料項,執行緒物件等關鍵資料。

當第一個執行緒進入的時候,會將資料放到 池化物件中,並賦值給 slot 的 item.並阻塞自己(通常不會立即阻塞,而是使用 yield 自旋一會兒),等待對方取值.

當第二個執行緒進入的時候,會拿出儲存在 slot item 中的值, 然後對 slot 的 match 賦值,並喚醒上次阻塞的執行緒.

當第一個執行緒阻塞被喚醒後,說明對方取到值了,就獲取 slot 的 match 值, 並重置 slot 的資料和池化物件的資料,並返回自己的資料.

如果超時了,就返回 Time_out 物件.

如果執行緒中斷了,就返回 null.

在該方法中,會返回 2 種結果,一是有效的 item, 二是 null— 要麼是執行緒競爭使用 slot 了,建立了 arena 陣列,要麼是執行緒中斷了.

用一幅圖來看看具體邏輯,其實還是挺簡單的。

image.png

當 slot 被別是執行緒使用了,那麼就需要建立一個 arena 的陣列了。通過操縱陣列裡面的元素來實現資料交換。

關於 arenaExchange 方法的原始碼我就不貼了,有 2 個原因,一個是總體邏輯和 slotExchange 相同,第二個原因則是,其中有一些細節我沒有弄懂,就不發出自己寫程式碼註釋了,防止誤導。但我們已經掌握了 Exchange 的原理。

總結

Exchange 和 SynchronousQueue 類似,都是通過兩個執行緒操作同一個物件實現資料交換,只不過就像我們開始說的,SynchronousQueue 使用的是同一個屬性,通過不同的 isData 來區分,多執行緒併發時,使用了佇列進行排隊。

Exchange 使用了一個物件裡的兩個屬性,item 和 match,就不需要 isData 屬性了,因為在 Exchange 裡面,沒有 isData 這個語義。而多執行緒併發時,使用陣列來控制,每個執行緒訪問陣列中不同的槽。

最後,用我們的圖收尾吧:

image.png

相關文章