上一篇所講的SynchronousQueue,是2個線程之間單向的資料轉送,一個put,一個take。
而今天所講的Exchange,顧明思義,是雙向的資料轉送,2個線程在一個同步點,交換資料。
其使用方式,大致如下:
Exchange<String> exchange = new Exchange<String>(); //建1個多個線程共用的exchange對象//把exchange對象,傳給4個線程對象。每個線程在自己的run函數裡面,調用exchange,把自己的資料當參數傳進去,傳回值是另外一個線程調用exchange塞進去的參數。ThreadA a = new ThreadA(exchange);run(){ String other = exchange.exchange(self.data) //沒有別的線程調用exchange的話,自己會阻塞在這。直到有別的線程調用exchange。}ThreadB b = new ThreadB(exchange);run(){ String other = exchange.exchange(self.data)}ThreadB c = new ThreadC(exchange);run(){ String other = exchange.exchange(self.data)}ThreadC d = new ThreadD(exchange);run(){ String other = exchange.exchange(self.data)}
在上面的例子中,4個線程並發的調用exchange,會兩兩互動資料。可能是A/B, C/D,也可能A/C, B/D,也可能是A/D, B/C。 Exchange實現
從簡單的角度來考慮,Exchange只需要一個互斥變數就夠了。因為可以限制,任何時候,只能有1對發生交換,不是多對,同時發生交換。
但為了提高並發度,Exchange內部用了多個變數,在其內部,稱之為Slot。
public class Exchanger<V> { private static final class Slot extends AtomicReference<Object> { long q0, q1, q2, q3, q4, q5, q6, q7, q8, q9, qa, qb, qc, qd, qe; } private volatile Slot[] arena = new Slot[CAPACITY]; 。。。 }
關鍵技術點1:CacheLine填充
在上面的代碼中,Slot其實就是一個AtomicReference,其裡面的q0, q1,..qd那些變數,都是多餘的,不用的。那為什麼要添加這些多餘的變數呢。
是為了讓不同的Slot不要落在cpu的同一個CacheLine裡面。因為cpu從記憶體讀取資料的時候,不是一個位元組一個位元組的讀,而是按塊讀取,這裡的塊也就是“CacheLine”,一般一個CacheLine大小是64Byte。
保證一個Slot的大小 >= 64Byte,這樣更改一個Slot,就不會導致另外一個Slot的cpu cache失效,從而提高效能。
知道了Slot其實就是一個AtomicReference,下面討論第2個技術點 關鍵技術點2:鎖分離
同ConcurrentHashMap類型,Exchange沒有只定義一個slot,而是定義了一個slot的數組。這樣在多線程調用exchange的時候,可以各自在不同的slot裡面進行匹配。
exchange的基本思路如下:
(1)根據每個線程的thread id, hash計算出自己所在的slot index;
(2)如果運氣好,這個slot被人佔著(slot裡面有node),並且有人正在等待交換,那就和它進行交換;
(3)slot為空白的(slot裡面沒有node),自己佔著,等人交換。沒人交換,向前挪個位置,把當前slot裡面內容取消,index減半,再看有沒有交換;
(4)挪到0這個位置,還沒有人互動,那就阻塞,一直等著。別的線程,也會一直挪動,直到0這個位置。
所以0這個位置,是一個交易的“終結點”位置。別的位置上找不到人交易,最後都會到0這個位置。
下面是exchange的原始碼:
public V exchange(V x) throws InterruptedException { if (!Thread.interrupted()) { Object v = doExchange(x == null? NULL_ITEM : x, false, 0); if (v == NULL_ITEM) return null; if (v != CANCEL) return (V)v; Thread.interrupted(); // Clear interrupt status on IE throw } throw new InterruptedException(); } private Object doExchange(Object item, boolean timed, long nanos) { Node me = new Node(item); int index = hashIndex(); //根據thread id計算出自己要去的那個交易位置(slot) int fails = 0; for (;;) { Object y; Slot slot = arena[index]; if (slot == null) createSlot(index); //slot = null,建立一個slot,然後會回到for迴圈,再次開始 else if ((y = slot.get()) != null && //slot裡面有人等著(有Node),則嘗試和其交換 slot.compareAndSet(y, null)) { //關鍵點1:slot清空,Node拿出來,倆人在Node裡面互動。把Slot讓給後面的人,做互動地點 Node you = (Node)y; if (you.compareAndSet(null, item)) {//把Node裡面的東西,換成自己的 LockSupport.unpark(you.waiter); //喚醒對方 return you.item; //自己把對方的東西拿走 } //關鍵點2:如果你運氣不好,在Node裡面要交換的時候,被另一個線程搶了,回到for迴圈,重新開始 } else if (y == null && //slot裡面為空白(沒有Node),則自己把位置佔住 slot.compareAndSet(null, me)) { if (index == 0) //如果是0這個位置,自己阻塞,等待別人來交換 return timed? awaitNanos(me, slot, nanos): await(me, slot); Object v = spinWait(me, slot); //不是0這個位置,自旋等待 if (v != CANCEL) //自旋等待的時候,運氣好,有人來交換了,返回 return v; me = new Node(item); //自旋的時候,沒人來交換。走執行下面的,index減半,挪個位置,重新開始for迴圈 int m = max.get(); if (m > (index >>>= 1)) max.compareAndSet(m, m - 1); } else if (++fails > 1) { //失敗 case1: slot有人,要互動,但被人家搶了 case2: slot沒人,自己要佔位置,又被人家搶了 int m = max.get(); if (fails > 3 && m < FULL && max.compareAndSet(m, m + 1)) index = m + 1; //3次匹配失敗,把index擴大,再次開始for迴圈 else if (--index < 0) index = m; } } }
注意:上面唯一有點繞的地方,是Slot裡面套了個Node。雙方的交易地點,準確說,不是在Slot裡面,是在Slot的Node裡面。
當Slot的Node = null時候,代表Slot為空白,建立一個Node佔住;
當Slot的Node 。= null的時候,把Node拿出來,雙方在Node裡面交換,此時Slot已經釋放出去了。 總結
Exchanger的代碼雖然少,但還是蠻繞的,總結下來,有以下幾個關鍵點:
(1) Cache Line填充技術
(2) 鎖分離
(3) 倆人還沒交換結束的時候,就把slot給釋放出去了。雙方拿著那個Node,在Node裡面互動的。也就是上面的for迴圈裡面的第2個分支。
這裡面有一種情況就是:運氣不好,2人正要在Node裡面交換的時候,被另外一個線程搶了。那這個沒搶到的,只能for迴圈,重新開始
(4)0是交換的終結點位置:沒有人交換的時候,會不斷挪位置,挪的時候,要把當前位置的slot清空掉,一個人不能同時佔2個Slot。直到挪到0這個位置,一直等待。