概要
MpscGrowableArrayQueue是JCTools裏的一個工具,是對於特定場景化的定製,即MPSC(Multi-Producer & Single-Consumer),在這種場景下,相對於BlockingQueue,能夠滿足高性能的需要。
背景
JCTools是一款對jdk併發數據結構進行增強的併發工具,主要提供了map以及queue的增強數據結構。
Mpsc**ArrayQueue是由JCTools提供的一個多生產者單個消費者的數組隊列。多個生產者同時併發的訪問隊列是線程安全的,但是同一時刻只允許一個消費者訪問隊列,這是需要程序控制的,因爲MpscQueue的用途即爲多個生成者可同時訪問隊列,但只有一個消費者會訪問隊列的情況。如果是其他情況你可以使用JCTools提供的其他隊列。
應用場景
上面說了MpscGrowableArrayQueue是用於特定化場景,即MPSC。其實這種場景我們平時也會看到很多,在各種框架、工具中也有它的身影。
Netty
原來Netty還是自己寫的MpscLinkedQueueNode,後來新版本就換成使用JCTools的併發隊列了。
Netty的線程模型決定了taskQueue可以用多個生產者線程同時提交任務,但只會有EventLoop所在線程來消費taskQueue隊列中的任務。這樣JCTools提供的MpscQueue完全符合Netty線程模式的使用場景。而LinkedBlockingQueue會在生產者線程操作隊列時以及消費者線程操作隊列時都對隊列加鎖以保證線程安全性。雖然,在Netty的線程模型中程序會控制訪問taskQueue的始終都會是EventLoop所在線程,這時會使用偏向鎖來降低線程獲得鎖的代價。
Caffeine
像我的一篇文章說的(Caffeine高性能設計剖析),如果對Caffeine設置了expireAfterWrite或refreshAfterWrite,那麼每次寫操作都會把afterWrite的task放在一個MpscGrowableArrayQueue裏,之後再異步處理這些task。一般寫操作有可能併發進行,有多個生產者, 但是隻用一個線程來處理,來降低複雜度,這裏的場景就很適合mpsc了。
使用
MpscGrowableArrayQueue的使用跟其他queue類似,提供offer
, poll
, peek
等常規方法,但由於特定化的場景,由於設計上的原因,做了一點限制,相當於犧牲了一些功能,不支持這三個方法:remove(Object o),removeAll(Collection),retainAll(Collection)。
原理分析
BlockingQueue對於每次的讀寫都會使用鎖Lock來阻塞操作,這樣在高併發下會產生性能問題,影響程序的吞吐量,那麼對於這種情況的優化,很自然就會想到要把鎖去掉,採用Lock-free的設計,這是生產端的原理;對於消費端,乾脆只限制只有一個線程來使用(沒有強制限制),那麼就不存在併發問題了。
下面我們來看看MpscGrowableArrayQueue的具體實現(源碼基於jctools的3.0.0版本
):
基本屬性
MpscGrowableArrayQueue
和MpscChunkedArrayQueue
的功能差不多,他們都繼承於BaseMpscLinkedArrayQueue
類。
相對於其他Mpsc**Queue類,MpscChunkedArrayQueue根據名字可以看出它是基於數組實現,跟準確的說是數組鏈表。這點可從它的父類BaseMpscLinkedArrayQueue看出。它融合了鏈表和數組,既可以動態變化長度,同時不會像鏈表頻繁分配Node。並且吞吐量優於傳統的鏈表。
BaseMpscLinkedArrayQueue實現了絕大部分的核心功能,下面講到的源碼都在這個類裏面。
看看官方對MpscGrowableArrayQueue的定義:
1 2 3 4 5 6 7 |
/** * An MPSC array queue which starts at <i>initialCapacity</i> and grows to <i>maxCapacity</i> in linked chunks, * doubling theirs size every time until the full blown backing array is used. * The queue grows only when the current chunk is full and elements are not copied on * resize, instead a link to the new chunk is stored in the old chunk for the consumer to follow. */ public class MpscGrowableArrayQueue<E> extends MpscChunkedArrayQueue<E> |
它是一個可自動擴展容量的array,擴展時它不會像hashmap那樣把舊數組的元素複製到新的數組,而是用一個link來連接到新的數組。
BaseMpscLinkedArrayQueue
的主要幾個屬性,這幾個屬性比較分散(由於用了很多的padding類來做繼承),我把他們集中起來:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
//生產者的最大下標 private volatile long producerLimit; //生產者數組的mask,偏移量offset & mask得到在數組的位置 protected long producerMask; //生產者的buffer,由於會產生的新的數組buffer,所以生產者和消費者各自維護自己的buffer protected E[] producerBuffer; //生產者的當前下標 private volatile long producerIndex; //消費者的當前下標 private volatile long consumerIndex; //消費者數組的mask protected long consumerMask; //生產者的buffer protected E[] consumerBuffer; |
初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
/** * @param initialCapacity the queue initial capacity. If chunk size is fixed this will be the chunk size. * Must be 2 or more. * @param maxCapacity the maximum capacity will be rounded up to the closest power of 2 and will be the * upper limit of number of elements in this queue. Must be 4 or more and round up to a larger * power of 2 than initialCapacity. */ // 可以指定init size,或者不指定 public MpscChunkedArrayQueue(int initialCapacity, int maxCapacity){ super(initialCapacity, maxCapacity); } public MpscChunkedArrayQueue(int maxCapacity){ super(max(2, min(1024, roundToPowerOfTwo(maxCapacity / 8))), maxCapacity); } MpscChunkedArrayQueueColdProducerFields(int initialCapacity, int maxCapacity){ super(initialCapacity); RangeUtil.checkGreaterThanOrEqual(maxCapacity, 4, "maxCapacity"); RangeUtil.checkLessThan(roundToPowerOfTwo(initialCapacity), roundToPowerOfTwo(maxCapacity), "initialCapacity"); //數組有一個最大的限制,最大值爲Pow2(max)*2,例如你指定max=100,maxQueueCapacity=256 maxQueueCapacity = ((long) Pow2.roundToPowerOfTwo(maxCapacity)) << 1; } public BaseMpscLinkedArrayQueue(final int initialCapacity){ RangeUtil.checkGreaterThanOrEqual(initialCapacity, 2, "initialCapacity"); //初始化的size爲2的整數倍 int p2capacity = Pow2.roundToPowerOfTwo(initialCapacity); //這裏mask爲什麼不是p2capacity - 1,是因爲把最後一位當作是否resizing的標記 //所以這裏的容量是虛擴成2倍,所以後面看到下標index追加時是加2,而不是1 //而且在獲取數組的偏移量offset時,把數組的arrayIndexScale也除以了2 //所以要注意index和limit的數值都爲實際容量的2倍 long mask = (p2capacity - 1) << 1; //初始化數組 E[] buffer = allocateRefArray(p2capacity + 1); //生產者的數組指向初始化數組 producerBuffer = buffer; //buffer對應的mask producerMask = mask; //消費者的數組指向初始化數組 consumerBuffer = buffer; //(同上) consumerMask = mask; //生產者的最大值limit等於mask soProducerLimit(mask); } |
可以看出初始化時初始化了init size的數組,以及mask,limit等。
注意,producer和consumer的index初始值都爲0,這裏沒寫。
offer
一個queue需要往裏面加入元素,offer操作是其最基本的功能。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 |
public boolean offer(final E e) { if (null == e) { throw new NullPointerException(); } long mask; E[] buffer; long pIndex; while (true) { long producerLimit = lvProducerLimit(); pIndex = lvProducerIndex(); //最低位爲1說明在resize,繼續自轉等resize結束 if ((pIndex & 1) == 1) { continue; } mask = this.producerMask; buffer = this.producerBuffer; //pIndex >= producerLimit有幾種可能 //idnex小於等於limit,說明生產者的位置達到了數組的“末端”,這裏用引號是因爲實際上不是,準確來說是可允許存放元素的位置 if (producerLimit <= pIndex) { //通過offerSlowPath判斷該進行什麼操作,retry還是resize等(下面展述) int result = offerSlowPath(mask, pIndex, producerLimit); switch (result) { case CONTINUE_TO_P_INDEX_CAS: break; case RETRY: continue; case QUEUE_FULL: return false; case QUEUE_RESIZE: //擴容(下面展述) resize(mask, buffer, pIndex, e, null); return true; } } //走到這裏說明producerLimit > pIndex,即允許添加元素 //用當前的index進行cas操作來設置值,成功則退出;失敗說明存在併發,則繼續while循環 if (casProducerIndex(pIndex, pIndex + 2)) { break; } } //這裏通過arrayBaseOffset和arrayIndexScale獲取數組的偏移量 final long offset = modifiedCalcCircularRefElementOffset(pIndex, mask); //根據偏移量offset把元素設進去 soRefElement(buffer, offset, e); return true; } |
offerSlowPath方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 |
private int offerSlowPath(long mask, long pIndex, long producerLimit) { //消費者下標 final long cIndex = lvConsumerIndex(); //用當前mask獲取當前buffer的size(其實就是mask) long bufferCapacity = getCurrentBufferCapacity(mask); //如果已經有消費的話,那麼下麪條件會成立 //如果是這種情況,就需要把已經消費的空位利用起來,像ring buffer一樣 if (cIndex + bufferCapacity > pIndex) { //通過cas設置limit,limit的大小爲 if (!casProducerLimit(producerLimit, cIndex + bufferCapacity)) { //失敗則重試 return RETRY; } else { //成功則出去cas pIndex return CONTINUE_TO_P_INDEX_CAS; } } //檢查是否有空位,因爲queue在初始化時有設置maxQueueCapacity else if (availableInQueue(pIndex, cIndex) <= 0) { //返回full return QUEUE_FULL; } //來到這裏說要擴容了,把最後index的最後以爲置爲1,表示resizing else if (casProducerIndex(pIndex, pIndex + 1)) { //返回resize return QUEUE_RESIZE; } else { //說明已經resize了,返回繼續重試,等待resize完成 return RETRY; } } |
resize方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
private void resize(long oldMask, E[] oldBuffer, long pIndex, E e, Supplier<E> s) { assert (e != null && s == null) || (e == null || s != null); //newBuffer的大小爲原來的2倍 int newBufferLength = getNextBufferSize(oldBuffer); final E[] newBuffer; try { //新建newBuffer數組 newBuffer = allocateRefArray(newBufferLength); } catch (OutOfMemoryError oom) { assert lvProducerIndex() == pIndex + 1; soProducerIndex(pIndex); throw oom; } //生產者的buffer指向newBuffer producerBuffer = newBuffer; //新的mask final int newMask = (newBufferLength - 2) << 1; producerMask = newMask; //計算pIndex在舊數組的偏移 final long offsetInOld = modifiedCalcCircularRefElementOffset(pIndex, oldMask); //計算pIndex在新數組的偏移 final long offsetInNew = modifiedCalcCircularRefElementOffset(pIndex, newMask); //設置新加入的元素到新數組 soRefElement(newBuffer, offsetInNew, e == null ? s.get() : e); //舊數組的最後一個位置指向新的數組 soRefElement(oldBuffer, nextArrayOffset(oldMask), newBuffer); final long cIndex = lvConsumerIndex(); final long availableInQueue = availableInQueue(pIndex, cIndex); RangeUtil.checkPositive(availableInQueue, "availableInQueue"); //更新limit soProducerLimit(pIndex + Math.min(newMask, availableInQueue)); //更新pIndex soProducerIndex(pIndex + 2); //pIndex在舊數組的位置設置一個固定值-JUMP,來告訴要跳到下一個數組 soRefElement(oldBuffer, offsetInOld, JUMP); } |
poll
poll方法:
注意這個方法會有併發問題,所以MPSC的使用要求是一次只用一個線程來poll。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
public E poll() { final E[] buffer = consumerBuffer; final long index = lpConsumerIndex(); final long mask = consumerMask; //獲取數組的偏移量 final long offset = modifiedCalcCircularRefElementOffset(index, mask); //獲取數據 Object e = lvRefElement(buffer, offset); if (e == null) { if (index != lvProducerIndex()) { //正常來說不會爲null,但是在offer添加數據時使用putOrderedObject,即lazySet,在併發時可能會發生,所以while繼續讀取 do { e = lvRefElement(buffer, offset); } while (e == null); } //cIndex == pIndex 說明沒有數據 else { return null; } } //link到下一個數組 if (e == JUMP) { final E[] nextBuffer = nextBuffer(buffer, mask); return newBufferPoll(nextBuffer, index); } //置爲null來釋放內存 soRefElement(buffer, offset, null); soConsumerIndex(index + 2); return (E) e; } |
總結
在多生產者單消費者的場景下,使用MpscGrowableArrayQueue
可以滿足高性能的隊列讀寫要求。
MpscGrowableArrayQueue不再使用Lock來阻塞操作,而是使用CAS來操作,包括使用putOrderedObject來進行快速set、使用arrayBaseOffset和arrayIndexScale來計算數組的偏移量等等;
還有padding來解決僞共享問題。