併發理論
JMM
概述
- Java Memory Model縮寫為JMM,直譯為Java記憶體模型,定義了一套在多執行緒讀寫共享資料時(成員變數、陣列)時,對資料的可見性、有序性和原子性的規則和保障;JMM用來遮蔽掉各種硬體和作業系統的記憶體訪問差異,以實現讓Java程式在各平臺下都能夠達到一致的記憶體訪問效果。
- JMM是一種規範,目的是解決由於多執行緒通過共享記憶體進行通訊時,存在的本地記憶體資料不一致、編譯器對程式碼指令重排序、處理器對程式碼亂序執行、CPU切換執行緒等帶來的問題。
併發與並行
- 併發:指的是多個事情,在同一時間段內同時發生了; 併發的多個任務之間是互相搶佔資源的。
- 並行:指的是多個事情,在同一時間點上同時發生了;並行的多個任務之間是不互相搶佔資源的。
- 只有在多CPU的情況中,才會發生並行。否則,看似同時發生的事情,其實都是併發執行的。
現代計算機記憶體模型
現代計算機處理器與儲存裝置運算速度完全不在同一量級上,至少相差幾個數量級,如果讓處理器等待計算機儲存裝置那麼這樣處理器的優勢就不會體現出來。為了提高處理效能實現高併發,在處理器和儲存裝置之間加入了快取記憶體(cache)來作為緩衝。將CPU運算需使用到的資料先複製到快取中,讓CPU運算能夠快速進行;當CPU運算完成之後,再將快取中的結果寫回主記憶體,這樣CPU運算就不用依賴等待主記憶體的讀寫操作了。
-
快取記憶體設定為多級快取,其目的為了解決CPU運算速度與記憶體讀寫速度不匹配的矛盾;在CPU和記憶體之間,引入了L1快取記憶體、L2快取記憶體、L3快取記憶體
每一級快取中所儲存的資料全部都是下一級快取中的一部分,當CPU需要資料時,先從快取中取,加快讀寫速度,提高CPU利用率。儲存層次金字塔的結構:-
暫存器 → L1快取 → L2快取 → L3快取 → 主記憶體 → 本地磁碟 → 遠端資料庫。
-
越往上訪問速度越快、成本越高,空間更小。越往下訪問速度越慢、成本越低,空間越大。
-
-
每個處理器都有自己的快取記憶體,同時又共同操作同一塊主記憶體,當多個處理器同時操作主記憶體時,可能將導致各自的的快取資料不一致,為了解決這個問題
主要提供了兩種解決辦法:- 匯流排鎖:在多 cpu 下,當其中一個處理器要對共享記憶體進行操作的時候,在匯流排上發出一個 LOCK# 訊號,這個訊號使得其他處理器無法通過匯流排來訪問到共享記憶體中的資料,匯流排鎖定把 CPU 和記憶體之間的通訊鎖住了,這使得鎖定期間,其他處理器不能操作其他記憶體地址的資料,匯流排鎖定的開銷比較大,這種機制顯然是不合適的。匯流排鎖的力度太大了,最好的方法就是控制鎖的保護粒度,只需要保證對於被多個 CPU 快取的同一份資料是一致的就可以了。
- 快取鎖:相比匯流排鎖,快取鎖即降低了鎖的力度。核心機制是基於快取一致性協議來實現的。為了達到資料訪問的一致,需要各個處理器在訪問快取時遵循一些協議,在讀寫時根據協議來操作,常見的協議有 MSI、MESI、MOSI 等,最常見的為Intel的MESI協議是四種狀態的縮寫,用來修飾快取行的狀態。
- M:被修改,該快取行的資料被修改了,和主存資料不一致。監聽所有想要修改此快取行對應的記憶體資料的操作,該操作必須等快取行資料更新到主記憶體中,狀態變成 S (Shared)共享狀態之後執行。
- E:獨享,該快取行和記憶體資料一致,資料只在本快取中;監聽所有讀取此快取行對應的記憶體資料的操作,如果發生這種操作,Cache Line 快取狀態從獨佔轉為共享狀態。
- S:分享,快取行和記憶體資料一致,資料位於多個快取中,監聽其他快取使該快取行失效或者獨享該快取行的操作,如果檢測到這種操作,將該快取行變成無效。
- I:無效的,該快取行的資料無效,沒有監聽,處於失效狀態的快取行需要去主存讀取資料。
- 如何發現資料是否失效?匯流排的嗅探機制每個處理器通過嗅探在匯流排上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從系統記憶體中把資料讀到處理器快取裡。
- 嗅探缺點:匯流排風暴由於Volatile的MESI快取一致性協議,需要不斷的從主記憶體嗅探和CAS不斷迴圈,無效互動會導致匯流排頻寬達到峰值。所以不要大量使用Volatile,至於什麼時候去使用Volatile什麼時候使用鎖,根據場景區分。
本地記憶體與主記憶體
-
JMM定義了執行緒和主記憶體之間的抽象關係:執行緒之間的共享變數儲存在主記憶體中,每個執行緒都有一個私有的本地記憶體或者叫工作記憶體,本地記憶體中儲存了該執行緒以讀/寫共享變數的副本。
-
但本地記憶體是JMM的一個抽象概念,並不真實存在;它涵蓋了快取,寫緩衝區,暫存器以及其他的硬體和編譯器優化。
- 主記憶體和本地記憶體的互動
- 執行緒的本地記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方本地記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。下面AB執行緒為例:
- A執行緒先把本地記憶體的值寫入主記憶體。
- B執行緒從主記憶體中去讀取出A執行緒寫的值。
- 執行緒的本地記憶體中儲存了被該執行緒使用到的變數的主記憶體副本拷貝,執行緒對變數的所有操作都必須在本地記憶體中進行,而不能直接讀寫主記憶體中的變數。不同的執行緒之間也無法直接訪問對方本地記憶體中的變數,執行緒間變數值的傳遞均需要通過主記憶體來完成。下面AB執行緒為例:
原子操作
在此互動過程中,Java記憶體模型定義了8種操作來完成,虛擬機器實現必須保證每一種操作都是原子的、不可再拆分的(double和long型別例外)
- read 讀取,作用於主記憶體把變數從主記憶體中讀取到本本地記憶體。
- load 載入,主要作用本地記憶體,把從主記憶體中讀取的變數載入到本地記憶體的變數副本中
- use 使用,主要作用本地記憶體,把工作記憶體中的一個變數值傳遞給執行引擎,每當虛擬機器遇到一個需要使用變數的值的位元組碼指令時將會執行這個操作。、
- assign 賦值 作用於工作記憶體的變數,它把一個從執行引擎接收到的值賦值給工作記憶體的變數,每當虛擬機器遇到一個給變數賦值的位元組碼指令時執行這個操作。
- store 儲存 作用於工作記憶體的變數,把工作記憶體中的一個變數的值傳送到主記憶體中,以便隨後的write的操作。
- write 寫入 作用於主記憶體的變數,它把store操作從工作記憶體中一個變數的值傳送到主記憶體的變數中。
- lock 鎖定 :作用於主記憶體的變數,把一個變數標識為一條執行緒獨佔狀態。
- unlock 解鎖:作用於主記憶體變數,把一個處於鎖定狀態的變數釋放出來,釋放後的變數才可以被其他執行緒鎖定。
三大特性
一個執行緒在執行的過程中不僅會用到CPU資源,還會用到IO,IO的速度遠遠比不上CPU的運算速度;當一個執行緒要請求IO的時候可以放棄CPU資源,這個時候其他執行緒就可以使用CPU,這就提高了CPU的利用率,當然執行緒之間的切換也會有額外的資源消耗,但多執行緒帶來回報更大。而有了多執行緒就存線上程安全的問題,在Java併發程式設計中的一種思路就是通過原子性、可見性和有序性這三大特性切入點去考慮;在併發程式設計中,必須同時保證程式的原子性、有序性和可見性才能夠保證程式的正確性。
-
原子性
- 定義:一個操作或多個操作做為一個整體,要麼全部執行並且必定成功執行,要麼不執行;簡單理解就是程式的執行是一步到位的。
- 一個或者多個操作在 CPU 執行的過程中不被中斷的特性。由於執行緒的切換,導致多個執行緒同時執行同一段程式碼,帶來的原子性問題。
- 就如我們常見i++也並不是原子操作;i++分為三步,第一步先讀取x的值,第二步進行x+1,第三步x+1的結果寫入到記憶體中。
- 還有如我們前面將單例設計模式的雙層檢測鎖時的instance = new DoubleCheckSingleton() 這一行程式碼jvm內部執行3補步,1先申請堆記憶體,2物件初始化,3物件指向記憶體地址;2和3由於jvm有指令重排序優化所以存在3先執行可能會導致instance還沒有初始化完成,其他執行緒就得到了這個instance不完整單例物件的引用值而報錯。
- 在java當中,直接的讀取操作和賦值(常量)屬於原子性操作。對於原本不具有原子性的操作我們可以通過synchronized關鍵字或者Lock介面來保證同一時間只有一個執行緒執行同一串程式碼,從而也具有了原子性。
-
有序性
-
定義:程式的執行是存在一定順序的。在Java記憶體模型中,為了提高效能和程式的執行效率,編譯器和處理器會對程式指令做重排序。在單執行緒中,重排序不會影響程式的正確性;as-if-serial原則是指不管編譯器和CPU如何重排序,必須保證單執行緒情況下程式的結果是正確的;但在併發程式設計中,卻有可能得出錯誤的結果。
-
在java當中使用volatile關鍵字、synchronized關鍵字或Lock介面來保證有序性。
-
-
可見性
- 定義:指在共享變數被某一個執行緒修改之後,另一個執行緒訪問的時候能夠立刻得到修改以後的值。如果多個執行緒同時讀取一個變數的時候,會在每個快取記憶體中都拷貝一份資料到工作記憶體,記憶體彼此之間是不可見的。快取不能及時重新整理導致了可見性問題。
- JSR-133 記憶體模型使用happens-before原則來闡釋執行緒之間的可見性。如果一個操作對另一個操作存在可見性,那麼他們之間必定符合happens-before原則:
- 程式順序規則:一個執行緒內,按照程式碼順序,書寫在前面的操作先行發生於書寫在後面的操作。
- 監視器鎖規則:一個unLock操作先行發生於後面對同一個鎖的lock操作。
- volatile域規則:對一個變數的寫操作先行發生於後面對這個變數的讀操作。
- 傳遞性規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C。
- 在java當中普通的、未加修飾的共享變數是不能保證可見性的。我們照樣可以通過synchronized關鍵字和Lock介面來保證可見性,同樣也能利用volatile實現。
volatile和synchronized
兩者區別
- volatile只能修飾例項變數和類變數,而synchronized可以修飾方法,以及程式碼塊。
- volatile保證資料的可見性,但是不保證原子性(多執行緒進行寫操作,不保證執行緒安全);而synchronized是一種排他(互斥)的機制。
- volatile用於禁止指令重排序:例如可以解決單例雙重檢查物件初始化程式碼執行亂序問題;volatile是通過記憶體屏障去完成的禁止指令重排序。
- volatile可以看做是輕量版的synchronized,volatile不保證原子性,但是如果是對一個共享變數進行多個執行緒的賦值,而沒有其他的操作,那麼就可以用volatile來代替synchronized,因為賦值本身是有原子性的,而volatile又保證了可見性,所以就可以保證執行緒安全了。
synchronized
-
在Java中任何一個物件都有一個monitor與之關聯,當且一個monitor被持有後,這個物件處於鎖定狀態;嘗試獲得鎖就是嘗試獲取物件所對應的monitor的所有權。
-
synchronized主要原理和思路通過monitor裡面設計一個計數器,synchronized關鍵字在底層編譯後的jvm指令中會有monitorenter(加鎖)和monitorexit(釋放鎖)兩個指令來實現鎖的使用,每個物件都有一個關聯的monitor,比如一個物件例項就有一個monitor,一個類的Class物件也有一個monitor,如果要對這個物件加鎖,那麼必須獲取這個物件關聯的monitor的lock鎖;計數器從0開始;如果一個執行緒要獲取monitor的鎖,就看看他的計數器是不是0,如果是0的話,那麼說明沒人獲取鎖,他就可以獲取鎖了,然後對計數器加1加鎖成功。
-
而物件頭是synchronized實現鎖的基礎,因為synchronized申請鎖、上鎖、釋放鎖都與物件頭有關。物件頭其中一個重要部分Mark Word儲存物件的hashCode、鎖資訊或分代年齡或GC標誌等資訊,鎖總共有四個狀態,分別為無鎖狀態、偏向鎖、輕量級鎖、重量級鎖,鎖的型別和狀態在物件頭Mark Word中都有記錄,在申請鎖、鎖升級等過程中JVM都需要讀取物件的Mark Word資料。java物件主要組成如下:
而物件頭Mark Word組成如下:
volatile
volatile原理有volatile修飾的共享變數進行寫操作的時候會多出Lock字首的指令,該指令在多核處理器下會引發兩件事情。
- 將當前處理器快取行資料刷寫到系統主記憶體。
- 這個刷寫回主記憶體的操作會使其他CPU快取的該共享變數記憶體地址的資料無效。
這樣就保證了多個處理器的快取是一致的,對應的處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器快取行設定無效狀態,當處理器對這個資料進行修改操作的時候會重新從主記憶體中把資料讀取到快取裡。例如在Jdk7的併發包裡新增了一個佇列集合類LinkedTransferQueue,它在使用volatile變數的時候,會採用一種將位元組追加到64位元組的方法來提高效能。那為什麼追加到64位元組能夠優化效能呢?這是因為在很多處理器中它們的L1、L2、L3快取的快取記憶體行都是64位元組寬,不支援填充快取行,例如,現在有兩個不足64位元組的變數AB,那麼在AB變數寫入快取行時會將AB變數的部分資料一起寫入一個快取行中,那麼在CPU1和CPU2想同時訪問AB變數時是無法實現的,也就是想同時訪問一個快取行的時候會引起衝突,如果可以填充到64位元組,AB兩個變數會分別寫入到兩個快取行中,這樣就可以併發,同時進行變數訪問,從而提高效率。
Disruptor實戰
概述
Disruptor是LMAX公司LMAX Development Team開源的高效能記憶體佇列,是一個高效能執行緒間訊息傳遞庫,提供併發環緩衝資料結構的庫;它的設計目的是在非同步事件處理體系結構中提供低延遲、高吞吐量的工作佇列。它能夠讓開發人員只需寫單執行緒程式碼,就能夠獲得非常強悍的效能表現,同時避免了寫併發程式設計的難度和坑; 其本質思想在於多執行緒未必比單執行緒跑的快。
快取行
-
CPU 為了更快的執行程式碼,當從記憶體中讀取資料時並不是只讀自己想要的部分, 而是讀取足夠的位元組來填入快取記憶體行。根據不同的 CPU ,快取記憶體行大小不同,有32個位元組和64個位元組處。這樣,當CPU訪問相鄰的資料時,就不必每次都從記憶體中讀取,提高了速度,這是因為訪問記憶體要比訪問快取記憶體用的時間多得多。這個快取是CPU內部自己的快取,內部的快取單位是行,叫做快取行。
-
當CPU嘗試訪問某個變數時,會先在L1 Cache中查詢,如果命中快取則直接使用;如果沒有找到,就去下一級,一直到記憶體,隨後將該變數所在的一個Cache行大小的區域複製到Cache中。查詢的路線越長,速度也越慢,因此頻繁訪問的資料應當保持在L1Cache中。另外,一個變數的大小往往小於一個Cache行的大小,這時就有可能把多個變數放到一個Cache行中。下面程式碼舉例陣列命中快取行和隨機讀寫執行耗時差異:
package cn.itxs.disruptor;
public class CacheMain {
private static final int ARR_SIZE = 20000;
public static void main(String[] args) {
int[][] arrInt = new int[ARR_SIZE][ARR_SIZE];
long startTime = System.currentTimeMillis();
// 第一種情況為順序訪問,一次訪問後,後面的多次訪問都可以命中快取
for (int i = 0; i < ARR_SIZE; i++) {
for (int j = 0; j < ARR_SIZE; j++) {
arrInt[i][j] = i * j;
}
}
long endTime = System.currentTimeMillis();
System.out.println("順序訪問耗時" + (endTime - startTime) + "毫秒");
startTime = System.currentTimeMillis();
// 第二情況為隨機訪問,每次都無法命中快取行
for (int i = 0; i < ARR_SIZE; i++) {
for (int j = 0; j < ARR_SIZE; j++) {
arrInt[j][i] = i * j;
}
}
endTime = System.currentTimeMillis();
System.out.println("隨機訪問耗時" + (endTime - startTime) + "毫秒");
}
}
偽共享
當CPU執行完後還需要將資料回寫到主記憶體上以便於其它執行緒可以從主記憶體中獲取最新的資料。假設兩個執行緒都載入了相同的CacheLine即快取行資料
- 資料 A、B、C 被載入到同一個 Cache line,假設執行緒 1 在 core1 中修改 A,執行緒 2 在 core2 中修改 B。
- 執行緒 1 首先對 A 進行修改,這時 core1 會告知其它 CPU 核,當前引用同一地址的 Cache line 已經無效,隨後 core2 發起修改 B,會導致 core1 將資料回寫到主記憶體中,core2 這時會重新從主記憶體中讀取該 Cache line 資料。
- 可見,如果同一個CacheLine的內容被多個執行緒讀取,就會產生相互競爭,頻繁回寫主記憶體,降低了效能。
核心概念
- Ring Buffer:環形緩衝區通常被認為是Disruptor的主要點。但從3.0開始Ring Buffer只負責儲存和更新通過Disruptor移動的資料(事件),對於一些高階用例,它甚至可以完全被使用者替換。
- Sequence:Disruptor使用序列作為一種方法來識別特定元件的位置。每個消費者(事件處理器)維護一個序列,就像中斷器本身一樣。大多數併發程式碼依賴於這些Sequence值的移動,因此Sequence支援AtomicLong的許多當前特性。事實上,兩者之間唯一的真正區別是,Sequence包含了額外的功能,以防止Sequence和其他值之間的錯誤共享。
- Sequencer:是Disruptor的真正核心。該介面的兩種實現(單一生產者和多生產者)實現了所有並行演算法,以便在生產者和消費者之間快速、正確地傳遞資料。
- Sequence Barrier:產生了一個序列屏障,其中包含了對Sequencer中釋出的主序列的引用和任何依賴消費者的序列的引用。它包含確定是否有任何事件可供使用者處理的邏輯。
- Wait Strategy:等待策略決定了消費者將如何等待事件被生產者放置到破壞者,如SleepingWaitStrategy、YieldingWaitStrategy、BlockingWaitStrategy、BusySpinWaitStrategy等。
- Event:從生產者傳遞到消費者的資料單位。事件沒有特定的程式碼表示,因為它完全由使用者定義。
- Event Processor:處理來自Disruptor的事件的主事件迴圈,並擁有消費者序列的所有權。有一種稱為BatchEventProcessor的表示,它包含事件迴圈的有效實現,並將回撥到使用過的EventHandler介面的提供實現。
- Event Handler:由使用者實現的介面,代表Disruptor的消費者。
- Producer:這是使用者程式碼呼叫Disruptor來排隊事件。
設計要點
- 記憶體分配更加合理,使用RingBuffer資料結構,陣列元素在初始化時一次性全部建立,提升快取命中率。
- 物件迴圈利用,避免頻繁 GC。
- 能夠避免偽共享,提升快取利用率。Disruptor為了解決偽共享問題,使用的方法是快取行填充,這是一種以空間換時間的策略,主要思想就是通過往物件中填充無意義的變數,來保證整個物件獨佔快取行。而JDK8之後也提供了一個@Contended註解,使用它就可以進行自動填充,使用時需要在啟動時增加一個JVM引數。
- 採用無鎖演算法,避免頻繁加鎖、解鎖的效能消耗。支援批量消費,消費者可以無鎖方式消費多個訊息。
- 有相對更多的等待策略實現。
示例程式碼(多生產者多消費者)
pom檔案引入disruptor的依賴
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.4.4</version>
</dependency>
事件類LongEvent.java
package cn.itxs.disruptor;
public class LongEvent
{
private long value;
public void set(long value)
{
this.value = value;
}
@Override
public String toString() {
return "LongEvent{" +
"value=" + value +
'}';
}
}
事件工廠類EventFactory.java
package cn.itxs.disruptor;
import com.lmax.disruptor.EventFactory;
public class LongEventFactory implements EventFactory<LongEvent> {
@Override
public LongEvent newInstance() {
return new LongEvent();
}
}
事件處理實現類,也即是消費者,這裡實現EventHandler介面,也即是每個消費者都消費相同數量的生產者資料,LongEventHandler.java
package cn.itxs.disruptor;
import com.lmax.disruptor.EventHandler;
public class LongEventHandler implements EventHandler<LongEvent> {
public static long count = 0;
@Override
public void onEvent(LongEvent event, long sequence, boolean endOfBatch) {
count ++;
System.out.println("[" + Thread.currentThread().getName() + "]" + event + "消費序號:" + sequence + ",event=" + event.toString());
}
}
測試類
package cn.itxs.disruptor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.SleepingWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.*;
public class DisruptorMain {
public static void main(String[] args) throws InterruptedException {
// The factory for the event
LongEventFactory factory = new LongEventFactory();
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024*1024;
// Construct the Disruptor
Disruptor<LongEvent> disruptor = new Disruptor<>(factory, bufferSize, Executors.defaultThreadFactory(),
ProducerType.MULTI, new SleepingWaitStrategy());
// Connect the handlers
LongEventHandler h1 = new LongEventHandler();
LongEventHandler h2 = new LongEventHandler();
disruptor.handleEventsWith(h1, h2);
// Start the Disruptor, starts all threads running
disruptor.start();
// Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
//================================================================================================
final int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount);
ExecutorService service = Executors.newCachedThreadPool();
for (long i = 0; i < threadCount; i++) {
final long threadNum = i;
service.submit(()-> {
System.out.printf("Thread %s ready to start!\n", threadNum );
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
for (int j = 0; j < 2; j++) {
final int seq = j;
ringBuffer.publishEvent((event, sequence) -> {
event.set(seq);
System.out.println(threadNum + "執行緒生產了序號為" + sequence + ",訊息為" + seq);
});
}
});
}
service.shutdown();
TimeUnit.SECONDS.sleep(3);
System.out.println(LongEventHandler.count);
}
}
事件處理實現類實現WorkHandler介面,也即是多個消費者合起來消費一份生產者資料,LongEventHandler.java
package cn.itxs.disruptor;
import com.lmax.disruptor.WorkHandler;
public class LongEventHandlerWorker implements WorkHandler<LongEvent> {
public static long count = 0;
@Override
public void onEvent(LongEvent longEvent) throws Exception {
count ++;
System.out.println("[" + Thread.currentThread().getName() + "]" + "event=" + longEvent.toString());
}
}
測試類
package cn.itxs.disruptor;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.SleepingWaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;
import java.util.concurrent.*;
public class DisruptorWorkerMain {
public static void main(String[] args) throws InterruptedException {
// The factory for the event
LongEventFactory factory = new LongEventFactory();
// Specify the size of the ring buffer, must be power of 2.
int bufferSize = 1024*1024;
// Construct the Disruptor
Disruptor<LongEvent> disruptor = new Disruptor<>(factory, bufferSize, Executors.defaultThreadFactory(),
ProducerType.MULTI, new SleepingWaitStrategy());
// Connect the handlers
// 建立10個消費者來處理同一個生產者發的訊息(這10個消費者不重複消費訊息)
LongEventHandlerWorker[] longEventHandlerWorkers = new LongEventHandlerWorker[4];
for (int i = 0; i < longEventHandlerWorkers.length; i++) {
longEventHandlerWorkers[i] = new LongEventHandlerWorker();
}
disruptor.handleEventsWithWorkerPool(longEventHandlerWorkers);
// Start the Disruptor, starts all threads running
disruptor.start();
// Get the ring buffer from the Disruptor to be used for publishing.
RingBuffer<LongEvent> ringBuffer = disruptor.getRingBuffer();
//================================================================================================
final int threadCount = 3;
CyclicBarrier barrier = new CyclicBarrier(threadCount);
ExecutorService service = Executors.newCachedThreadPool();
for (long i = 0; i < threadCount; i++) {
final long threadNum = i;
service.submit(()-> {
System.out.printf("Thread %s ready to start!\n", threadNum );
try {
barrier.await();
} catch (InterruptedException e) {
e.printStackTrace();
} catch (BrokenBarrierException e) {
e.printStackTrace();
}
for (int j = 0; j < 2; j++) {
final int seq = j;
ringBuffer.publishEvent((event, sequence) -> {
event.set(seq);
System.out.println(threadNum + "執行緒生產了序號為" + sequence + ",訊息為" + seq);
});
}
});
}
service.shutdown();
TimeUnit.SECONDS.sleep(3);
System.out.println(LongEventHandlerWorker.count);
}
}
**本人部落格網站 **IT小神 www.itxiaoshen.com