解讀Disruptor系列-Disruptor論文精選

本文是筆者在研究Disruptor過程中翻譯的Disruptor1.0論文精選,中間穿插了一些感想和說明,均以“譯注”的形式說明。
論文原地址:https://lmax-exchange.github.io/disruptor/files/Disruptor-1.0.pdf

Disruptor: 用于并發線程間數據交換的有界隊列的高性能替代

  • Martin Thompson
  • Dave Farley
  • Michael Barker
  • Patricia Gee
  • Andrew Stewart
    May-2011

1 摘要

LMAX是為了創造一個極高性能的金融交易所而成立。作為實現這一目標的工作的一部分,我們已經評估了這種系統的幾種設計方法,但是隨著開始測試,我們遇到了使用傳統方法的一些基本限制。
許多應用程序依賴隊列在不同的處理階段之間交換數據。我們的性能測試表明,以這種方式使用隊列時的延遲成本與IO操作到磁盤(基于RAID或SSD的磁盤系統)的成本大致處于相同的數量級 - 非常非常慢。如果端對端操作中有多個隊列,將為總延遲增加數百微秒。顯然有優化的空間。
進一步的調查和對計算機科學的關注使我們認識到,傳統方法固有問題的結合(例如隊列和處理節點) ,導致了多線程實現中的爭用,這表明可能有更好的方法。
考慮現代CPU如何工作,我們喜歡稱之為“機械同情”,使用良好的設計實踐,強調關注焦點,我們提出了一種我們稱之為Disruptor的數據結構和使用模式。
測試表明,使用Disruptor進行三級流水線的平均延遲比基于隊列的等效方法低3個數量級。此外,Disruptor在相同配置下處理大約8倍的吞吐量。
這些性能改進代表著圍繞并行編程的思考的一個改變。這種新模式是需要高吞吐量和低延遲的任何異步事件處理架構的理想基礎。
在LMAX,我們已經建立了一個訂單匹配引擎,實時風險管理和一個高可用的內存中事務處理系統,所有這一切都取得了巨大的成功。這些系統中的每一個都制定了新的性能標準,據我們所知,這些標準是無與倫比的。
然而,這并不是一個只有在金融業才有意義的專業解決方案。 Disruptor是一種通用的機制,它能夠以最大化性能的方式解決并發編程中的復雜問題,而且實現起來也很簡單。盡管有些概念看起來很不尋常,但我們的經驗是,建立在這種模式上的系統要比類似的機制要簡單得多。
Disruptor顯著減少了寫時爭用,并發開銷較低,并且比可當前其他方法更加緩存友好,所有這些都會導致更高的吞吐量,更低的延遲且具有較少的抖動。 在中等時鐘速率的處理器上,我們已經看到每秒超過2500萬條消息,延遲低于50納秒。 與我們看到的任何其他實現相比,這種性能是一個明顯的改進。 這非常接近現代處理器在內核之間交換數據的理論限制。
(譯注:一句話總結,現有的隊列結構限制了并發編程的效率,Disruptor用于代替隊列實現高效地線程間的數據交換。)

2 概述

Disruptor是我們努力在LMAX建立世界最高性能金融交易所的結果。 早期的設計集中在從SEDA和Actors派生的架構,使用管道(pipelines)提升吞吐量。 在對各種實現進行了分析之后,很明顯,在管道之間的各個階段之間的排隊是控制成本的主要因素。我們發現隊列也引入了延遲和高水平的抖動。 我們花了大量精力開發具有更好性能的新隊列實現。 然而,由于對生產者,消費者和他們的數據存儲的設計問題的混淆,作為基本數據結構的隊列變得非常有限。 Disruptor是我們的工作成果,用于構建一個能將這些問題劃分清楚的并發結構。

3并發的復雜性

在本文檔和計算機科學的上下文中,并發不僅意味著兩個或多個任務并行發生,而且他們也在爭奪資源的訪問。競爭資源可能是數據庫,文件,套接字甚至內存中的位置。
代碼的并發執行大約是兩件事,互斥和變化的可見性。互斥是關于管理某些資源的競爭更新。變化的可見性是關于控制何時使這些更改對其他線程可見。如果可以消除對競爭更新的需求,可以避免互斥的需要。如果您的算法能夠保證任何給定的資源只被一個線程修改,那么就不需要相互排斥。讀和寫操作要求所有的更改都對其他線程可見。然而,只有爭用的寫操作需要相互排斥更改。
任何并發環境中,最昂貴的操作是爭用的寫訪問。要將多個線程寫入相同的資源需要復雜且昂貴的協調。通常情況下,這是通過采用某種鎖定策略實現的。

3.1 鎖的成本

通過操作系統內核的上下文切換實現,會暫停線程去等待鎖直到釋放。執行這樣的上下文切換,會丟失之前保存的數據和指令。

循環5億次64位計數操作使用時間對比:

image.png

3.2 CAS的成本

CAS(Compare And Swap)操作是特殊的機器碼指令,允許以原子操作對內存中的字(word)進行有條件地設置。如x86上的"lock cmpxchg"。
對于“遞增計數器實驗”,每個線程可以循環讀取計數器,然后嘗試將其原子地設置為其新的遞增值。舊的和新的值作為該指令的參數提供。如果在執行操作時,計數器的值與提供的預期值相匹配,計數器將用新值更新。另一方面,如果實際值與預期值不等,CAS操作將失敗。然后由線程嘗試執行更改以重試,重新讀取從該值增加的計數器,依此類推,直到更改成功。
可以不斷進行重讀、重試,直到操作成功。與鎖相比,避免了上下文切換帶來的巨大開銷。但是并非沒有開銷。處理器必須鎖定指令流水線(instruction pipeline)來保證原子性,并且使用一個內存屏障(memory barrier)使變化對于其他線程可見。
最優算法是:一個線程執行所有寫操作,其他線程讀取結果。在多處理器環境下讀取結果需要內存屏障來保證變化對運行在其他處理器上的線程可見。

3.3 內存屏障

出于性能原因,現代處理器使用亂序的指令執行亂序的數據載入及存儲(內存和執行單元間)。處理器需要保證的只是,程序邏輯結果不受亂序影響
處理器使用內存屏障來指示內存更新排序很重要的代碼段。它們是在線程之間實現硬件排序和改變可見性的手段。 編譯器可以添加軟件屏障,以確保編譯代碼的排序,這些軟件內存障礙是作為處理器硬件屏障的附加而由處理器使用的。
讀內存屏障通過在“內存變化無效隊列”做標記,來排序CPU裝載指令。寫內存屏障通過在用于刷寫的存儲緩沖中做標記,來排序存儲指令。一個完整的內存屏障只在執行它的CPU上排序裝載和存儲(指令)。
JAVA中,通過volatile關鍵字實現讀寫屏障。由Java5內存模型明確定義。

3.4 緩存行

現代處理器的緩存用法對于成功的高性能操作至關重要。目前來說,處理器在處理緩存中的數據和指令非常高效,相反的,當緩存缺失(cache miss)時則非常低效。
硬件并非以字節或字長為單位處理內存。為了高效率,緩存被組織成大小為32到256字節的緩存行(cache-line),最常用的是64字節。這是高速緩存一致性協議的粒度級別。
上面的意思是:如果有兩個變量在相同的緩存行中,且這兩個變量是被不同線程寫入,這是就會存在寫入爭用問題,就好像這兩個變量是一個變量一樣(兩個線程雖然操作的是各自的變量,但會導致同一個緩存行失效)。這就是偽共享(false sharding)。從高性能上考慮,重要的是要確保爭用最小化,獨立但并發寫入的變量不會共享相同的高速緩存行當以可預測的方式訪問主存時,CPU可以通過預測將要訪問的位置進行預讀取來隱藏訪問主存的成本。但是如果是遍歷鏈表或樹這樣的結構時,CPU就不能預測下一步的訪問位置了。這將最終導致訪問效率下降2個數量級。

3.5 Queue的問題

Queue通常使用鏈表或數組結構存儲元素。使用無界隊列通常都需要保證生產者生產速度小于消費者消費速度,否則將會由于耗盡內存導致災難結果。為了避免這種結果,隊列通常是有界的。保持隊列有界要么使用數組,要么就是能夠主動跟蹤大小。
隊列實現傾向于對head,tail和size變量進行寫入爭用。 在使用中,由于消費者和生產者之間的差異,隊列通常總是接近滿或接近空。 他們很少在平衡的中間地帶進行生產和消費的比例均勻匹配。 總是充滿或總是空的傾向導致高水平的爭用和/或昂貴的緩存一致性。 問題是,即使使用諸如鎖或CAS變量的不同并發對象來分離頭尾機制,它們通常也占用相同的高速緩存行。
使用一個粗粒度的鎖管理整個queue的put和take非常容易實現,但是也很容易成為吞吐量瓶頸。而使用更細粒度的控制就需要更復雜的并發設計。如果將并發問題從queue的語義中剔除,queue的實現無非就是單生產者-單消費者的模式。
在Java中,使用隊列還有一個問題-隊列是垃圾的重要來源。首先,必須分配對象并將其放置在隊列中。 其次,如果支持鏈表,則必須分配表示列表節點的對象。 當不再引用時,需要重新聲明分配給支持隊列實現的所有這些對象。
(譯注:簡單總結,使用隊列時,由于消費者生產者之間的差異,導致并發損耗;存在偽共享問題;隊列元素是垃圾重要來源。)

3.6 流水線和圖表

對于許多類問題,將幾個處理階段組織成(wire together)管道是有意義的。 這樣的管道通常具有并行路徑,被組織成圖形狀的拓撲(topologies)。 每個階段之間的鏈接通常由隊列實現,每個階段都有自己的線程。
這種方法并不廉價 - 在每個階段,都必須承擔入隊和出隊的消耗。 當路徑必須分叉(fork)時,目標的數量乘以這個成本,并且在這樣一個分支之后必須重新加入時會產生不可避免的爭用成本(譯注:如一個事件既需要被記錄日志,也需要被業務邏輯消費)。
如果依賴圖的表達不會導致階段之間放置隊列的消耗,將是最理想的。(譯注:將不同階段通過隊列組成拓撲圖時,避免隊列本身的消耗)

4 LMAX Disruptor的設計思想

設計Disruptor就是為了解決以上問題。之所以叫做"Disruptor",是因為在Java7中,用于支持Fork-join的"Phaser"在處理依賴圖表時有相似的地方。(譯注:Phaser相位槍、Disruptor裂解炮都是星際迷航中的武器,Phaser是聯邦武器,Disruptor是克林貢的等價物)

4.1 內存分配

環形緩沖(Ring Buffer)的所有內存都是在啟動時預分配的。環形緩沖既可以存儲條目(entry)指針的數組,也可以存儲代表條目的結構數組。Java語言限制了條目以對象指針的形式與環形緩沖相關聯。每個條目都并非數據本身,而是一個容器。條目的預分配消除了支持垃圾回收編程語言的問題,所有的條目在Disruptor實例生存周期中都將重用并一直存在。這些條目的內存同時被分配,將有極大可能在主存中連續分配,這樣就支持了緩存位置預測(cache striding,緩存跨越)。這是由John Rose提出的建議,將“值類型”引入到java語言,允許類似C語言中的元組數組那樣,來保證內存的連續分配而避免指針間接尋址。
在受管理的運行時環境(如Java)中開發低延遲系統時,垃圾收集可能會有問題。 分配的內存越多,垃圾收集器的負擔就越大。 垃圾收集者在對象生命周期非常短暫或有效永生(譯注:而非內存泄露引起的永生)的情況下工作在最佳狀態。 在環形緩沖區中預先分配條目意味著對于垃圾收集器而言,它是永生的,因此代表著很小的負擔。
在重負載隊列系統中進行備份,會引起處理速率的下降,導致已分配對象生存的時候更久,將會被垃圾收集器提升到老年代。這意味著兩點:一不同代之間的對象復制會造成延遲抖動;二垃圾收集器回收老年代對象代價更大,而且當內存碎片空間需要壓縮時,會增加“Stop the world”停頓的可能性。在大內存堆中可能會導致每GB幾秒的停頓。

4.2 梳理問題

我們在所有隊列實現中總結了以下問題:
a. 被交換元素如何存儲
b. 如何協調生產者聲明(claiming 取得)下一個用于交換(exchange)的序號
c. 在新元素可用時如何協調要通知的消費者

在使用垃圾回收的語言設計金融交易系統時,內存分配過多可能會造成問題。 所以,正如我們描述的使用支持鏈表的隊列不是一個好辦法。 如果可以預先分配用于處理階段之間的數據交換的整個存儲空間,則垃圾收集將被最小化。 此外,如果這種分配可以在統一的塊中執行,則將以對現代處理器采用的緩存策略非常友好的方式來完成該數據的遍歷。 滿足此要求的數據結構是預先填充所有插槽的數組。 在創建環形緩沖區時,Disruptor利用抽象工廠模式預先分配條目。 當聲明條目時,生產者可以將其數據復制到預先分配的結構中。
在大多數處理器上,對序列號進行余數計算的成本非常高,這決定了環中的插槽。 通過使環形緩沖器大小為2的平方,可以大大降低該成本。可以使用大小(size)減去1的位掩碼來有效地執行求余(remainder)操作。(如size為8,序號為14,則序號1110 & 111 = 110即6)(譯注:注意,此處原文應該有誤,實際上這里應為求模操作,這部分的源碼會在后續講
就像先前描述的,有界隊列的頭部和維護遭受爭用。環形緩沖器的數據結構免除了這種爭用和并發原語,因為這些問題被轉移到了環形緩沖器關聯的生產者和消費者的屏障(barrier)中。屏障邏輯如下:
大多數情況下Disruptor只使用一個生產者。通常生產者是文件讀取器或網絡監聽器。當只有一個生產者時,當然沒有序號(sequence)和條目分配的爭用。
在有多個生產者的不常見情況下,多個生產者將會競爭聲明(claim)環形緩沖器中下一個條目。這種(多生產者對同一個條目的)爭用可以使用一個簡單的CAS操作得到管理。
當生產者將關聯數據復制進聲明的條目中后,會通過提交這個條目的序號讓此條目對消費者可見。這可以不使用CAS而使用一個簡單的忙碌旋轉(busy spin)直到其他生產者在各自的提交中到達該序號。生產者可以提前一個游標表示下一個可用條目的消費。生產者可以在寫入環形緩沖區之前,通過跟蹤消費者的序號作為簡單的讀取操作來避免環繞環。
消費者在讀取條目前先等待環形緩沖器中的序列可用。有多種等待策略可供選擇。如果CPU資源很寶貴,消費者可以做鎖的條件等待生產者發送通知。這種做法會造成爭用,只有在CPU資源比延遲或吞吐更重要時才會用。消費者也可以循環檢查代表當前可用序列的游標。這樣可以使用CPU資源交換更小的延遲,可以選擇用或者不用線程退讓(thread yield)。如果我們不使用鎖和條件變量,我們已經打破了生產者和消費者之間的競爭依賴關系,這樣做非常好。無鎖的多生產者 - 多消費者隊列確實存在,但是它們需要在頭部,尾部,大小計數器上進行多個CAS操作。 Disruptor不能忍受這樣的CAS爭用。

4.3 序列(Sequencing)

序列是Disruptor中管理并發的核心概念。每個生產者和消費者在同環形緩沖器交互時,都采用嚴格的序列概念。當生產者要求環形中一個條目時,會要求序列中的下一個槽位。下一個可用槽位的序列可以是單生產者時的簡單計數器,或者是多生產者時的使用CAS操作的原子計數器。當序列值被聲明時,環形緩沖器中的這個條目就可以被那個特定的生產者寫入了。當生產者完成更新條目時,它可以通過更新單獨的計數器來提交更改,該計數器表示環形緩沖區上的光標,以為消費者提供可用的最新條目。環形緩沖器游標可以被生產者使用內存屏障以忙循環(busy spin)讀取和寫入,而無須使用以下的CAS操作:

long expectedSequence = claimedSequence – 1;
while (cursor != expectedSequence) {
  // busy spin
}
cursor = claimedSequence;

消費者通過使用內存屏障來讀取游標等待給定的序列可用。 一旦光標被更新,內存屏障就可以確保在緩沖區中條目的更改對于等待光標前進的消費者是可見的。
每個消費者都包含一個自己的處理環形緩沖器條目的序列。這些消費者序列允許生產者跟蹤消費者,防止繞環(譯注:prevent the ring from wrapping)。消費者序列還允許消費者以有序的方式在同一條目上協調工作。
在只有一個生產者的情況下,無論消費者圖表的復雜程度如何,都不需要鎖或CAS操作。 可以通過上述討論的序列上的內存屏障來實現整個并發協調。

4.4 批量效應(Batching Effect)

當消費者等待環形緩沖器下一個游標序列時,可能會出現一個有趣的不太可能在隊列上出現的現象。如果消費者發現環形緩沖區游標自上次檢查以來已經前進了多步,則可以(譯注:從落后位置)一直處理到該序號位置,而不會涉及并發機制。 這導致滯后的消費者在生產者加速向前時迅速趕上生產者的步伐,從而平衡系統。這種批處理增加了吞吐量,同時減少和平滑了延遲。基于我們的觀察結果,這種效應不論負載如何將總是保持常量時間的延遲,直到存儲子系統飽和,根據利特爾法則(Little's Law),輪廓是線性的。這與我們在時隊列觀察到的延遲的“J”曲線效應非常不同。
(譯注:代碼實現為BatchEventProcessor)

4.5 依賴圖(Dependency Graphs)

一個隊列代表了生產者和消費者之間僅一步流水線依賴。如果消費者組成了一個依賴的鏈條或圖表結構,那么在圖結構每個階段間都需要隊列。在依賴階段的圖中, 這樣增加了許多次的隊列固定消耗。在設計LMAX金融交易系統時,我們的分析表明,使用基于隊列方法的消耗決定了處理交易的總消耗。
因為在Disruptor模式中,生產者和消費者問題是分開的,只在核心的一個環形結構中表示出一個消費者復雜依賴圖就成為了可能。這導致執行的固定成本大大降低,從而提升了吞吐量同時減少了延遲。
單一一個環形緩沖器可以用來存儲可以表達整個工作流復雜結構的條目。必須注意這種結構的設計,在被獨立消費者寫入狀態時不能導致偽共享問題。
(譯注:多任務流水線之間可能有依賴,如ABC為三個任務,A->B->C是基本的流程,在多線程任務中,只有完成A的線程才能執行B,再執行C。)

4.6 Disruptor類設計

Disruptor框架的核心關系類圖描述如下。圖中省略了一些可以簡化編程模型的工具類。生產者通過ProcuderBarrier按順序要求條目,將變化寫入要求的條目,再通過ProcuderBarrier把條目提交回去,讓它們可供消費。每個消費者需要做的是提供一個BatchHandler實現,用于接收新條目可用時的回調。這種編程模型很類似Actor模型的事件驅動模型。將通常合并在一起的隊列實現問題分離開,將允許更大的設計自由度。一個環形緩沖器是Disruptor模式核心存在,提供了無爭用的數據存儲交換。并發問題也被分成生產者和消費者同環形緩沖器的交互。ProducerBarrier管理任意在環形緩沖器中聲明槽位的并發問題,同時跟蹤獨立的消費者來阻止環繞環(prevent the ring from wrapping)。當新條目可用時,ConsumerBarrier通知消費者,消費者可以被構造成一個處理流水線的多階段依賴圖。

image.png

(譯注:Disruptor自發布以來已有若干變化。
Entry -> Event
Consumer -> EventProcessor
ProcuderBarrier -> AbstractSequencer
Consumer -> EventProcessor
ConsumerBarrier -> SequenceBarrier)

4.7 代碼樣例

單生產者和單消費者模式。(譯注:由于代碼設計發生了較大變化,以下代碼只用作理解)

// Callback handler which can be implemented by consumers
final BatchHandler batchHandler = new BatchHandler()
{
     public void onAvailable(final ValueEntry entry) throws Exception
     {
      // process a new entry as it becomes available.
     }
     public void onEndOfBatch() throws Exception
     {
     // useful for flushing results to an IO device if necessary.
     }
     public void onCompletion()
     {
     // do any necessary clean up before shutdown
     }
};

RingBuffer ringBuffer = new RingBuffer(ValueEntry.ENTRY_FACTORY, SIZE, ClaimStrategy.Option.SINGLE_THREADED, WaitStrategy.Option.YIELDING);
ConsumerBarrier consumerBarrier = ringBuffer.createConsumerBarrier();
BatchConsumer batchConsumer = new BatchConsumer(consumerBarrier, batchHandler);
ProducerBarrier producerBarrier = ringBuffer.createProducerBarrier(batchConsumer);

// Each consumer can run on a separate thread
EXECUTOR.submit(batchConsumer);
 
// Producers claim entries in sequence ValueEntry
entry = producerBarrier.nextEntry();

// copy data into the entry container
 
// make the entry available to consumers
producerBarrier.commit(entry);

5 吞吐量性能測試

作為參考,我們選擇Doug Lea的出色的java.util.concurrent.ArrayBlockingQueue,基于我們的測試它在任何有界隊列中具有最高性能。 測試以阻塞編程風格進行,以匹配Disruptor的編程風格。 以下詳細的測試案例可在Disruptor開源項目中找到。 注意:運行測試需要能夠并行執行至少4個線程的系統。
(譯注:方便起見,使用P1代表第一個生產者,P2代表第二個生產者,C1代表第一個消費者,C2代表第二個消費者,以此類推)
單播:1P-1C
P1-> C1
三步流水線:1P-3C
P1 -> C1 -> C2 -> C3
多生產者: 3P-1C
P1,P2,P3 -> C1
多播(多消費者):1P-3C
P1 -> C1, C2, C3
鉆石型:1P-3C
P1 -> C1, C2 -> C3

image.png

對于上述配置,與Disruptor的屏障配置相比,每個數據流弧應用了ArrayBlockingQueue。 下表顯示了使用Java 1.6.0_25 64位Sun JVM,Windows 7,不帶超線程的Intel Core i7 860 @ 2.8 GHz和Intel Core i7-2720QM(Ubuntu 11.04)的操作的性能結果,并充分利用3次處理5億條消息的運行結果。 不同的JVM執行結果可能會有很大差異,下面的數字并不是我們觀察到的最高結果。

image.png

單位:操作/秒

6 延遲性能測試

為了測量延遲,我們采用三級流水線,并以小于飽和度的速度生成事件。這是通過在注入事件之前等待1微秒,然后再注入下一個并重復5000萬次來實現。要按照這個精度要求,需要使用CPU的時間戳計數器(TSC)。我們選擇具有不變的TSC的CPU,因為由于省電和睡眠狀態,較舊的處理器遭受頻率變化的影響。英特爾Nehalem和更高級的處理器使用一個不變的TSC,可以在Ubuntu 11.04上運行的最新的Oracle JVM訪問。該測試沒有使用CPU綁定。
為了比較,我們再次使用ArrayBlockingQueue。我們可以使用ConcurrentLinkedQueue,這可能會給出更好的結果,但是我們希望使用有界隊列實現來確保生產者不要超過消費者創造背壓。以下結果是2.2Ghz Core i7-2720QM在Ubuntu 11.04上運行Java 1.6.0_25 64位。
Disruptor的平均延遲時間為52納秒,而ArrayBlockingQueue32,757納秒。分析顯示使用鎖和信令通過條件變量是ArrayBlockingQueue的延遲的主要原因。

image.png
image.png

7 結論

Disruptor是提高吞吐量,減少并發執行上下文之間的延遲并確保可預測延遲的重大進步,這在許多應用程序中是一個重要的考慮因素。我們的測試表明,它比在線程間交換數據的同類方法性能要好。我們認為這是這種數據交換的最高性能機制。通過關注跨線程數據交換中涉及的關注點的清晰分離,通過消除寫入爭用、最小化讀取爭用和確保代碼與現代處理器使用的緩存工作良好,我們已經創建了一種高效的機制來在任何應用程序之間交換數據。
允許消費者在沒有任何爭用的情況下處理條目達到給定閾值的批處理效應,在高性能系統中引入了一個新特性。對于大多數系統,當負載和爭用增加時,延遲指數增加,即典型的“J”曲線。隨著Disruptor上的負載增加,延遲保持幾乎平坦,直到內存子系統發生飽和。
我們相信Disruptor建立了高性能計算的新基準,并且非常適合繼續利用當前處理器和計算機設計的趨勢。

引用:
1 Staged Event Driven Architecture – http://www.eecs.harvard.edu/~mdw/proj/seda/
2 Actor model – http://dspace.mit.edu/handle/1721.1/6952
3 Java Memory Model - http://www.ibm.com/developerworks/library/j-jtp02244/index.html
4 Phasers -
http://gee.cs.oswego.edu/dl/jsr166/dist/jsr166ydocs/jsr166y/Phaser.html
5 Value Types - http://blogs.oracle.com/jrose/entry/tuples_in_the_vm
6 Little’s Law - http://en.wikipedia.org/wiki/Little%27s_law
7 ArrayBlockingQueue - http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/ArrayBlockingQueue.html
8 ConcurrentLinkedQueue -
http://download.oracle.com/javase/1.5.0/docs/api/java/util/concurrent/ConcurrentLinkedQueue.html

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容

  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,991評論 19 139
  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 173,521評論 25 708
  • 以下,都是在生活中我所見到的有趣的事。如果你恰好了解,卻覺得平常,也請不要見怪。 1、當你一直看著某個人,那個...
    陸清禾閱讀 718評論 2 0
  • 知識經常被認為是儲存在昏暗圖書館里堆滿塵埃的書架上的死物。遺憾的是,圖書館里沉寂的氣氛會使人想起教堂的葬禮或...
    鄧潔兒閱讀 165評論 0 1
  • 不能再說沒時間碼字,沒時間學習。畢竟,追的劇實在是太多了,時間自然而然也就沒了,沒了,沒了…… 01 美劇《Min...
    差點叫冬梅的lily閱讀 189評論 0 0