中文字幕在线观看,亚洲а∨天堂久久精品9966,亚洲成a人片在线观看你懂的,亚洲av成人片无码网站,亚洲国产精品无码久久久五月天

Disruptor源碼閱讀筆記

2018-10-08    來(lái)源:importnew

容器云強(qiáng)勢(shì)上線!快速搭建集群,上萬(wàn)Linux鏡像隨意使用

Disruptor是什么

關(guān)于 Disruptor,網(wǎng)絡(luò)上有很多的解釋和說(shuō)法。這里簡(jiǎn)單的概括下。Disruptor 是一個(gè)消費(fèi)者生產(chǎn)者隊(duì)列框架,據(jù)官網(wǎng)介紹,可以提供非常強(qiáng)大的性能。Disruptor 與其說(shuō)為我們帶來(lái)了一個(gè)框架,更多的是為我們帶來(lái)了一個(gè)獨(dú)特思路的編程實(shí)踐?偨Y(jié)來(lái)說(shuō)大致有3點(diǎn)。

  • 使用循環(huán)數(shù)組的方式代替隊(duì)列,使用預(yù)先填充數(shù)據(jù)的方式來(lái)避免 GC;
  • 使用 CPU 緩存行填充的方式來(lái)避免極端情況下的數(shù)據(jù)爭(zhēng)用導(dǎo)致的性能下降;
  • 多線程編程中盡量避免鎖爭(zhēng)用的編碼技巧。

上面的三點(diǎn)是在 Disruptor 中帶來(lái)的一些技巧。有些是常用的,有些是實(shí)現(xiàn)起來(lái)比較獨(dú)特的。

使用循環(huán)數(shù)組代替隊(duì)列

生產(chǎn)者消費(fèi)者模型自然是離不開隊(duì)列的。但是使用傳統(tǒng)的隊(duì)列,面對(duì)并發(fā)等問(wèn)題,在性能上是否已經(jīng)足夠的高效?或者說(shuō)是否有其他的辦法來(lái)進(jìn)一步的提高性能。Disruptor 為我們提供了一個(gè)思路和實(shí)踐(這個(gè)思路不是 Disruptor 首創(chuàng),但是他們提供了一個(gè)好的完整實(shí)踐)

基本的循環(huán)數(shù)組實(shí)現(xiàn)

定義一個(gè)數(shù)組,長(zhǎng)度為2的次方冪(因?yàn)橛?jì)算機(jī)是二進(jìn)制的,所以2次方冪可以進(jìn)行并運(yùn)算來(lái)代替取模運(yùn)算)。設(shè)定一個(gè)數(shù)字標(biāo)志表示當(dāng)前的可用的位置(可以從0開始)。當(dāng)這個(gè)數(shù)字標(biāo)志不斷增長(zhǎng)到大于數(shù)組長(zhǎng)度時(shí)進(jìn)行與數(shù)組長(zhǎng)度的并運(yùn)算,得到的新數(shù)字依然在數(shù)組的長(zhǎng)度范圍內(nèi),就又可以插入。這樣就好像一直插入直到數(shù)組末尾又再次從頭開始,故而稱之為循環(huán)數(shù)組。

一般的循環(huán)數(shù)組有頭尾兩個(gè)標(biāo)志位。這點(diǎn)和隊(duì)列很像。頭標(biāo)志位表示下一個(gè)可以插入的位置,尾標(biāo)志位表示下一個(gè)可以讀取的位置。頭標(biāo)志位不能大于尾標(biāo)志位一個(gè)數(shù)組長(zhǎng)度(因?yàn)檫@樣就插入的位置和讀取的位置就重疊了會(huì)導(dǎo)致數(shù)據(jù)丟失),尾標(biāo)志位不能等于頭標(biāo)志位(因?yàn)檫@樣讀取的數(shù)據(jù)實(shí)際上是上一輪的舊數(shù)據(jù))

預(yù)先填充提高性能

我們知道在java中如果創(chuàng)造大量的對(duì)象使用后棄用,JVM 會(huì)在適當(dāng)?shù)臅r(shí)候進(jìn)行 GC 操作。大量的對(duì)象 GC 操作是很消耗時(shí)間的。所以如果能夠避免 GC 也可以提高性能,特別是在數(shù)據(jù)交互非常頻繁的時(shí)候。

在循環(huán)數(shù)組中,可以事先在數(shù)組中填充好數(shù)據(jù)。一旦有新數(shù)據(jù)的產(chǎn)生,要做的就是修改數(shù)組中某一位中的一些屬性值。這樣可以避免頻繁創(chuàng)建數(shù)據(jù)和棄用數(shù)據(jù)導(dǎo)致的 GC。這點(diǎn)比起隊(duì)列是要好的。

只保留一個(gè)標(biāo)志位

多線程在隊(duì)列也好,循環(huán)數(shù)組也好,必然存在對(duì)標(biāo)志位的競(jìng)爭(zhēng)。無(wú)論是使用鎖來(lái)避免競(jìng)爭(zhēng),還是使用 CAS 來(lái)進(jìn)行無(wú)鎖算法。只要爭(zhēng)用的情況存在,并且線程較多,都會(huì)出現(xiàn)對(duì)資源的不斷消耗。爭(zhēng)用的對(duì)象越多,爭(zhēng)用中消耗掉的資源也就越多。為了避免這樣的情況,減少爭(zhēng)用的資源就是一個(gè)手段。比如在循環(huán)數(shù)組中只保留一個(gè)標(biāo)志位,也就是下一個(gè)可以寫入數(shù)據(jù)位置的標(biāo)志位。而尾部標(biāo)志位則在各個(gè)消費(fèi)者線程中保存(具體的編程手法后續(xù)細(xì)講)。

循環(huán)數(shù)組在單線程中的使用

如果確定只有一個(gè)生產(chǎn)者,也就是說(shuō)只有一個(gè)寫線程。則在循環(huán)數(shù)組中的使用會(huì)更加簡(jiǎn)化。具體來(lái)說(shuō)單線程更新數(shù)組上的標(biāo)志位,那這種情況,標(biāo)志位就無(wú)需采用 CAS 寫的方式來(lái)確定下一個(gè)可寫入的位置,直接就是在單線程內(nèi)進(jìn)行普通的更新即可。

循環(huán)數(shù)組在多線程中的使用

如果存在多個(gè)生產(chǎn)者,則可寫入的標(biāo)志位需要用 CAS 算法來(lái)進(jìn)行爭(zhēng)奪,避免鎖的使用。多個(gè)線程通過(guò) CAS 得到唯一的不沖突的下一個(gè)可寫序號(hào)。由于需要獲得序號(hào)后才能進(jìn)行寫入,而寫入完成才可以讓消費(fèi)者線程進(jìn)行消費(fèi)。所以才獲得序號(hào)后,完成寫入前,必須有一種方式讓消費(fèi)者檢測(cè)是否完成。以避免消費(fèi)者拿到還未填入輸入的數(shù)組位。

為了達(dá)到這個(gè)目標(biāo),存在簡(jiǎn)單—效率低和復(fù)雜—效率高兩種方式。

簡(jiǎn)單但是可能效率低的方式

使用兩個(gè)標(biāo)志位。

  • + prePut:表示下一個(gè)可以供生產(chǎn)者放入的位置;
  • + put:表示最后一個(gè)生產(chǎn)者已經(jīng)放入的位置。

多個(gè)生產(chǎn)者通過(guò) CAS 獲得 prePut 的不同的值。在獲得的序號(hào)并且完成數(shù)據(jù)寫入后,將 put 的值以 CAS 方式遞增(比如獲得的序號(hào)是7,只有 put 是6的時(shí)候才允許設(shè)置成功),稱之為發(fā)布。這種方式存在一個(gè)缺點(diǎn),如果多個(gè)線程并發(fā)寫入,獲取 prePut 的值不會(huì)堵塞,假設(shè)其中一個(gè)生產(chǎn)者在寫入數(shù)據(jù)的時(shí)候稍慢,則其他的線程寫入完畢也無(wú)法完成發(fā)布。就會(huì)導(dǎo)致循環(huán)等待,浪費(fèi)了 CPU 性能。

復(fù)雜但是可能效率高的方式

在上面的方式中,主要的爭(zhēng)奪環(huán)節(jié)集中在多線程發(fā)布中,序號(hào)大的線程發(fā)布需要等到序號(hào)小的線程發(fā)布完成后才能發(fā)布。那我們的優(yōu)化的點(diǎn)也在這個(gè)地方。如果只有一個(gè)地方可以寫入完成信息,必然需要爭(zhēng)奪。為了避免爭(zhēng)奪,我們可以使用標(biāo)志數(shù)組(長(zhǎng)度和內(nèi)容數(shù)組相同,每一位表示相同下標(biāo)的內(nèi)容數(shù)組是否發(fā)布)來(lái)表示每一個(gè)位置是否寫入。這樣就可以避免發(fā)布的爭(zhēng)奪(大家的標(biāo)志位都不在一起了)。

但是又來(lái)帶來(lái)一個(gè)問(wèn)題,用什么數(shù)字來(lái)表示是否已經(jīng)發(fā)布完成?如果只是0和1,那么寫過(guò)1輪以后,標(biāo)志數(shù)組位上就都是1了。又無(wú)法區(qū)分。所以標(biāo)志數(shù)組上的數(shù)字應(yīng)該在循環(huán)數(shù)組的每一輪循環(huán)的值都不同。比如一開始都是-1,第一輪中是0的表示已發(fā)布,第二輪中是0表示沒(méi)發(fā)布,是1的表示已發(fā)布。下面的是發(fā)布的算法步驟:

  1. 將序號(hào)除以標(biāo)志數(shù)組長(zhǎng)度(因?yàn)殚L(zhǎng)度是2的次方冪,這一步可以通過(guò)右移來(lái)完成)得到填入值 x;
  2. 將序號(hào)和標(biāo)志數(shù)組長(zhǎng)度減一進(jìn)行并運(yùn)算得到填入位置 index;
  3. 將index位置寫入 x。

CPU緩存行填充技術(shù)

一般在軟件編程中,很少有工程師會(huì)關(guān)注一些硬件的信息。不過(guò)如果追求性能達(dá)到極致,那么對(duì)于一些硬件知識(shí)的了解就成了必要。這其中CPU 緩存的知識(shí)會(huì)神奇的提高我們的程序性能。

CPU緩存行

在編程上,網(wǎng)絡(luò)關(guān)于 CPU 緩存的知識(shí)介紹很多。這里簡(jiǎn)單說(shuō)下。在硬件中,CPU 存在著多級(jí)緩存的結(jié)果,越接近 CPU 的緩存容量越小,速度越快。每一個(gè)物理內(nèi)核都有自己的緩存體系。不同的?CPU?之間通過(guò)緩存嗅探協(xié)議來(lái)確定緩存中的數(shù)據(jù)是否已經(jīng)失效。如果失效了,CPU?會(huì)去內(nèi)存中讀取數(shù)據(jù),并且將最新的數(shù)據(jù)在特定指令的幫助下寫入到內(nèi)存中。

CPU?緩存是以行為單位進(jìn)行存取的。以前的?CPU?是32個(gè)字節(jié)一行,現(xiàn)在則是64個(gè)字節(jié)一行。因?yàn)檫@種行存取的方式,所以稱之為緩存行。如果一個(gè)對(duì)象中不同屬性在多線程中被頻繁更新,會(huì)導(dǎo)致一個(gè)問(wèn)題:由于在同一個(gè)緩存行中的不相關(guān)變量的更新導(dǎo)致整個(gè)緩存行失效。緩存行失效后?CPU?就只好到主存中重新讀取數(shù)據(jù)。這個(gè)問(wèn)題在并發(fā)隊(duì)列中特別明顯。為了修正這個(gè)問(wèn)題,JDK 7 中特意提供了 transferqueue 來(lái)解決這個(gè)問(wèn)題。

緩存行填充

既然問(wèn)題的發(fā)生是因?yàn)橥粋(gè)緩存行中有不相關(guān)的變量被更新導(dǎo)致緩存行需要的數(shù)據(jù)一起失效,那么解決的辦法就是讓這個(gè)頻繁被更新的變量獨(dú)占一個(gè)緩存行即可。也就是剩下的位置就用無(wú)關(guān)數(shù)據(jù)填充。這樣就保證了關(guān)鍵變量不會(huì)因?yàn)槠渌兞康母露。具體的填充方式,就是在一個(gè) Java 對(duì)象中設(shè)定無(wú)意義的變量,根據(jù)變量的長(zhǎng)度來(lái)計(jì)算需要的個(gè)數(shù)。以下是示例代碼:

//現(xiàn)在一般的cpu架構(gòu)都是64個(gè)字節(jié)的緩存行,針對(duì)這個(gè)情況,緩存行填充可以如下進(jìn)行
class LeftPad
{
    protected long p1, p2, p3, p4, p5, p6, p7;
}

class RealValue extends LeftPad
{
    protected volatile long point = -1; // 前后都有7個(gè)元素填充,可以保證該核心變量獨(dú)自在一個(gè)緩存行中
}

class RightPad extends RealValue
{
    protected long p9, p10, p11, p12, p13, p14, p15;
}

public class CpuCachePadingValue extends RightPad
{
}

多線程編程中減少對(duì)鎖的使用

Disruptor 整個(gè)框架的實(shí)現(xiàn)過(guò)程都在盡量的減少對(duì)鎖的使用。比如生產(chǎn)者消費(fèi)者中最容易出現(xiàn)爭(zhēng)奪的,其實(shí)就是其中的消息隊(duì)列。那么對(duì)于這個(gè)消息隊(duì)列,我們可以采用的優(yōu)化手段包括

  • 使用循環(huán)數(shù)組代替隊(duì)列,使用 CAS 算法來(lái)代替鎖爭(zhēng)奪;
  • 消費(fèi)者各自保存自己當(dāng)前已經(jīng)處理過(guò)的序號(hào),而不是將這個(gè)序號(hào)的信息在隊(duì)列中來(lái)存儲(chǔ),避免多線程爭(zhēng)用。

針對(duì)上面的第二點(diǎn)詳細(xì)展開說(shuō)一下。一般來(lái)說(shuō),隊(duì)列中信息的處理有兩種不同的形式,第一種是這個(gè)消息需要所有消費(fèi)者都處理完畢,才能認(rèn)為是被使用好了。第二種是爭(zhēng)奪到使用權(quán)的消費(fèi)者線程進(jìn)行消費(fèi),其他消費(fèi)者線程爭(zhēng)奪下一個(gè)。無(wú)論哪一種,都可以將消費(fèi)者已經(jīng)處理的序號(hào)保存在消費(fèi)者線程內(nèi)。而如果信息只允許被一個(gè)線程消費(fèi),可以在內(nèi)部使用 CAS 來(lái)爭(zhēng)奪。而生產(chǎn)者線程則需要持有消費(fèi)者的類的信息,好用來(lái)判斷所有消費(fèi)者中消費(fèi)的最小的序號(hào),以避免在數(shù)據(jù)寫入時(shí)覆蓋了某個(gè)消費(fèi)者尚未處理的數(shù)據(jù)信息。

指定消費(fèi)者不同的處理順序

Disruptor 可以讓不同的消費(fèi)者按照一定的順序進(jìn)行消息處理。比如一個(gè)消息,必須先經(jīng)過(guò)日志處理 A1 保存日志,數(shù)據(jù)轉(zhuǎn)換處理器 A2 清理才能最終被業(yè)務(wù)處理器 A3 進(jìn)行實(shí)際的業(yè)務(wù)處理。而 A1 和 A2 并沒(méi)有任何前后關(guān)系,但是 A3 必須等 A1 和 A2 都完成后才能進(jìn)行。那么在實(shí)際編碼時(shí),可以讓 A3 追蹤 A1 和 A2 的處理序號(hào)。所有的消費(fèi)者都在等待隊(duì)列中可用序號(hào)達(dá)到自己需要的序號(hào),一旦到達(dá),排位靠后的處理器就循環(huán)檢測(cè)排位靠前的處理器是否已經(jīng)將數(shù)據(jù)處理完畢,處理完畢之后自己開始對(duì)數(shù)據(jù)的處理。

總結(jié)

Disruptor 這個(gè)框架在整個(gè)的編碼過(guò)程中一直都在體現(xiàn)本地緩存數(shù)據(jù),使用 CAS 來(lái)代替鎖,盡可能無(wú)鎖甚至無(wú) CAS 這樣的一種編程思想。根據(jù)官網(wǎng)的說(shuō)明,這樣的編碼思想是在他們追求多線程以提高性能遇到失敗后(項(xiàng)目復(fù)雜性、可測(cè)試性、維護(hù)性等),回過(guò)頭思考在單線程下的性能可能性(單線程無(wú)鎖必然是性能最高的,但是吞吐量就有待商榷)。

標(biāo)簽: 代碼 網(wǎng)絡(luò)

版權(quán)申明:本站文章部分自網(wǎng)絡(luò),如有侵權(quán),請(qǐng)聯(lián)系:west999com@outlook.com
特別注意:本站所有轉(zhuǎn)載文章言論不代表本站觀點(diǎn)!
本站所提供的圖片等素材,版權(quán)歸原作者所有,如需使用,請(qǐng)與原作者聯(lián)系。

上一篇:Ubuntu下面MySQL的參數(shù)文件my.cnf淺析

下一篇:Java并發(fā)之Condition的實(shí)現(xiàn)分析