前言
JUC 包中除了 CountDownLatch, CyclicBarrier, Semaphore, 還有一個重要的工具,只不過相對而言使用的不多,什麼呢? Exchange —— 交換器。用於在兩個執行緒之間交換資料,A 執行緒將 a 資料交給 B 執行緒,B 執行緒將 b 資料交給 a 執行緒。
具體使用例子參見 併發程式設計之 執行緒協作工具類。我們這篇文章就不再講述如何使用了。
而今天,我們將從原始碼處分析,Exchange 的實現原理。如果大家看過之前關於 SynchronousQueue 的文章 併發程式設計之 SynchronousQueue 核心原始碼分析,就能夠看的出來,Exchange 的原理和他很類似。
1. 原始碼
類 UML:
內部有 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;
}
複製程式碼
說一下方法的邏輯:
- 如果執行 slotExchange 有結果,就不再執行 arenaExchange.
- 如果 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 陣列,要麼是執行緒中斷了.
用一幅圖來看看具體邏輯,其實還是挺簡單的。
當 slot 被別是執行緒使用了,那麼就需要建立一個 arena 的陣列了。通過操縱陣列裡面的元素來實現資料交換。
關於 arenaExchange 方法的原始碼我就不貼了,有 2 個原因,一個是總體邏輯和 slotExchange 相同,第二個原因則是,其中有一些細節我沒有弄懂,就不發出自己寫程式碼註釋了,防止誤導。但我們已經掌握了 Exchange 的原理。
總結
Exchange 和 SynchronousQueue 類似,都是通過兩個執行緒操作同一個物件實現資料交換,只不過就像我們開始說的,SynchronousQueue 使用的是同一個屬性,通過不同的 isData 來區分,多執行緒併發時,使用了佇列進行排隊。
Exchange 使用了一個物件裡的兩個屬性,item 和 match,就不需要 isData 屬性了,因為在 Exchange 裡面,沒有 isData 這個語義。而多執行緒併發時,使用陣列來控制,每個執行緒訪問陣列中不同的槽。
最後,用我們的圖收尾吧: