Disruptor之RingBuffer

(一)RingBuffer是什麼?

     RingBuffer顧名思義,就是一個內存環,每一次讀寫操做都循環利用這個內存環,從而避免頻繁分配和回收內存,減輕GC壓力,同時因爲Ring Buffer能夠實現爲無鎖的隊列,從而總體上大幅提升系統性能。java

1.Ring Buffer是由一個大數組組成的。數組

2..Ring Buffer的「指針」(也稱爲序列或遊標)是java long類型的(64位有符號數),指針採用往上計數自增的方式。緩存

3..Ring Buffer中的指針進行按ring buffer的size取模找出數組的下標來定位入口。爲了提升性能,咱們一般將ring buffer的size大小設置成實際使用的2倍。安全

                                                

    RingBuffer沒有尾指針,維護了一個指向下一個可用位置的序號。RingBuffer和經常使用的隊列之間的區別是,不刪除buffer中的數據,也就是說這些數據一直存放在buffer中,直到新的數據覆蓋他們。數據結構

它爲何如此優秀?

    之因此ringbuffer採用這種數據結構,是由於它在可靠消息傳遞方面有很好的性能。併發

    首先,由於它是數組,因此要比鏈表快,並且有一個容易預測的訪問模式。(數組內元素的內存地址是連續性存儲的)。這是對CPU緩存友好的—也就是說,在硬件級別,數組中的元素是會被預加載的,所以在ringbuffer當中,cpu無需時不時去主存加載數組中的下一個元素。(注:由於只要一個元素被加載到緩存行,其餘相鄰的幾個元素也會被加載進同一個緩存行)app

    其次,你能夠爲數組預先分配內存,使得數組對象一直存在(除非程序終止)。這就意味着不須要花大量的時間用於垃圾回收。此外,不像鏈表那樣,須要爲每個添加到其上面的對象創造節點對象—對應的,當刪除節點時,須要執行相應的內存清理操做。函數

 

(二)如何從Ringbuffer讀取

    

    消費者(Consumer)是一個想從Ring Buffer裏讀取數據的線程,它能夠訪問ConsumerBarrier對象——這個對象由RingBuffer建立而且表明消費者與RingBuffer進行交互。就像Ring Buffer顯然須要一個序號才能找到下一個可用節點同樣,消費者也須要知道它將要處理的序號——每一個消費者都須要找到下一個它要訪問的序號。在上面的例子中,消費者處理完了Ring Buffer裏序號8以前(包括8)的全部數據,那麼它期待訪問的下一個序號是9性能

    消費者能夠調用ConsumerBarrier對象的waitFor()方法,傳遞它所須要的下一個序號.spa

final long availableSeq = consumerBarrier.waitFor(nextSequence);

    ConsumerBarrier返回RingBuffer的最大可訪問序號——在上面的例子中是12

    接下來,消費者會一直原地停留,等待更多數據被寫入Ring Buffer。而且,一旦數據寫入後消費者會收到通知——節點9101112 已寫入。如今序號12到了,消費者可讓ConsumerBarrier去拿這些序號節點裏的數據了。

拿到了數據後,消費者(Consumer)會更新本身的標識(cursor)。

這樣作有助於平緩延遲的峯值?

    之前須要逐個節點地詢問「我能夠拿下一個數據嗎?如今能夠了麼?如今呢?」,消費者(Consumer)如今只須要簡單的說「當你拿到的數字比我這個要大的時候請告訴我」,函數返回值會告訴它有多少個新的節點能夠讀取數據了。由於這些新的節點的確已經寫入了數據(Ring Buffer自己的序號已經更新),並且消費者對這些節點的惟一操做是讀而不是寫,所以訪問不用加鎖。這太好了,不只代碼實現起來能夠更加安全和簡單,並且不用加鎖使得速度更快。另外一個好處是你能夠用多個消費者(Consumer)去讀同一個RingBuffer ,不須要加鎖,也不須要用另外的隊列來協調不一樣的線程(消費者)。這樣你能夠在Disruptor的協調下實現真正的併發數據處理。

 

(三)寫入 Ringbuffer

    寫入 Ring Buffer 的過程涉及到兩階段提交 (two-phase commit)。首先,你的生產者須要申請 buffer 裏的下一個節點。而後,當生產者向節點寫完數據,它將會調用 ProducerBarrier 的 commit 方法。

 Ring Buffer 仍是與消費端同樣提供了一個 ProducerBarrier 對象,讓生產者經過它來寫入 Ring Buffer。

ProducerBarrier如何防止RingBuffer重疊

Disruptor 全解析(3):寫入 Ring Buffer

在這幅圖中,咱們假設只有一個生產者寫入 Ring Buffer。

    ConsumerTrackingProducerBarrier 對象擁有全部正在訪問 Ring Buffer 的 消費者 列表。這看起來有點兒奇怪-我從沒有指望 ProducerBarrier 瞭解任何有關消費端那邊的事情。可是等等,這是有緣由的。由於咱們不想與隊列「混爲一談」(隊列須要追蹤隊列的頭和尾,它們有時候會指向相同的位置),Disruptor 由消費者負責通知它們處理到了哪一個序列號,而不是 Ring Buffer。因此,若是咱們想肯定咱們沒有讓 Ring Buffer 重疊,須要檢查全部的消費者們都讀到了哪裏。

    在上圖中,有一個 消費者 順利的讀到了最大序號 12(用紅色/粉色高亮)。第二個消費者 有點兒落後——可能它在作 I/O 操做之類的——它停在序號 3。所以消費者 2 在遇上消費者 1 以前要跑完整個 Ring Buffer 一圈的距離。

    如今生產者想要寫入 Ring Buffer 中序號 3 佔據的節點,由於它是 Ring Buffer 當前遊標的下一個節點。可是 ProducerBarrier 明白如今不能寫入,由於有一個消費者正在佔用它。因此,ProducerBarrier 停下來自旋 (spins),等待,直到那個消費者離開。

申請下一個節點

Disruptor 全解析(3):寫入 Ring Buffer

ProducerBarier 會看到下一個節點——序號 3 那個已經能夠用了。它會搶佔這個節點上的 Entry(我尚未特別介紹 Entry 對象,基本上它是一個放寫入到某個序號的 Ring Buffer 數據的桶),把下一個序號(13)更新成 Entry 的序號,而後把 Entry 返回給生產者。生產者能夠接着往 Entry 裏寫入數據。

提交新的數據

Disruptor 全解析(3):寫入 Ring Buffer

當生產者結束向 Entry 寫入數據後,它會要求 ProducerBarrier 提交。

ProducerBarrier先等待Ring Buffer的遊標追上當前的位置(對於單生產者這毫無心義-好比,咱們已經知道遊標到了 12 ,並且沒有其餘人正在寫入 Ring Buffer)。而後 ProducerBarrier 更新 Ring Buffer 的遊標到剛纔寫入的 Entry 序號-在咱們這兒是 13。接下來,ProducerBarrier 會讓消費者知道 buffer 中有新東西了。它戳一下 ConsumerBarrier 上的 WaitStrategy 對象說-「喂,醒醒!有事情發生了!」(注意-不一樣的 WaitStrategy 實現以不一樣的方式來實現提醒,取決於它是否採用阻塞模式)。如今消費者 1 能夠讀 Entry 13 的數據,消費者 2 能夠讀 Entry 13 以及前面的全部數據,而後它們都過得很 happy。

ProducerBarrier 上的批處理

    Disruptor 能夠同時在生產者和消費者兩端實現批處理。

Disruptor 全解析(3):寫入 Ring Buffer

ProducerBarrier 知道 Ring Buffer 的遊標指向 12,而最慢的消費者在 9 的位置,它就可讓生產者寫入節點 3,4,5,6,7 和 8,中間不須要再次檢查消費者的位置。

多個生產者的場景

    如今生產者 1 申請提交節點 13 的數據(生產者 1 發出的綠色箭頭表明這個請求)。ProducerBarrier 讓 ClaimStrategy 先等待 Ring Buffer 的遊標到達序號 12,固然如今已經到了。所以 Ring Buffer 移動遊標到 13,讓 ProducerBarrier 戳一下 WaitStrategy 告訴全部人都知道 Ring Buffer 有更新了。如今 ProducerBarrier 能夠完成生產者 2 的請求,讓 Ring Buffer 移動遊標到 14,而且通知全部人都知道。

    Ring Buffer的內容順序老是會遵循nextEntry()的初始調用順序。也就是說,若是一個生產者在寫入 Ring Buffer 的時候暫停了,只有當它解除暫停後,其餘等待中的提交纔會當即執行。

相關文章
相關標籤/搜索