聲明:原創文章,轉載請注明出處。http://www.lxweimin.com/u/e02df63eaa87
1、從生產者消費者說起
在傳統的生產者消費者模型中,通常是采用BlockingQueue實現。其中生產者線程負責提交需求,消費者線程負責處理任務,二者之間通過共享內存緩沖區進行通信。由于內存緩沖區的存在,允許生產者和消費者之間速度的差異,確保系統正常運行。
下圖展示一個簡單的生產者消費者模型,生產者從文件中讀取數據,將數據內容寫入到阻塞隊列中,消費者從隊列的另一邊獲取數據,進行計算并將結果輸出。其中Main負責創建兩類線程并初始化隊列。
Main:
public class Main {
public static void main(String[] args) {
// 初始化阻塞隊列
BlockingQueue<String> blockingQueue = new ArrayBlockingQueue<>(1000);
// 創建生產者線程
Thread producer = new Thread(new Producer(blockingQueue, "temp.dat"));
producer.start();
// 創建消費者線程
Thread consumer = new Thread(new Consumer(blockingQueue));
consumer.start();
}
}
生產者:
public class Producer implements Runnable {
private BlockingQueue<String> blockingQueue;
private String fileName;
private static final String FINIDHED = "EOF";
public Producer(BlockingQueue<String> blockingQueue, String fileName) {
this.blockingQueue = blockingQueue;
this.fileName = fileName;
}
@Override
public void run() {
try {
BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)));
String line;
while ((line = reader.readLine()) != null) {
blockingQueue.put(line);
}
// 結束標志
blockingQueue.put(FINIDHED);
} catch (Exception e) {
e.printStackTrace();
}
}
}
消費者:
public class Consumer implements Runnable {
private BlockingQueue<String> blockingQueue;
private static final String FINIDHED = "EOF";
public Consumer(BlockingQueue<String> blockingQueue) {
this.blockingQueue = blockingQueue;
}
@Override
public void run() {
String line;
String[] arrStr;
int ret;
try {
while (!(line = blockingQueue.take()).equals(FINIDHED)) {
// 消費
arrStr = line.split("\t");
if (arrStr.length != 2) {
continue;
}
ret = Integer.parseInt(arrStr[0]) + Integer.parseInt(arrStr[1]);
System.out.println(ret);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
生產者-消費者模型可以很容易地將生產和消費進行解耦,優化系統整體結構,并且由于存在緩沖區,可以緩解兩端性能不匹配的問題。
2、BlockingQueue的不足
上述使用了ArrayBlockingQueue
,通過查看其實現,完全是使用鎖和阻塞等待實現線程同步。在高并發場景下,性能不是很優越。
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
insert(e);
} finally {
lock.unlock();
}
}
但是,ConcurrentLinkedQueue
卻是一個高性能隊列,這是因為其實現使用了無鎖的CAS操作。
3、Disruptor初體驗
Disruptor是由LMAX公司開發的一款高效無鎖內存隊列。使用無鎖方式實現了一個環形隊列代替線性隊列。相對于普通的線性隊列,環形隊列不需要維護頭尾兩個指針,只需維護一個當前位置就可以完成出入隊操作。受限于環形結構,隊列的大小只能初始化時指定,不能動態擴展。
如下圖所示,Disruptor的實現為一個循環隊列,ringbuffer擁有一個序號(Seq),這個序號指向數組中下一個可用的元素。
隨著不停地填充這個buffer(可能也會有相應的讀取),這個序號會一直增長,直到超過這個環。
Disruptor要求數組大小設置為2的N次方。這樣可以通過Seq & (QueueSize - 1) 直接獲取,其效率要比取模快得多。這是因為(Queue - 1)的二進制為全1等形式。例如,上圖中QueueSize大小為8,Seq為10,則只需要計算二進制1010 & 0111 = 2,可直接得到index=2位置的元素。
在RingBuffer中,生產者向數組中寫入數據,生產者寫入數據時,使用CAS操作。消費者從中讀取數據時,為防止多個消費者同時處理一個數據,也使用CAS操作進行數據保護。
這種固定大小的RingBuffer還有一個好處是,可以內存復用。不會有新空間需要分配或者舊的空間回收,當數組填充滿后,再寫入數據會將數據覆蓋。
4、Disruptor小試牛刀
同樣地,使用Disruptor處理第一節中的生產者消費者的案例。
4.1 添加Maven依賴
<dependency>
<groupId>com.lmax</groupId>
<artifactId>disruptor</artifactId>
<version>3.3.2</version>
</dependency>
4.2 定義事件對象
由于我們只需要將文件中的數據行讀出,然后進行計算。因此,定義FileData.class
來保存文件行。
public class FileData {
private String line;
public String getLine() {
return line;
}
public void setLine(String line) {
this.line = line;
}
}
4.3 定義工廠類
用于產生FileData
的工廠類,會在Disruptor系統初始化時,構造所有的緩沖區中的對象實例。
public class DisruptorFactory implements EventFactory<FileData> {
public FileData newInstance() {
return new FileData();
}
}
4.4 定義消費者
消費者的作用是讀取數據并進行處理。數據的讀取已經由Disruptor封裝,onEvent()
方法為Disruptor框架的回調方法。只需要進行簡單的數據處理即可。
public class DisruptorConsumer implements WorkHandler<FileData> {
private static final String FINIDHED = "EOF";
@Override
public void onEvent(FileData event) throws Exception {
String line = event.getLine();
if (line.equals(FINIDHED)) {
return;
}
// 消費
String[] arrStr = line.split("\t");
if (arrStr.length != 2) {
return;
}
int ret = Integer.parseInt(arrStr[0]) + Integer.parseInt(arrStr[1]);
System.out.println(ret);
}
}
4.5 定義生產者
生產者需要一個Ringbuffer
的引用。其中pushData()
方法是將生產的數據寫入到RingBuffer中。具體的過程是,首先通過next()
方法得到下一個可用的序列號;取得下一個可用的FileData
,并設置該對象的值;最后,進行數據發布,這個FileData
對象會傳遞給消費者。
public class DisruptorProducer {
private static final String FINIDHED = "EOF";
private final RingBuffer<FileData> ringBuffer;
public DisruptorProducer(RingBuffer<FileData> ringBuffer) {
this.ringBuffer = ringBuffer;
}
public void pushData(String line) {
long seq = ringBuffer.next();
try {
FileData event = ringBuffer.get(seq); // 獲取可用位置
event.setLine(line); // 填充可用位置
} catch (Exception e) {
e.printStackTrace();
} finally {
ringBuffer.publish(seq); // 通知消費者
}
}
public void read(String fileName) {
try {
BufferedReader reader = new BufferedReader(new FileReader(new File(fileName)));
String line;
while ((line = reader.readLine()) != null) {
// 生產數據
pushData(line);
}
// 結束標志
pushData(FINIDHED);
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.6 定義Main函數
最后需要一個DisruptorMain()
將上述的數據、生產者和消費者進行整合。
public class DisruptorMain {
public static void main(String[] args) {
DisruptorFactory factory = new DisruptorFactory(); // 工廠
ExecutorService executor = Executors.newCachedThreadPool(); // 線程池
int BUFFER_SIZE = 16; // 必須為2的冪指數
// 初始化Disruptor
Disruptor<FileData> disruptor = new Disruptor<>(factory,
BUFFER_SIZE,
executor,
ProducerType.MULTI, // Create a RingBuffer supporting multiple event publishers to the one RingBuffer
new BlockingWaitStrategy() // 默認阻塞策略
);
// 啟動消費者
disruptor.handleEventsWithWorkerPool(new DisruptorConsumer(),
new DisruptorConsumer()
);
disruptor.start();
// 啟動生產者
RingBuffer<FileData> ringBuffer = disruptor.getRingBuffer();
DisruptorProducer producer = new DisruptorProducer(ringBuffer);
producer.read("temp.dat");
// 關閉
disruptor.shutdown();
executor.shutdown();
}
}
5、Disruptor策略
Disruptor生產者和消費者之間是通過什么策略進行同步呢?Disruptor提供了如下幾種策略:
- BlockingWaitStrategy:默認等待策略。和BlockingQueue的實現很類似,通過使用鎖和條件(Condition)進行線程同步和喚醒。此策略對于線程切換來說,最節約CPU資源,但在高并發場景下性能有限。
-
SleepingWaitStrategy:CPU友好型策略。會在循環中不斷等待數據。首先進行自旋等待,若不成功,則使用
Thread.yield()
讓出CPU,并使用LockSupport.parkNanos(1)
進行線程睡眠。所以,此策略數據處理數據可能會有較高的延遲,適合用于對延遲不敏感的場景。優點是對生產者線程影響小,典型應用場景是異步日志。 -
YieldingWaitStrategy:低延時策略。消費者線程會不斷循環監控RingBuffer的變化,在循環內部使用
Thread.yield()
讓出CPU給其他線程。 - BusySpinWaitStrategy:死循環策略。消費者線程會盡最大可能監控緩沖區的變化,會占用所有CPU資源。
6、Disruptor解決CPU Cache偽共享問題
為了解決CPU和內存速度不匹配的問題,CPU中有多個高速緩存Cache。在Cache中,讀寫數據的基本單位是緩存行,緩存行是內存復制到緩存的最小單位。
若兩個變量放在同一個Cache Line中,在多線程情況下,可能會相互影響彼此的性能。如上圖所示,CPU1上的線程更新了變量X,則CPU上的緩存行會失效,同一行的Y即使沒有更新也會失效,導致Cache無法命中。
同樣地,若CPU2上的線程更新了Y,則導致CPU1上的緩存行又失效。如果CPU經常不能命中緩存,則系統的吞吐量則會下降。這就是偽共享問題。
解決偽共享問題,可以在變量的前后都占據一定的填充位置,盡量讓變量占用一個完整的緩存行。如上圖中,CPU1上的線程更新了X,則CPU2上的Y則不會失效。同樣地,CPU2上的線程更新了Y,則CPU1的不會失效。
class LhsPadding
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
class Value extends LhsPadding
{
protected volatile long value;
}
class RhsPadding extends Value
{
protected long p9, p10, p11, p12, p13, p14, p15;
}
/**
* <p>Concurrent sequence class used for tracking the progress of
* the ring buffer and event processors. Support a number
* of concurrent operations including CAS and order writes.
*
* <p>Also attempts to be more efficient with regards to false
* sharing by adding padding around the volatile field.
*/
public class Sequence extends RhsPadding
{
static final long INITIAL_VALUE = -1L;
private static final Unsafe UNSAFE;
private static final long VALUE_OFFSET;
static
{
UNSAFE = Util.getUnsafe();
try
{
VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));
}
catch (final Exception e)
{
throw new RuntimeException(e);
}
}
... ...
}
在Sequence
的實現中,主要使用的是Value,但通過LhsPadding
和RhsPadding
在Value的前后填充了一些空間,使Value無沖突的存在于緩存行中。