高性能隊列——Disruptor學習

Disruptor是什麼

Disruptor是一個由英國外匯交易公司LMAX研發並開源的高性能的有界內存隊列,其主要用於在線程之間完成數據的傳遞。github地址
那麼,以高性能著稱的Disruptor到底有多快呢
我將常用的2種線程安全隊列(ArrayBlockingQueue和LinkedBlockingQueue)與Disruptor作了個簡單對比對比,場景是啓動兩個線程,一個線程往隊列填充自增數字,另一個線程取數字進行累加,其對比結果如下:

1000w
ArrayBlockingQueue耗時:927ms
LinkedBlockingQueue耗時:1495ms
Disruptor耗時:598ms
5000w
ArrayBlockingQueue耗時:4044ms
LinkedBlockingQueue耗時:11145ms
Disruptor耗時:2824ms
1e
ArrayBlockingQueue耗時:7514ms
LinkedBlockingQueue耗時:23144ms
Disruptor耗時:4668ms

可以看到,Disruptor在速度上較其他兩個隊列是有着明顯的優勢的。

爲什麼可以這麼快

內存預分配

在Disruptor裏,底層存儲爲數組結構,而事件(Event)作爲真實數據的一個載體,在初始化時會調用預設的EventFactory創建對應數量的Event填充數組,加上其環形數組的設計,數組中的Event對象可以很方便地實現複用,這在一定程度可以減少GC的次數,提升了性能。

private void fill(EventFactory<E> eventFactory){
    for (int i = 0; i < bufferSize; i++){
        entries[BUFFER_PAD + i] = eventFactory.newInstance();
    }
}

消除“僞共享”,充分利用硬件緩存

什麼是“僞共享”

內存與CPU之間存在着多級緩存,L3,L2,L1,而越靠近CPU核心,速度也越快,爲也提高處理速度,處理器不直接與內存通信,而是先將內存的數據讀到內部緩存再進行操作。在多核心處理器下,爲了保證各個核心的緩存是一致的,會實現緩存一致性協議。
而僞共享指的是由於共享緩存行(通常爲64個字節)導致緩存無效的場景:
僞共享
就上圖而言,線程1和線程2運行分別運行在兩個核心上,線程1對putIndex讀寫,線程2對takeIndex讀寫,由於putIndex與takeIndex內存的相鄰性,在加載到緩存時將被讀到同一個緩存行中,而由於對其中一個變量的寫操作會使緩存回寫到主存,造成整個緩存行的失效,這也導致了同處於同一個緩存行的其他變量的緩存失效。

它是如何被消除的

一方面,底層採用數組結構,CPU在加載數據時,會根據空間局部性原理,把相鄰的數據一起加載進來,由於由於數組上結構的內存分配是連續的,也就能更好地利用CPU的緩存;
另一方面,通過增加無意義變量,增大變量間的間隔,使得一個變量可以獨佔一個緩存行,以空間換取時間(注: Java 8 可以使用@Contended註解,配合JVM參數-XX:-RestrictContended,來消除“僞共享”):

class LhsPadding
{
	//7*8個字節
    protected long p1, p2, p3, p4, p5, p6, p7;
}

class Value extends LhsPadding
{
    protected volatile long value;
}

class RhsPadding extends Value
{
	//7*8個字節
    protected long p9, p10, p11, p12, p13, p14, p15;
}

無鎖數據結構RingBuffer

RingBuffer
RingBuffer作爲Disruptor的底層數據結構,其內部有一個cursor變量,表示當前可讀的最大下標,cursor是Sequence類的一個對象,其內部維護了一個long類型的value成員,value使用了volatile修飾,在不使用鎖的前提下保證了線程之間的可見性,並通過Unsafe工具封裝了對value變量的CAS系列操作。
關於volatile變量,有以下兩個特性:
可見性:對一個volatile變量讀,總能看到(任意線程)對這個變量的最後寫入;
原子性:對任意單個volatile變量的讀/寫具有原子性;

public class Sequence extends RhsPadding
{
	static final long INITIAL_VALUE = -1L;
    private static final Unsafe UNSAFE;
    private static final long VALUE_OFFSET;
	...
}

數據寫入

RingBuffer數據的寫入分爲兩個階段,在第一階段會先申請下一個可寫入節點(cursor+1),多寫入者模式下通過CAS操作移動cursor,來保存線程安全性;第二階段,數據提交,提交時爲保證順序寫,需要保證cursor追上當前提交的寫入位置。
寫入成功後,再調用具體的WaitStrategy實現通知其他消費線程RingBuffer數據寫入

數據讀取

在讀取數據的時候,多個消費者可以同時消費,每個消費者都會維護有一個讀取位置,在沒有可讀數據時,通過具體的WaitStrategy進行等待(阻塞等待或自旋等)。
RingBuffer數據讀取

簡單上手(生產者-消費者模型)

public class DisruptorStart {

    public static void main(String[] args) throws Exception {
        // RingBuffer大小,2的冪次
        int bufferSize = 1024;

        // 創建Disruptor
        Disruptor<LongEvent> disruptor = new Disruptor<>(
                LongEvent::new,
                bufferSize,
                DaemonThreadFactory.INSTANCE);

        // 事件消費
        disruptor.handleEventsWith((event, sequence, endOfBatch) -> System.out.println("Event: " + event));

        // 啓動
        disruptor.start();

        // 拿到RingBuffer,用於向隊列傳輸數據
        RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();

        ByteBuffer bb = ByteBuffer.allocate(8);
        for (long l = 0; true; l++) {
            bb.putLong(0, l);
            //往隊列填充數據
            ringBuffer.publishEvent((event, sequence, buffer) -> event.set(buffer.getLong(0)), bb);
            Thread.sleep(1000);
        }
    }

}

參考:
併發框架Disruptor譯文
高性能隊列——Disruptor
Disruptor系列3:Disruptor樣例實戰

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章