1、起源
Disruptor最初由lmax.com開發,2010年在Qcon公開發表,並於2011年開源,其官網定義為:“High Performance Inter-Thread Messaging Library”,即:線程間的高性能消息框架。其實JDK已經為我們提供了很多開箱即用的線程間通信的消息隊列,如:ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue等,這些都是基於無鎖的CAS設計。
那么Disruptor為什么還有存在的意義呢?其實無鎖並不代表沒有競爭,所以當高並發寫或者讀的時候,這些工具類一樣會面臨資源爭用的極限性能問題。而lmax.com作為一家頂級外匯交易商,其交易系統需要處理的並發量非常巨大,對響應延遲也非常敏感。在這種背景下,Disruptor誕生了,它的核心思想就是:把多線程並發寫的線程安全問題轉化為線程本地寫,即:不需要做同步。同時,lmax公司基於Disruptor構建的交易系統也多次斬獲金融界大獎。
2、發展
框架很輕量
Disruptor非常輕量,整個框架最新版3.4.2也才70
多個類,但性能卻非常強悍。得益於其優秀的設計,和對計算機底層原理的運用,官網說的:mechanical sympathy,我翻譯成硬件偏向
或者面向硬件編程
。同時它跟我們常見的MQ不一樣,這里說的線程間其實就是同一個進程內,不同線程間的消息傳遞,跟JDK中的那些阻塞和並發隊列的用法是一樣的,也就是說它們不會誇進程。
性能很厲害
- 比JDK的ArrayBlockingQueue性能高近一個數量級
- 單線程每秒能處理超
600W
的數據(處理600W並非是消費者消費完600W的數據,而是說Disruptor能在1秒內將600W數據發送給消費者,換句話說,不是600W的TPS,而是每秒600W的派發。再有,其實600W是Disruptor剛發布時硬件的水平了,現在在個人PC上也能輕松突破2000W)(為什么這里要強調單線程呢??為什么單線程的性能反而會更高呢??) - 基於
事件驅動
模型,不用消費者主動拉取消息
應用很廣泛
Apache Storm、Apache Camel、Log4j2(見:org.apache.logging.log4j.core.async. AsyncLoggerDisruptor)等都在用。(怎么最快在你的項目里用上Disruptor呢?日志框架換成Log4j2,然后打開異步就可以了)
3、核心類
主要核心類只有這6個:
簡單使用方法可以參考: https://github.com/hiccup234/web-advanced/blob/master/disruptor-client/src/main/java/top/hiccup/disruptor/SampleTest.java
4、有多快?
JDK自帶的隊列都是優秀程序員的智慧結晶,性能也是非常的強悍,下圖是其特點對比和總結:
同時Disruptor在這樣強悍的基礎上把性能提升了近一個數量級,這是非常了不起的(-- 就像要把我的存款增長10倍相對容易,但要讓東哥的身價再漲一番就難了)通過上圖我們可以看到,無鎖的方式一般都是無界的(無法保證隊列的長度在確定的范圍內),加鎖的方式,可以實現有界隊列。
但是,在穩定性要求特別高的系統中,為了防止生產者速度過快,導致內存溢出,只能選擇有界隊列。所以我們綜合一下,JDK的一眾隊列中,跟Disruptor最匹配的就是ArrayBlockingQueue
了。
沒有對比就沒有傷害
這是我本機測試的幾個隊列的性能對比,測試程序見:https://github.com/hiccup234/web-advanced/tree/master/disruptor-client
可見Disruptor在單線程情況下吞吐量竟能達到2500W以上,遠遠超過其他隊列。在多生產者的情況下,這幾個隊列的吞吐量卻是一樣的(說明隊列在多線程環境下,性能瓶頸並不在其本身)
再看Log4j2官網的性能測試截圖:
大家注意最右邊的64線程,吞吐量比最左邊的單線程高了不少,為什么這里多線程的吞吐量反而更好?是上面我的多線程測試程序有問題嗎?
其實不是的,這是Disruptor更有魅力的一個特點:RingBuffer有一個重載的next方法,即:一次為當前線程分配多個事件槽,一個線程一次性批量生產多個事件。這樣在極限性能的情況下就可以大大減少線程間的上下文的切換,畢竟線程調度對JVM來說是很重的一個操作,也是上上圖中各隊列的多線程性能瓶頸所在。
5、為什么那么快?
Disruptor為什么這么快呢?我主要總結了這3點:
- 預分配
- 無鎖(CAS)以及減小鎖競爭
- 緩存行和偽共享
預分配思想
預分配其實是一個空間換時間的思想,常見的如:JVM啟動時的堆內存分配,線程創建對象時堆內存中的TLAB分配,Redis中的動態字符串結構SDS,甚至Java語言中動態數組ArrayList等等。
Disruptor中對預分配思想的實踐有:
- RingBuffer中的
fill
方法,創建Disruptor時就填充整個RingBuffer,而不是每次生產者生產事件時再去創建事件對象(這樣可以避免JVM大量創建和回收對象,對GC造成壓力) - 生產者生產事件時,可以一次性取出多個事件槽,批量生產和批量發布
無鎖(CAS)以及減小鎖競爭
其實,在任何並發環境中開銷最大的操作都是:爭用寫訪問
,因為我們可以把讀和寫分離開,讀
可以做共享鎖,但是寫
只能是獨占。JDK的阻塞隊列包括並發隊列中都存在對寫操作的獨占訪問,這也是他們的多線程性能瓶頸所在。當然,Disruptor中也存在寫訪問
爭用,但是它通過巧妙的辦法,減弱了這種爭用的激烈程度(RingBuffer的next(int n)就是個例子),而且通過無鎖的CAS操作,避免了龐大的線程切換開銷。
Disruptor使用CAS操作的場景,大家可以對比ConcurrentLinkedQueue,這里就不再贅述了。
緩存行和偽共享
再看看CPU與內存的速度差多少倍?如果說CPU是一輛高速飛奔的高鐵,那么當前內存就像旁邊蹣跚踱步的老人。然而,更氣人的是,CPU的每個指令周期中的讀指令寫數據都要依賴內存(與CPU速度對等的是寄存器)。
那么如何解決CPU與內存如此大的速度差異呢?聰明的計算機科學家早就想到了辦法:加一個緩存層,即CPU高速緩存。
加了緩存后又引出另外一個問題:局部性原理,即2/8原則,80%的計算用20%的指令訪問20%的數據。同時,CPU讀高速緩存和讀內存的速度差了100倍,所以緩存的命中率越高系統的性能越厲害。高速緩存的存放一般都是按緩存行(一個緩存行64Byte)管理的,同一個緩存行里不同數據存在偽共享的問題,具體描述大家可以參考https://github.com/hiccup234/misc/blob/master/src/main/java/top/hiccup/jdk/vm/jmm/FalseSharingTest.java
那么Disruptor是怎么解決偽共享的問題呢?答案是:緩存行填充,其實這不是Disrutpor的發明,我們打開老點的JDK的JUC包下的Exchanger就可以看到大神Doug Lea的神來之筆:
新版的JDK已經換成了@sun.misc.Contended注解,也更優雅。
再談RingBuffer
RingBuffer是整個Disruptor的精神內核所在,通過查看源碼,我們可以知道RingBuffer是要利用緩存行來守護indexMask、entries、bufferSize、sequencer不被偽共享換出。
Ringbuffer是一個首尾相連的環,或者叫循環隊列,但是它自己沒有尾指針,跟正常的循環隊列不一樣,底層數據結構采用數組實現。
- 減少競爭點,比如不刪除數據,所以不需要尾指針(整個隊列的尾指針由消費者維護)
- 重復利用數組,不需要GC事件對象
- 使用數組存儲數據,可以利用CPU緩存每次都加載一個cacheline的特性,同時也可以避開偽共享的問題
6、總結
Disruptor其實還有一些其他的特性,如:Sequences(類似AtomicLong)、Sequencer、多播事件(類似MQ的Fanout交換機)以及RingBuffer持有的首指針,消費者持有的尾指針的控制和同步問題等等,大家可以對照源碼分析和整理。