存儲設備每每是速度越快價格越昂貴,速度越快價格越低廉。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的整數冪個字節,最多見的緩存行大小是 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 的執行效率。
這就是緩存僞共享問題,兩個毫無關聯的線程執行,一個線程卻由於另外一個線程的操做,致使緩存失效。這兩個線程其實就是對同一緩存行產生了競爭,下降了併發性。
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; }
在 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; ... }
將 volatile long value 封裝爲對象,四線程並行,每一個線程循環 1 億次,對 value 進行更新操做,測試緩存行對速度的影響。
做者: Jitwxs
連接: https://jitwxs.cn/13836b16.html
近期熱文推薦:
1.Java 15 正式發佈, 14 個新特性,刷新你的認知!!
2.終於靠開源項目弄到 IntelliJ IDEA 激活碼了,真香!
3.我用 Java 8 寫了一段邏輯,同事直呼看不懂,你試試看。。
以爲不錯,別忘了隨手點贊+轉發哦!