2018-08-02 你應(yīng)該知道的高性能無(wú)鎖隊(duì)列Disruptor

https://juejin.im/post/5b5f10d65188251ad06b78e3

你應(yīng)該知道的高性能無(wú)鎖隊(duì)列Disruptor

1.何為隊(duì)列

聽到隊(duì)列相信大家對(duì)其并不陌生辫塌,在我們現(xiàn)實(shí)生活中隊(duì)列隨處可見匪蝙,去超市結(jié)賬凉泄,你會(huì)看見大家都會(huì)一排排的站得好好的悔政,等待結(jié)賬,為什么要站得一排排的圆恤,你想象一下大家都沒有素質(zhì)刃跛,一窩蜂的上去結(jié)賬蚂蕴,不僅讓這個(gè)超市崩潰,還會(huì)容易造成各種踩踏事件熟掂,當(dāng)然這些事其實(shí)在我們現(xiàn)實(shí)中也是會(huì)經(jīng)常發(fā)生缎浇。

當(dāng)然在計(jì)算機(jī)世界中,隊(duì)列是屬于一種數(shù)據(jù)結(jié)構(gòu)赴肚,隊(duì)列采用的FIFO(first in firstout)素跺,新元素(等待進(jìn)入隊(duì)列的元素)總是被插入到尾部,而讀取的時(shí)候總是從頭部開始讀取誉券。在計(jì)算中隊(duì)列一般用來做排隊(duì)(如線程池的等待排隊(duì)指厌,鎖的等待排隊(duì)),用來做解耦(生產(chǎn)者消費(fèi)者模式)踊跟,異步等等踩验。

2.jdk中的隊(duì)列

在jdk中的隊(duì)列都實(shí)現(xiàn)了java.util.Queue接口,在隊(duì)列中又分為兩類商玫,一類是線程不安全的箕憾,ArrayDeque,LinkedList等等决帖,還有一類都在java.util.concurrent包下屬于線程安全厕九,而在我們真實(shí)的環(huán)境中,我們的機(jī)器都是屬于多線程地回,當(dāng)多線程對(duì)同一個(gè)隊(duì)列進(jìn)行排隊(duì)操作的時(shí)候扁远,如果使用線程不安全會(huì)出現(xiàn),覆蓋數(shù)據(jù)刻像,數(shù)據(jù)丟失等無(wú)法預(yù)測(cè)的事情畅买,所以我們這個(gè)時(shí)候只能選擇線程安全的隊(duì)列。在jdk中提供的線程安全的隊(duì)列下面簡(jiǎn)單列舉部分隊(duì)列:

隊(duì)列名字 是否加鎖 數(shù)據(jù)結(jié)構(gòu) 關(guān)鍵技術(shù)點(diǎn) 是否有鎖 是否有界
ArrayBlockingQueue 數(shù)組array ReentrantLock 有鎖 有界
LinkedBlockingQueue 鏈表 ReentrantLock 有鎖 有界
LinkedTransferQueue 鏈表 CAS 無(wú)鎖 無(wú)界
ConcurrentLinkedQueue 鏈表 CAS 無(wú)鎖 無(wú)界

我們可以看見细睡,我們無(wú)鎖的隊(duì)列是無(wú)界的谷羞,有鎖的隊(duì)列是有界的,這里就會(huì)涉及到一個(gè)問題溜徙,我們?cè)谡嬲木€上環(huán)境中湃缎,無(wú)界的隊(duì)列,對(duì)我們系統(tǒng)的影響比較大蠢壹,有可能會(huì)導(dǎo)致我們內(nèi)存直接溢出嗓违,所以我們首先得排除無(wú)界隊(duì)列,當(dāng)然并不是無(wú)界隊(duì)列就沒用了图贸,只是在某些場(chǎng)景下得排除蹂季。其次還剩下ArrayBlockingQueue冕广,LinkedBlockingQueue兩個(gè)隊(duì)列,他們兩個(gè)都是用ReentrantLock控制的線程安全偿洁,他們兩個(gè)的區(qū)別一個(gè)是數(shù)組撒汉,一個(gè)是鏈表,在隊(duì)列中涕滋,一般獲取這個(gè)隊(duì)列元素之后緊接著會(huì)獲取下一個(gè)元素睬辐,或者一次獲取多個(gè)隊(duì)列元素都有可能,而數(shù)組在內(nèi)存中地址是連續(xù)的何吝,在操作系統(tǒng)中會(huì)有緩存的優(yōu)化(下面也會(huì)介紹緩存行)溉委,所以訪問的速度會(huì)略勝一籌,我們也會(huì)盡量去選擇ArrayBlockingQueue爱榕。而事實(shí)證明在很多第三方的框架中瓣喊,比如早期的log4j異步,都是選擇的ArrayBlockingQueue黔酥。

當(dāng)然ArrayBlockingQueue藻三,也有自己的弊端,就是性能比較低跪者,為什么jdk會(huì)增加一些無(wú)鎖的隊(duì)列棵帽,其實(shí)就是為了增加性能,很苦惱渣玲,又需要無(wú)鎖逗概,又需要有界,這個(gè)時(shí)候恐怕會(huì)忍不住說一句你咋不上天呢忘衍?但是還真有人上天了逾苫。

3.Disruptor

Disruptor就是上面說的那個(gè)天,Disruptor是英國(guó)外匯交易公司LMAX開發(fā)的一個(gè)高性能隊(duì)列枚钓,并且是一個(gè)開源的并發(fā)框架铅搓,并獲得2011Duke’s程序框架創(chuàng)新獎(jiǎng)。能夠在無(wú)鎖的情況下實(shí)現(xiàn)網(wǎng)絡(luò)的Queue并發(fā)操作搀捷,基于Disruptor開發(fā)的系統(tǒng)單線程能支撐每秒600萬(wàn)訂單星掰。目前,包括Apache Storm嫩舟、Camel氢烘、Log4j2等等知名的框架都在內(nèi)部集成了Disruptor用來替代jdk的隊(duì)列,以此來獲得高性能家厌。

3.1為什么這么牛逼威始?

上面已經(jīng)把Disruptor吹出了花了,你肯定會(huì)產(chǎn)生疑問像街,他真的能有這么牛逼嗎黎棠,我的回答是當(dāng)然的,在Disruptor中有三大殺器:

  • CAS
  • 消除偽共享
  • RingBuffer 有了這三大殺器镰绎,Disruptor才變得如此牛逼脓斩。

3.1.1鎖和CAS

我們ArrayBlockingQueue為什么會(huì)被拋棄的一點(diǎn),就是因?yàn)橛昧酥亓考?jí)lock鎖畴栖,在我們加鎖過程中我們會(huì)把鎖掛起随静,解鎖后,又會(huì)把線程恢復(fù),這一過程會(huì)有一定的開銷吗讶,并且我們一旦沒有獲取鎖燎猛,這個(gè)線程就只能一直等待,這個(gè)線程什么事也不能做照皆。

CAS(compare and swap)重绷,顧名思義先比較在交換,一般是比較是否是老的值膜毁,如果是的進(jìn)行交換設(shè)置昭卓,大家熟悉樂觀鎖的人都知道CAS可以用來實(shí)現(xiàn)樂觀鎖,CAS中沒有線程的上下文切換瘟滨,減少了不必要的開銷候醒。 這里使用JMH,用兩個(gè)線程杂瘸,每次1一次調(diào)用倒淫,在我本機(jī)上進(jìn)行測(cè)試,代碼如下:

@BenchmarkMode({Mode.SampleTime})
@OutputTimeUnit(TimeUnit.MILLISECONDS)
@Warmup(iterations=3, time = 5, timeUnit = TimeUnit.MILLISECONDS)
@Measurement(iterations=1,batchSize = 100000000)
@Threads(2)
@Fork(1)
@State(Scope.Benchmark)
public class Myclass {
    Lock lock = new ReentrantLock();
    long i = 0;
    AtomicLong atomicLong = new AtomicLong(0);
    @Benchmark
    public void measureLock() {
        lock.lock();
        i++;
        lock.unlock();
    }
    @Benchmark
    public void measureCAS() {
        atomicLong.incrementAndGet();
    }
    @Benchmark
    public void measureNoLock() {
        i++;
    }
}
復(fù)制代碼

測(cè)試出來結(jié)果如下:

測(cè)試項(xiàng)目 測(cè)試結(jié)果
Lock 26000ms
CAS 4840ms
無(wú)鎖 197ms

可以看見Lock是五位數(shù)败玉,CAS是四位數(shù)敌土,無(wú)鎖更小是三位數(shù)。 由此我們可以知道Lock>CAS>無(wú)鎖绒怨。

而我們的Disruptor中使用的就是CAS纯赎,他利用CAS進(jìn)行隊(duì)列中的一些下標(biāo)設(shè)置,減少了鎖的沖突南蹂,提高了性能犬金。

另外對(duì)于jdk中其他的無(wú)鎖隊(duì)列也是使用CAS,原子類也是使用CAS六剥。

3.1.2偽共享

談到了偽共享就不得不說計(jì)算機(jī)CPU緩存,緩存大小是CPU的重要指標(biāo)之一晚顷,而且緩存的結(jié)構(gòu)和大小對(duì)CPU速度的影響非常大,CPU內(nèi)緩存的運(yùn)行頻率極高疗疟,一般是和處理器同頻運(yùn)作该默,工作效率遠(yuǎn)遠(yuǎn)大于系統(tǒng)內(nèi)存和硬盤。實(shí)際工作時(shí)策彤,CPU往往需要重復(fù)讀取同樣的數(shù)據(jù)塊栓袖,而緩存容量的增大匣摘,可以大幅度提升CPU內(nèi)部讀取數(shù)據(jù)的命中率,而不用再到內(nèi)存或者硬盤上尋找裹刮,以此提高系統(tǒng)性能音榜。但是從CPU芯片面積和成本的因素來考慮,緩存都很小捧弃。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-a913a7-1533189693182-9)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

CPU緩存可以分為一級(jí)緩存赠叼,二級(jí)緩存,如今主流CPU還有三級(jí)緩存违霞,甚至有些CPU還有四級(jí)緩存嘴办。每一級(jí)緩存中所儲(chǔ)存的全部數(shù)據(jù)都是下一級(jí)緩存的一部分,這三種緩存的技術(shù)難度和制造成本是相對(duì)遞減的买鸽,所以其容量也是相對(duì)遞增的涧郊。

為什么CPU會(huì)有L1、L2癞谒、L3這樣的緩存設(shè)計(jì)底燎?主要是因?yàn)楝F(xiàn)在的處理器太快了,而從內(nèi)存中讀取數(shù)據(jù)實(shí)在太慢(一個(gè)是因?yàn)閮?nèi)存本身速度不夠弹砚,另一個(gè)是因?yàn)樗xCPU太遠(yuǎn)了双仍,總的來說需要讓CPU等待幾十甚至幾百個(gè)時(shí)鐘周期),這個(gè)時(shí)候?yàn)榱吮WCCPU的速度桌吃,就需要延遲更小速度更快的內(nèi)存提供幫助朱沃,而這就是緩存。對(duì)這個(gè)感興趣可以把電腦CPU拆下來茅诱,自己把玩一下逗物。

每一次你聽見intel發(fā)布新的cpu什么,比如i7-7700k,8700k,都會(huì)對(duì)cpu緩存大小進(jìn)行優(yōu)化瑟俭,感興趣可以自行下來搜索翎卓,這些的發(fā)布會(huì)或者發(fā)布文章。

header 1 header 2
row 1 col 1 row 1 col 2
row 2 col 1 row 2 col 2

Martin和Mike的 QConpresentation演講中給出了一些每個(gè)緩存時(shí)間:

從CPU到 大約需要的CPU周期 大約需要的時(shí)間
主存 約60-80納秒
QPI 總線傳輸(between sockets, not drawn) 約20ns
L3 cache 約40-45 cycles 約15ns
L2 cache 約10 cycles 約3ns
L1 cache 約3-4 cycles 約1ns
寄存器 1 cycle

緩存行

在cpu的多級(jí)緩存中摆寄,并不是以獨(dú)立的項(xiàng)來保存的失暴,而是類似一種pageCahe的一種策略,以緩存行來保存微饥,而緩存行的大小通常是64字節(jié)逗扒,在Java中Long是8個(gè)字節(jié),所以可以存儲(chǔ)8個(gè)Long,舉個(gè)例子欠橘,你訪問一個(gè)long的變量的時(shí)候矩肩,他會(huì)把幫助再加載7個(gè),我們上面說為什么選擇數(shù)組不選擇鏈表肃续,也就是這個(gè)原因黍檩,在數(shù)組中可以依靠緩沖行得到很快的訪問叉袍。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-f42d23-1533189693181-8)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

緩存行是萬(wàn)能的嗎?NO刽酱,因?yàn)樗廊粠砹艘粋€(gè)缺點(diǎn)畦韭,我在這里舉個(gè)例子說明這個(gè)缺點(diǎn),可以想象有個(gè)數(shù)組隊(duì)列肛跌,ArrayQueue,他的數(shù)據(jù)結(jié)構(gòu)如下:

class ArrayQueue{
    long maxSize;
    long currentIndex;
}
復(fù)制代碼

對(duì)于maxSize是我們一開始就定義好的察郁,數(shù)組的大小衍慎,對(duì)于currentIndex,是標(biāo)志我們當(dāng)前隊(duì)列的位置皮钠,這個(gè)變化比較快稳捆,可以想象你訪問maxSize的時(shí)候,是不是把currentIndex也加載進(jìn)來了麦轰,這個(gè)時(shí)候乔夯,其他線程更新currentIndex,就會(huì)把cpu中的緩存行置位無(wú)效,請(qǐng)注意這是CPU規(guī)定的款侵,他并不是只吧currentIndex置位無(wú)效末荐,如果此時(shí)又繼續(xù)訪問maxSize他依然得繼續(xù)從內(nèi)存中讀取,但是MaxSize卻是我們一開始定義好的新锈,我們應(yīng)該訪問緩存即可甲脏,但是卻被我們經(jīng)常改變的currentIndex所影響。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-40a670-1533189693181-7)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

Padding的魔法

為了解決上面緩存行出現(xiàn)的問題妹笆,在Disruptor中采用了Padding的方式块请,

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;
}
復(fù)制代碼

其中的Value就被其他一些無(wú)用的long變量給填充了。這樣你修改Value的時(shí)候拳缠,就不會(huì)影響到其他變量的緩存行墩新。

最后順便一提,在jdk8中提供了@Contended的注解窟坐,當(dāng)然一般來說只允許Jdk中內(nèi)部海渊,如果你自己使用那就得配置Jvm參數(shù) -RestricContentended = fase,將限制這個(gè)注解置位取消狸涌。很多文章分析了ConcurrentHashMap切省,但是都把這個(gè)注解給忽略掉了,在ConcurrentHashMap中就使用了這個(gè)注解帕胆,在ConcurrentHashMap每個(gè)桶都是單獨(dú)的用計(jì)數(shù)器去做計(jì)算朝捆,而這個(gè)計(jì)數(shù)器由于時(shí)刻都在變化,所以被用這個(gè)注解進(jìn)行填充緩存行優(yōu)化懒豹,以此來增加性能芙盘。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-f04e88-1533189693181-6)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

3.1.3RingBuffer

在Disruptor中采用了數(shù)組的方式保存了我們的數(shù)據(jù)驯用,上面我們也介紹了采用數(shù)組保存我們?cè)L問時(shí)很好的利用緩存,但是在Disruptor中進(jìn)一步選擇采用了環(huán)形數(shù)組進(jìn)行保存數(shù)據(jù)儒老,也就是RingBuffer蝴乔。在這里先說明一下環(huán)形數(shù)組并不是真正的環(huán)形數(shù)組,在RingBuffer中是采用取余的方式進(jìn)行訪問的驮樊,比如數(shù)組大小為 10薇正,0訪問的是數(shù)組下標(biāo)為0這個(gè)位置,其實(shí)10囚衔,20等訪問的也是數(shù)組的下標(biāo)為0的這個(gè)位置挖腰。

實(shí)際上,在這些框架中取余并不是使用%運(yùn)算练湿,都是使用的&與運(yùn)算猴仑,這就要求你設(shè)置的大小一般是2的N次方也就是,10,100,1000等等肥哎,這樣減去1的話就是辽俗,1,11篡诽,111崖飘,就能很好的使用index & (size -1),這樣利用位運(yùn)算就增加了訪問速度。 如果在Disruptor中你不用2的N次方進(jìn)行大小設(shè)置霞捡,他會(huì)拋出buffersize必須為2的N次方異常坐漏。

當(dāng)然其不僅解決了數(shù)組快速訪問的問題,也解決了不需要再次分配內(nèi)存的問題碧信,減少了垃圾回收赊琳,因?yàn)槲覀?,10砰碴,20等都是執(zhí)行的同一片內(nèi)存區(qū)域躏筏,這樣就不需要再次分配內(nèi)存,頻繁的被JVM垃圾回收器回收呈枉。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-7c69e7-1533189693181-5)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

自此三大殺器已經(jīng)說完了趁尼,有了這三大殺器為Disruptor如此高性能墊定了基礎(chǔ)。接下來還會(huì)在講解如何使用Disruptor和Disruptor的具體的工作原理猖辫。

3.2Disruptor怎么使用

下面舉了一個(gè)簡(jiǎn)單的例子:

ublic static void main(String[] args) throws Exception {
        // 隊(duì)列中的元素
        class Element {
            @Contended
            private String value;

            public String getValue() {
                return value;
            }

            public void setValue(String value) {
                this.value = value;
            }
        }

        // 生產(chǎn)者的線程工廠
        ThreadFactory threadFactory = new ThreadFactory() {
            int i = 0;
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "simpleThread" + String.valueOf(i++));
            }
        };

        // RingBuffer生產(chǎn)工廠,初始化RingBuffer的時(shí)候使用
        EventFactory<Element> factory = new EventFactory<Element>() {
            @Override
            public Element newInstance() {
                return new Element();
            }
        };

        // 處理Event的handler
        EventHandler<Element> handler = new EventHandler<Element>() {
            @Override
            public void onEvent(Element element, long sequence, boolean endOfBatch) throws InterruptedException {
                System.out.println("Element: " + Thread.currentThread().getName() + ": " + element.getValue() + ": " + sequence);
//                Thread.sleep(10000000);
            }
        };

        // 阻塞策略
        BlockingWaitStrategy strategy = new BlockingWaitStrategy();

        // 指定RingBuffer的大小
        int bufferSize = 8;

        // 創(chuàng)建disruptor酥泞,采用單生產(chǎn)者模式
        Disruptor<Element> disruptor = new Disruptor(factory, bufferSize, threadFactory, ProducerType.SINGLE, strategy);

        // 設(shè)置EventHandler
        disruptor.handleEventsWith(handler);

        // 啟動(dòng)disruptor的線程
        disruptor.start();
        for (int i = 0; i < 10; i++) {
            disruptor.publishEvent((element, sequence) -> {
                System.out.println("之前的數(shù)據(jù)" + element.getValue() + "當(dāng)前的sequence" + sequence);
                element.setValue("我是第" + sequence + "個(gè)");
            });

        }
    }
復(fù)制代碼

在Disruptor中有幾個(gè)比較關(guān)鍵的: ThreadFactory:這是一個(gè)線程工廠,用于我們Disruptor中生產(chǎn)者消費(fèi)的時(shí)候需要的線程啃憎。 EventFactory:事件工廠芝囤,用于產(chǎn)生我們隊(duì)列元素的工廠,在Disruptor中,他會(huì)在初始化的時(shí)候直接填充滿RingBuffer悯姊,一次到位羡藐。 EventHandler:用于處理Event的handler,這里一個(gè)EventHandler可以看做是一個(gè)消費(fèi)者悯许,但是多個(gè)EventHandler他們都是獨(dú)立消費(fèi)的隊(duì)列仆嗦。 WorkHandler:也是用于處理Event的handler,和上面區(qū)別在于先壕,多個(gè)消費(fèi)者都是共享同一個(gè)隊(duì)列瘩扼。 WaitStrategy:等待策略,在Disruptor中有多種策略垃僚,來決定消費(fèi)者獲消費(fèi)時(shí)邢隧,如果沒有數(shù)據(jù)采取的策略是什么?下面簡(jiǎn)單列舉一下Disruptor中的部分策略

  • BlockingWaitStrategy:通過線程阻塞的方式冈在,等待生產(chǎn)者喚醒,被喚醒后按摘,再循環(huán)檢查依賴的sequence是否已經(jīng)消費(fèi)包券。

  • BusySpinWaitStrategy:線程一直自旋等待,可能比較耗cpu

  • LiteBlockingWaitStrategy:線程阻塞等待生產(chǎn)者喚醒炫贤,與BlockingWaitStrategy相比溅固,區(qū)別在signalNeeded.getAndSet,如果兩個(gè)線程同時(shí)訪問一個(gè)訪問waitfor,一個(gè)訪問signalAll時(shí),可以減少lock加鎖次數(shù).

  • LiteTimeoutBlockingWaitStrategy:與LiteBlockingWaitStrategy相比兰珍,設(shè)置了阻塞時(shí)間侍郭,超過時(shí)間后拋異常。

  • YieldingWaitStrategy:嘗試100次掠河,然后Thread.yield()讓出cpu

EventTranslator:實(shí)現(xiàn)這個(gè)接口可以將我們的其他數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為在Disruptor中流通的Event亮元。

3.3工作原理

上面已經(jīng)介紹了CAS,減少偽共享,RingBuffer三大殺器唠摹,介紹下來說一下Disruptor中生產(chǎn)者和消費(fèi)者的整個(gè)流程爆捞。

3.3.1生產(chǎn)者

對(duì)于生產(chǎn)者來說,可以分為多生產(chǎn)者和單生產(chǎn)者勾拉,用ProducerType.Single,和ProducerType.MULTI區(qū)分煮甥,多生產(chǎn)者和單生產(chǎn)者來說多了CAS,因?yàn)閱紊a(chǎn)者由于是單線程藕赞,所以不需要保證線程安全成肘。

在disruptor中通常用disruptor.publishEvent和disruptor.publishEvents()進(jìn)行單發(fā)和群發(fā)。

在disruptor發(fā)布一個(gè)事件進(jìn)入隊(duì)列需要下面幾個(gè)步驟:

  1. 首先獲取RingBuffer中下一個(gè)在RingBuffer上可以發(fā)布的位置斧蜕,這個(gè)可以分為兩類:
  • 從來沒有寫過的位置

  • 已經(jīng)被所有消費(fèi)者讀過双霍,可以在寫的位置。 如果沒有讀取到會(huì)一直嘗試去讀,disruptor做的很巧妙店煞,并沒有一直占據(jù)CPU蟹演,而是通過LockSuport.park(),進(jìn)行了一下將線程阻塞掛起操作顷蟀,為的是不讓CPU一直進(jìn)行這種空循環(huán)酒请,不然其他線程都搶不到CPU時(shí)間片。

    <figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-6716a4-1533189693174-1)]

    <figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

    </figure>

    獲取位置之后會(huì)進(jìn)行cas進(jìn)行搶占鸣个,如果是單線程就不需要羞反。

  1. 接下來調(diào)用我們上面所介紹的EventTranslator將第一步中RingBuffer中那個(gè)位置的event交給EventTranslator進(jìn)行重寫。
  2. 進(jìn)行發(fā)布囤萤,在disruptor還有一個(gè)額外的數(shù)組用來記錄當(dāng)前ringBuffer所在位置目前最新的序列號(hào)是多少昼窗,拿上面那個(gè)0,10涛舍,20舉例澄惊,寫到10的時(shí)候這個(gè)avliableBuffer就在對(duì)應(yīng)的位置上記錄目前這個(gè)是屬于10,有什么用呢后面會(huì)介紹富雅。進(jìn)行發(fā)布的時(shí)候需要更新這個(gè)avliableBuffer掸驱,然后進(jìn)行喚醒所有阻塞的生產(chǎn)者。

下面簡(jiǎn)單畫一下流程没佑,上面我們拿10舉例是不對(duì)的毕贼,因?yàn)閎ufferSize必須要2的N次方,所以我們這里拿Buffersize=8來舉例:下面介紹了當(dāng)我們已經(jīng)push了8個(gè)event也就是一圈的時(shí)候蛤奢,接下來再push 3條消息的一些過程: 1.首先調(diào)用next(3)鬼癣,我們當(dāng)前在7這個(gè)位置上所以接下來三條是8,9啤贩,10待秃,取余也就是0,1痹屹,2锥余。 2.重寫0,1痢掠,2這三個(gè)內(nèi)存區(qū)域的數(shù)據(jù)驱犹。 3.寫avaliableBuffer。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-29eb30-1533189693180-4)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

對(duì)了不知道大家對(duì)上述流程是不是很熟悉呢足画,對(duì)的那就是類似我們的2PC雄驹,兩階段提交,先進(jìn)行RingBuffer的位置鎖定淹辞,然后在進(jìn)行提交和通知消費(fèi)者医舆。具體2PC的介紹可以參照我的另外一篇文章再有人問你分布式事務(wù),給他看這篇文章

3.3.1消費(fèi)者

對(duì)于消費(fèi)者來說蔬将,上面介紹了分為兩種爷速,一種是多個(gè)消費(fèi)者獨(dú)立消費(fèi),一種是多個(gè)消費(fèi)者消費(fèi)同一個(gè)隊(duì)列霞怀,這里介紹一下較為復(fù)雜的多個(gè)消費(fèi)者消費(fèi)同一個(gè)隊(duì)列惫东,能理解這個(gè)也就能理解獨(dú)立消費(fèi)。 在我們的disruptor.strat()方法中會(huì)啟動(dòng)我們的消費(fèi)者線程以此來進(jìn)行后臺(tái)消費(fèi)毙石。在消費(fèi)者中有兩個(gè)隊(duì)列需要我們關(guān)注廉沮,一個(gè)是所有消費(fèi)者共享的進(jìn)度隊(duì)列,還有個(gè)是每個(gè)消費(fèi)者獨(dú)立消費(fèi)進(jìn)度隊(duì)列徐矩。 1.對(duì)消費(fèi)者共享隊(duì)列進(jìn)行下一個(gè)Next的CAS搶占滞时,以及對(duì)自己消費(fèi)進(jìn)度的隊(duì)列標(biāo)記當(dāng)前進(jìn)度。 2.為自己申請(qǐng)可讀的RingBuffer的Next位置滤灯,這里的申請(qǐng)不僅僅是申請(qǐng)到next坪稽,有可能會(huì)申請(qǐng)到比Next大的一個(gè)范圍,阻塞策略的申請(qǐng)的過程如下:

  • 獲取生產(chǎn)者對(duì)RingBuffer最新寫的位置

  • 判斷其是否小于我要申請(qǐng)讀的位置

  • 如果大于則證明這個(gè)位置已經(jīng)寫了鳞骤,返回給生產(chǎn)者刽漂。

  • 如果小于證明還沒有寫到這個(gè)位置,在阻塞策略中會(huì)進(jìn)行阻塞弟孟,其會(huì)在生產(chǎn)者提交階段進(jìn)行喚醒。 3.對(duì)這個(gè)位置進(jìn)行可讀校驗(yàn)样悟,因?yàn)槟闵暾?qǐng)的位置可能是連續(xù)的拂募,比如生產(chǎn)者目前在7,接下來申請(qǐng)讀窟她,如果消費(fèi)者已經(jīng)把8和10這個(gè)序列號(hào)的位置寫進(jìn)去了陈症,但是9這個(gè)位置還沒來得及寫入,由于第一步會(huì)返回10震糖,但是9其實(shí)是不能讀的录肯,所以得把位置向下收縮到8。

    <figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-8d94c8-1533189693174-0)]

    <figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

    </figure>

    4.如果收縮完了之后比當(dāng)前next要小吊说,則繼續(xù)循環(huán)申請(qǐng)论咏。 5.交給handler.onEvent()處理

一樣的我們舉個(gè)例子,我們要申請(qǐng)next=8這個(gè)位置颁井。 1.首先在共享隊(duì)列搶占進(jìn)度8厅贪,在獨(dú)立隊(duì)列寫入進(jìn)度7 2.獲取8的可讀的最大位置,這里根據(jù)不同的策略進(jìn)行雅宾,我們選擇阻塞养涮,由于生產(chǎn)者生產(chǎn)了8,9,10贯吓,所以返回的是10懈凹,這樣和后續(xù)就不需要再次和avaliableBuffer進(jìn)行對(duì)比了。 3.最后交給handler進(jìn)行處理悄谐。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-5b2164-1533189693180-3)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

4.Log4j中的Disruptor

下面的圖展現(xiàn)了Log4j使用Disruptor,ArrayBlockingQueue以及同步的Log4j吞吐量的對(duì)比介评,可以看見使用了Disruptor完爆了其他的,當(dāng)然還有更多的框架使用了Disruptor尊沸,這里就不做介紹了威沫。

<figure style="display: block; margin: 22px auto; text-align: center;">[圖片上傳中...(image-9ac368-1533189693180-2)]

<figcaption style="display: block; text-align: center; font-size: 1rem; line-height: 1.6; color: rgb(144, 144, 144); margin-top: 2px;"></figcaption>

</figure>

最后

本文介紹了傳統(tǒng)的阻塞隊(duì)列的缺點(diǎn),后文重點(diǎn)吹逼了下Disruptor洼专,以及他這么牛逼的原因棒掠,以及具體的工作流程。

如果以后有人問你叫你設(shè)計(jì)一個(gè)高效無(wú)鎖隊(duì)列屁商,需要怎么設(shè)計(jì)烟很?相信你能從文章中總結(jié)出答案,如果對(duì)其有疑問或者想和我交流思路蜡镶,可以關(guān)注我的公眾號(hào)雾袱,加我好友和我一起討論。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末官还,一起剝皮案震驚了整個(gè)濱河市芹橡,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌望伦,老刑警劉巖林说,帶你破解...
    沈念sama閱讀 222,590評(píng)論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異屯伞,居然都是意外死亡腿箩,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,157評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門劣摇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來珠移,“玉大人,你說我怎么就攤上這事末融【澹” “怎么了?”我有些...
    開封第一講書人閱讀 169,301評(píng)論 0 362
  • 文/不壞的土叔 我叫張陵勾习,是天一觀的道長(zhǎng)垢乙。 經(jīng)常有香客問我,道長(zhǎng)语卤,這世上最難降的妖魔是什么追逮? 我笑而不...
    開封第一講書人閱讀 60,078評(píng)論 1 300
  • 正文 為了忘掉前任酪刀,我火速辦了婚禮,結(jié)果婚禮上钮孵,老公的妹妹穿的比我還像新娘骂倘。我一直安慰自己,他們只是感情好巴席,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,082評(píng)論 6 398
  • 文/花漫 我一把揭開白布历涝。 她就那樣靜靜地躺著,像睡著了一般漾唉。 火紅的嫁衣襯著肌膚如雪荧库。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,682評(píng)論 1 312
  • 那天赵刑,我揣著相機(jī)與錄音分衫,去河邊找鬼。 笑死般此,一個(gè)胖子當(dāng)著我的面吹牛蚪战,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播铐懊,決...
    沈念sama閱讀 41,155評(píng)論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼邀桑,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了科乎?” 一聲冷哼從身側(cè)響起壁畸,我...
    開封第一講書人閱讀 40,098評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎茅茂,沒想到半個(gè)月后捏萍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,638評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡玉吁,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,701評(píng)論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了腻异。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片进副。...
    茶點(diǎn)故事閱讀 40,852評(píng)論 1 353
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖悔常,靈堂內(nèi)的尸體忽然破棺而出影斑,到底是詐尸還是另有隱情,我是刑警寧澤机打,帶...
    沈念sama閱讀 36,520評(píng)論 5 351
  • 正文 年R本政府宣布矫户,位于F島的核電站,受9級(jí)特大地震影響残邀,放射性物質(zhì)發(fā)生泄漏皆辽。R本人自食惡果不足惜柑蛇,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,181評(píng)論 3 335
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望驱闷。 院中可真熱鬧耻台,春花似錦、人聲如沸空另。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,674評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)扼菠。三九已至摄杂,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間循榆,已是汗流浹背析恢。 一陣腳步聲響...
    開封第一講書人閱讀 33,788評(píng)論 1 274
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留冯痢,地道東北人氮昧。 一個(gè)月前我還...
    沈念sama閱讀 49,279評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像浦楣,于是被迫代替她去往敵國(guó)和親袖肥。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,851評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容