高性能內存隊列Disruptor--原理分析

一、起源

    Disruptor最初由lmax.com開發,2010年在Qcon公開發表,並於2011年開源,其官網定義爲:「High Performance Inter-Thread Messaging Library」,即:線程間的高性能消息框架。其實JDK已經爲咱們提供了不少開箱即用的線程間通訊的消息隊列,如:ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue等,這些都是基於無鎖的CAS設計。html

    那麼Disruptor爲何還有存在的意義呢?其實無鎖並不表明沒有競爭,因此當高併發寫或者讀的時候,這些工具類同樣會面臨資源爭用的極限性能問題。而lmax.com做爲一家頂級外匯交易商,其交易系統須要處理的併發量很是巨大,對響應延遲也很是敏感。在這種背景下,Disruptor誕生了,它的核心思想就是:把多線程併發寫的線程安全問題轉化爲線程本地寫,即:不須要作同步。同時,lmax公司基於Disruptor構建的交易系統也屢次斬獲金融界大獎。java

二、發展

框架很輕量

Disruptor很是輕量,整個框架最新版3.4.2也才70多個類,但性能卻很是強悍。得益於其優秀的設計,和對計算機底層原理的運用,官網說的:mechanical sympathy,我翻譯成硬件偏向或者面向硬件編程。同時它跟咱們常見的MQ不同,這裏說的線程間其實就是同一個進程內,不一樣線程間的消息傳遞,跟JDK中的那些阻塞和併發隊列的用法是同樣的,也就是說它們不會誇進程。git

性能很厲害

  • 比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,而後打開異步就能夠了)程序員

三、核心類

主要核心類只有這6個: 簡單使用方法能夠參考: https://github.com/hiccup234/web-advanced/blob/master/disruptor-client/src/main/java/top/hiccup/disruptor/SampleTest.javagithub

四、有多快?

    JDK自帶的隊列都是優秀程序員的智慧結晶,性能也是很是的強悍,下圖是其特色對比和總結:     同時Disruptor在這樣強悍的基礎上把性能提高了近一個數量級,這是很是了不得的(-- 就像要把個人存款增加10倍相對容易,但要讓東哥的身價再漲一番就難了)經過上圖咱們能夠看到,無鎖的方式通常都是無界的(沒法保證隊列的長度在肯定的範圍內),加鎖的方式,能夠實現有界隊列。     可是,在穩定性要求特別高的系統中,爲了防止生產者速度過快,致使內存溢出,只能選擇有界隊列。因此咱們綜合一下,JDK的一衆隊列中,跟Disruptor最匹配的就是ArrayBlockingQueue了。web

沒有對比就沒有傷害

這是我本機測試的幾個隊列的性能對比,測試程序見:https://github.com/hiccup234/web-advanced/tree/master/disruptor-client 可見Disruptor在單線程狀況下吞吐量竟能達到2500W以上,遠遠超過其餘隊列。在多生產者的狀況下,這幾個隊列的吞吐量倒是同樣的(說明隊列在多線程環境下,性能瓶頸並不在其自己)apache

再看Log4j2官網的性能測試截圖:

你們注意最右邊的64線程,吞吐量比最左邊的單線程高了很多,爲何這裏多線程的吞吐量反而更好?是上面個人多線程測試程序有問題嗎?     其實不是的,這是Disruptor更有魅力的一個特色:RingBuffer有一個重載的next方法,即:一次爲當前線程分配多個事件槽,一個線程一次性批量生產多個事件。這樣在極限性能的狀況下就能夠大大減小線程間的上下文的切換,畢竟線程調度對JVM來講是很重的一個操做,也是上上圖中各隊列的多線程性能瓶頸所在。 編程

五、爲何那麼快?

    Disruptor爲何這麼快呢?我主要總結了這3點:數組

  • 預分配
  • 無鎖(CAS)以及減少鎖競爭
  • 緩存行和僞共享

預分配思想

預分配實際上是一個空間換時間的思想,常見的如:JVM啓動時的堆內存分配,線程建立對象時堆內存中的TLAB分配,Redis中的動態字符串結構SDS,甚至Java語言中動態數組ArrayList等等。 Disruptor中對預分配思想的實踐有:緩存

  1. RingBuffer中的fill方法,建立Disruptor時就填充整個RingBuffer,而不是每次生產者生產事件時再去建立事件對象(這樣能夠避免JVM大量建立和回收對象,對GC形成壓力)
  2. 生產者生產事件時,能夠一次性取出多個事件槽,批量生產和批量發佈

無鎖(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是一個首尾相連的環,或者叫循環隊列,可是它本身沒有尾指針,跟正常的循環隊列不同,底層數據結構採用數組實現。

  1. 減小競爭點,好比不刪除數據,因此不須要尾指針(整個隊列的尾指針由消費者維護)
  2. 重複利用數組,不須要GC事件對象
  3. 使用數組存儲數據,能夠利用CPU緩存每次都加載一個cacheline的特性,同時也能夠避開僞共享的問題

六、總結

    Disruptor其實還有一些其餘的特性,如:Sequences(相似AtomicLong)、Sequencer、多播事件(相似MQ的Fanout交換機)以及RingBuffer持有的首指針,消費者持有的尾指針的控制和同步問題等等,你們能夠對照源碼分析和整理。

原文出處:https://www.cnblogs.com/ocean234/p/11363487.html

相關文章
相關標籤/搜索