自從項目用了 Disruptor,性能提高了 2.5 倍!

1、CPU Cache

存儲設備每每是速度越快價格越昂貴,速度越快價格越低廉。html

在計算機中,CPU 的速度遠高於主存的速度,而主存的速度又遠高於磁盤的速度。爲了解決不一樣存儲部件的速度不對等問題,讓高速設備充分發揮性能,引入了多級緩存機制。java

爲了解決內存和 CPU 的速度不匹配問題,相繼引入了 L1 Cache、L2 Cache、L3 Cache,數字越小,容量越小,速度越快,位置越接近 CPU。spring

如今的 CPU 都是由多個處理器,每一個處理器由多個核心構成。 一個處理器對應一個物理插槽,不一樣的處理器間經過 QPI 總線相連。一個處理器間的多核共享 L3 Cache。一個核包含寄存器、L1 Cache、L2 Cache,下圖是Intel Sandy Bridge CPU架構:數組

2、緩存行與僞共享

緩存中的數據並非獨立的進行存儲的,它的最小存儲單位是緩存行,緩存行的大小是2的整數冪個字節,最多見的緩存行大小是 64 字節。CPU 爲了執行的高效,會在讀取某個對象時,從內存上加載 64 的整數倍的長度,來補齊緩存行。緩存

以 Java 的 long 類型爲例,它是 8 個字節,假設咱們存在一個長度爲 8 的 long 數組 arr,那麼CPU 在讀取 arr[0] 時,首先查詢緩存,緩存沒有命中,緩存就會去內存中加載。因爲緩存的最小存儲單位是緩存行,64 字節,且數組的內存地址是連續的,則將 arr[0] 到 arr[7] 加載到緩存中。後續 CPU 查詢 arr[6] 時候也能夠直接命中緩存。多線程

如今假設多線程狀況下,線程 A 的執行者 CPU Core-1 讀取 arr[1],首先查詢緩存,緩存沒有命中,緩存就會去內存中加載。架構

從內存中讀取 arr[1] 起的連續的 64 個字節地址到緩存中,組成緩存行。因爲從arr[1] 起,arr 的長度不足夠 64 個字節,只夠 56 個字節。假設最後 8 個字節內存地址上存儲的是對象 bar,那麼對象 bar 也會被一塊兒加載到緩存行中。併發

如今有另外一個線程 B,線程 B 的執行者 CPU Core-2 去讀取對象 bar,首先查詢緩存,發現命中了,由於 Core-1 在讀取 arr 數組的時候也順帶着把 bar 加載到了緩存中。intellij-idea

這就是緩存行共享,聽起來不錯,可是一旦牽扯到了寫入操做就不妙了。dom

假設 Core-1 想要更新 arr[7] 的值,根據 CPU 的 MESI 協議,那麼它所屬的緩存行就會被標記爲失效。由於它須要告訴其餘的 Core,這個 arr[7] 的值已經被更新了,緩存已經再也不準確了,你必須得從新去內存拉取。可是因爲緩存的最小單元是緩存行,所以只能把 arr[7] 所在的一整行給標識爲失效。

此時 Core-2 就會很鬱悶了,剛剛還可以從緩存中讀取到對象 bar,如今再讀取卻被告知緩存行失效,必須得去內存從新拉取,延緩了 Core-2 的執行效率。

這就是緩存僞共享問題,兩個毫無關聯的線程執行,一個線程卻由於另外一個線程的操做,致使緩存失效。這兩個線程其實就是對同一緩存行產生了競爭,下降了併發性。

3、Disruptor 緩存行填充

Disruptor 爲了解決僞共享問題,使用的方法是緩存行填充。這是一種以空間換時間的策略,主要思想就是經過往對象中填充無心義的變量,來保證整個對象獨佔緩存行。

舉個例子,以 Disruptor 中的 Sequence 爲例,在 volatile long value 的先後各放置了 7 個 long 型變量,確保 value 獨佔一個緩存行。

public class Sequence extends RhsPadding {
    private static final long VALUE_OFFSET;
    
    static {
        VALUE_OFFSET = UNSAFE.objectFieldOffset(Value.class.getDeclaredField("value"));
        ...
    }
    ...
}

class RhsPadding extends Value {
    protected long p9, p10, p11, p12, p13, p14, p15;
}

class Value extends LhsPadding {
    protected volatile long value;
}

class LhsPadding {
    protected long p1, p2, p3, p4, p5, p6, p7;
}

以下圖所示,其中 V 就是 Value 類的 value,P 爲 value 先後填充的無心義 long 型變量,U 爲其它無關的變量。不論什麼狀況下,都能保證 V 不和其餘無關的變量處於同一緩存行中,這樣 V 就不會被其餘無關的變量所影響。

這裏的 V 也不限定爲 long 類型,其實只要對象的大小大於等於8個字節,經過先後各填充 7 個 long 型變量,就必定可以保證獨佔緩存行。

此處以 Disruptor 的 RingBuffer 爲例,最左邊的 7 個 long 型變量被定義在頂級父類 RingBufferPad 中,最右邊的 7 個 long 型變量被定義在 RingBuffer 的最後一行變量定義中,這樣全部的須要獨佔的變量都被左右 long 型給包圍,確保會獨佔緩存行。

public final class RingBuffer<E> extends RingBufferFields<E> implements Cursored, EventSequencer<E>, EventSink<E> {
    public static final long INITIAL_CURSOR_VALUE = Sequence.INITIAL_VALUE;
    protected long p1, p2, p3, p4, p5, p6, p7;
    ...
}

abstract class RingBufferFields<E> extends RingBufferPad
{
    ...
}

abstract class RingBufferPad {
    protected long p1, p2, p3, p4, p5, p6, p7;
}

4、@Contended

在 JDK 1.8 中,提供了 @sun.misc.Contended 註解,使用該註解就可讓變量獨佔緩存行,再也不須要手動填充了。 另外,關注公衆號Java技術棧,在後臺回覆:Java,能夠獲取我整理的 Java 1.8 系列教程,很是齊全。

注意,JVM 須要添加參數 -XX:-RestrictContended 才能開啓此功能。

若是該註解被定義在了類上,表示該類的每一個變量都會獨佔緩存行;若是被定義在了變量上,經過指定 groupName,相同的 groupName 會獨佔同一緩存行。

// 類前加上表明整個類的每一個變量都會在單獨的cache line中
@sun.misc.Contended
public class ContendedData {
    int value;
    long modifyTime;
    boolean flag;
    long createTime;
    char key;
}

// 同一 groupName 在同一緩存行
public class ContendedGroupData {
    @sun.misc.Contended("group1")
    int value;
    @sun.misc.Contended("group1")
    long modifyTime;
    @sun.misc.Contended("group2")
    boolean flag;
    @sun.misc.Contended("group3")
    long createTime;
    @sun.misc.Contended("group3")
    char key;
}

@Contended 在 JDK 源碼中已經有所應用,以 Thread 類爲例,爲了保證多線程狀況下隨機數的操做不會產生僞共享,相關的變量被設置爲同一 groupName。

public class Thread implements Runnable {
    ...
    // The following three initially uninitialized fields are exclusively
    // managed by class java.util.concurrent.ThreadLocalRandom. These
    // fields are used to build the high-performance PRNGs in the
    // concurrent code, and we can not risk accidental false sharing.
    // Hence, the fields are isolated with @Contended.

    /** The current seed for a ThreadLocalRandom */
    @sun.misc.Contended("tlr")
    long threadLocalRandomSeed;
    
    /** Probe hash value; nonzero if threadLocalRandomSeed initialized */
    @sun.misc.Contended("tlr")
    int threadLocalRandomProbe;
    
    /** Secondary seed isolated from public ThreadLocalRandom sequence */
    @sun.misc.Contended("tlr")
    int threadLocalRandomSecondarySeed;
    
    ...
}

5、速度測試

將 volatile long value 封裝爲對象,四線程並行,每一個線程循環 1 億次,對 value 進行更新操做,測試緩存行對速度的影響。

  • CPU:AMD 3600 3.6 GHz
  • Memory:16 GB

做者: Jitwxs
連接: https://jitwxs.cn/13836b16.html

近期熱文推薦:

1.Java 15 正式發佈, 14 個新特性,刷新你的認知!!

2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!

3.我用 Java 8 寫了一段邏輯,同事直呼看不懂,你試試看。。

4.吊打 Tomcat ,Undertow 性能很炸!!

5.《Java開發手冊(嵩山版)》最新發布,速速下載!

以爲不錯,別忘了隨手點贊+轉發哦!

相關文章
相關標籤/搜索