Java并發系列7-Disruptor無鎖緩存框架

聲明:原創文章,轉載請注明出處。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),這個序號指向數組中下一個可用的元素。

Disruptor循環隊列

隨著不停地填充這個buffer(可能也會有相應的讀取),這個序號會一直增長,直到超過這個環。


Disruptor循環隊列

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,但通過LhsPaddingRhsPadding在Value的前后填充了一些空間,使Value無沖突的存在于緩存行中。

參考
http://ifeve.com/dissecting-disruptor-whats-so-special/

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,763評論 6 539
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,238評論 3 428
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,823評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,604評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,339評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,713評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,712評論 3 445
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,893評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,448評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,201評論 3 357
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,397評論 1 372
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,944評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,631評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,033評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,321評論 1 293
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,128評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,347評論 2 377

推薦閱讀更多精彩內容

  • 本文是筆者在研究Disruptor過程中翻譯的Disruptor1.0論文精選,中間穿插了一些感想和說明,均以“譯...
    coder_jerry閱讀 5,210評論 3 52
  • Java-Review-Note——4.多線程 標簽: JavaStudy PS:本來是分開三篇的,后來想想還是整...
    coder_pig閱讀 1,667評論 2 17
  • 距離是什么? 你在的地方下雪了, 而我還在過夏天。 幅員遼闊的中國, 終究隔開了你我。 渠藝 2016.10.26
    渠六億閱讀 201評論 0 3
  • 首先登陸Navicat官網下載Linux版本: https://www.navicat.com.cn/downlo...
    呂志豪閱讀 933評論 0 0
  • 我寫的東西,你最好別看。因為我知道,我寫得不好。可是我需要,我需要表達。負能量退散,正能量快來。寫得不好是,因為太...
    夜深月明閱讀 156評論 0 0