您好,登錄后才能下訂單哦!
這篇文章將為大家詳細(xì)講解有關(guān)Disruptor的共享與緩存是怎樣的,文章內(nèi)容質(zhì)量較高,因此小編分享給大家做個(gè)參考,希望大家閱讀完這篇文章后對(duì)相關(guān)知識(shí)有一定的了解。
下圖是計(jì)算的基本結(jié)構(gòu)。L1、L2、L3分別表示一級(jí)緩存、二級(jí)緩存、三級(jí)緩存,越靠近CPU的緩存,速度越快,容量也越小。所以L1緩存很小但很快,并且緊靠著在使用它的CPU內(nèi)核;L2大一些,也慢一些,并且仍然只能被一個(gè)單獨(dú)的CPU核使用;L3更大、更慢,并且被單個(gè)插槽上的所有CPU核共享;最后是主存,由全部插槽上的所有CPU核共享。
圖3 計(jì)算機(jī)CPU與緩存示意圖
當(dāng)CPU執(zhí)行運(yùn)算的時(shí)候,它先去L1查找所需的數(shù)據(jù)、再去L2、然后是L3,如果最后這些緩存中都沒有,所需的數(shù)據(jù)就要去主內(nèi)存拿。走得越遠(yuǎn),運(yùn)算耗費(fèi)的時(shí)間就越長(zhǎng)。所以如果你在做一些很頻繁的事,你要盡量確保數(shù)據(jù)在L1緩存中。
另外,線程之間共享一份數(shù)據(jù)的時(shí)候,需要一個(gè)線程把數(shù)據(jù)寫回主存,而另一個(gè)線程訪問主存中相應(yīng)的數(shù)據(jù)。
下面是從CPU訪問不同層級(jí)數(shù)據(jù)的時(shí)間概念:
從CPU到 | 大約需要的CPU周期 | 大約需要的時(shí)間 |
---|---|---|
主存 | 約60-80ns | |
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讀取主存中的數(shù)據(jù)會(huì)比從L1中讀取慢了近2個(gè)數(shù)量級(jí)。
Cache是由很多個(gè)cache line組成的。每個(gè)cache line通常是64字節(jié),并且它有效地引用主內(nèi)存中的一塊兒地址。一個(gè)Java的long類型變量是8字節(jié),因此在一個(gè)緩存行中可以存8個(gè)long類型的變量。
CPU每次從主存中拉取數(shù)據(jù)時(shí),會(huì)把相鄰的數(shù)據(jù)也存入同一個(gè)cache line。
在訪問一個(gè)long數(shù)組的時(shí)候,如果數(shù)組中的一個(gè)值被加載到緩存中,它會(huì)自動(dòng)加載另外7個(gè)。因此你能非??斓谋闅v這個(gè)數(shù)組。事實(shí)上,你可以非??焖俚谋闅v在連續(xù)內(nèi)存塊中分配的任意數(shù)據(jù)結(jié)構(gòu)。
下面的例子是測(cè)試?yán)胏ache line的特性和不利用cache line的特性的效果對(duì)比。
package com.meituan.FalseSharing; /** * @author gongming * @description * @date 16/6/4 */ public class CacheLineEffect { //考慮一般緩存行大小是64字節(jié),一個(gè) long 類型占8字節(jié) static long[][] arr; public static void main(String[] args) { arr = new long[1024 * 1024][]; for (int i = 0; i < 1024 * 1024; i++) { arr[i] = new long[8]; for (int j = 0; j < 8; j++) { arr[i][j] = 0L; } } long sum = 0L; long marked = System.currentTimeMillis(); for (int i = 0; i < 1024 * 1024; i+=1) { for(int j =0; j< 8;j++){ sum = arr[i][j]; } } System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms"); marked = System.currentTimeMillis(); for (int i = 0; i < 8; i+=1) { for(int j =0; j< 1024 * 1024;j++){ sum = arr[j][i]; } } System.out.println("Loop times:" + (System.currentTimeMillis() - marked) + "ms"); } }
在2G Hz、2核、8G內(nèi)存的運(yùn)行環(huán)境中測(cè)試,速度差一倍。
結(jié)果: Loop times:30ms Loop times:65ms
ArrayBlockingQueue有三個(gè)成員變量: - takeIndex:需要被取走的元素下標(biāo) - putIndex:可被元素插入的位置的下標(biāo) - count:隊(duì)列中元素的數(shù)量
這三個(gè)變量很容易放到一個(gè)緩存行中,但是之間修改沒有太多的關(guān)聯(lián)。所以每次修改,都會(huì)使之前緩存的數(shù)據(jù)失效,從而不能完全達(dá)到共享的效果。
圖4 ArrayBlockingQueue偽共享示意圖
如上圖所示,當(dāng)生產(chǎn)者線程put一個(gè)元素到ArrayBlockingQueue時(shí),putIndex會(huì)修改,從而導(dǎo)致消費(fèi)者線程的緩存中的緩存行無效,需要從主存中重新讀取。
這種無法充分使用緩存行特性的現(xiàn)象,稱為偽共享。
對(duì)于偽共享,一般的解決方案是,增大數(shù)組元素的間隔使得由不同線程存取的元素位于不同的緩存行上,以空間換時(shí)間。
package com.meituan.FalseSharing; public class FalseSharing implements Runnable{ public final static long ITERATIONS = 500L * 1000L * 100L; private int arrayIndex = 0; private static ValuePadding[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { for(int i=1;i<10;i++){ System.gc(); final long start = System.currentTimeMillis(); runTest(i); System.out.println("Thread num "+i+" duration = " + (System.currentTimeMillis() - start)); } } private static void runTest(int NUM_THREADS) throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; longs = new ValuePadding[NUM_THREADS]; for (int i = 0; i < longs.length; i++) { longs[i] = new ValuePadding(); } for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = 0L; } } public final static class ValuePadding { protected long p1, p2, p3, p4, p5, p6, p7; protected volatile long value = 0L; protected long p9, p10, p11, p12, p13, p14; protected long p15; } public final static class ValueNoPadding { // protected long p1, p2, p3, p4, p5, p6, p7; protected volatile long value = 0L; // protected long p9, p10, p11, p12, p13, p14, p15; } }
在2G Hz,2核,8G內(nèi)存, jdk 1.7.0_45 的運(yùn)行環(huán)境下,使用了共享機(jī)制比沒有使用共享機(jī)制,速度快了4倍左右。
結(jié)果: Thread num 1 duration = 447 Thread num 2 duration = 463 Thread num 3 duration = 454 Thread num 4 duration = 464 Thread num 5 duration = 561 Thread num 6 duration = 606 Thread num 7 duration = 684 Thread num 8 duration = 870 Thread num 9 duration = 823
把代碼中ValuePadding都替換為ValueNoPadding后的結(jié)果: Thread num 1 duration = 446 Thread num 2 duration = 2549 Thread num 3 duration = 2898 Thread num 4 duration = 3931 Thread num 5 duration = 4716 Thread num 6 duration = 5424 Thread num 7 duration = 4868 Thread num 8 duration = 4595 Thread num 9 duration = 4540
備注:在jdk1.8中,有專門的注解@Contended來避免偽共享,更優(yōu)雅地解決問題。
關(guān)于Disruptor的共享與緩存是怎樣的就分享到這里了,希望以上內(nèi)容可以對(duì)大家有一定的幫助,可以學(xué)到更多知識(shí)。如果覺得文章不錯(cuò),可以把它分享出去讓更多的人看到。
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。