一、CPU Cache
存盤設備往往是速度越快價格越昂貴,速度越快價格越低廉,
在計算機中,CPU 的速度遠高于主存的速度,而主存的速度又遠高于磁盤的速度,為了解決不同存盤部件的速度不對等問題,讓高速設備充分發揮性能,引入了多級快取機制,
為了解決記憶體和 CPU 的速度不匹配問題,相繼引入了 L1 Cache、L2 Cache、L3 Cache,數字越小,容量越小,速度越快,位置越接近 CPU,

現在的 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 加載到了快取中,
這就是快取行共享,聽起來不錯,但是一旦牽扯到了寫入操作就不妙了,
假設 Core-1 想要更新 arr[7] 的值,根據 CPU 的 MESI 協議,那么它所屬的快取行就會被標記為失效,因為它需要告訴其他的 Core,這個 arr[7] 的值已經被更新了,快取已經不再準確了,你必須得重新去記憶體拉取,但是由于快取的最小單元是快取行,因此只能把 arr[7] 所在的一整行給標識為失效,
此時 Core-2 就會很郁悶了,剛付訓能夠從快取中讀取到物件 bar,現在再讀取卻被告知快取行失效,必須得去記憶體重新拉取,延緩了 Core-2 的執行效率,
這就是快取偽共享問題,兩個毫無關聯的執行緒執行,一個執行緒卻因為另一個執行緒的操作,導致快取失效,這兩個執行緒其實就是對同一快取行產生了競爭,降低了并發性,
三、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 = https://www.cnblogs.com/javastack/archive/2021/02/03/Sequence.INITIAL_VALUE;
protected long p1, p2, p3, p4, p5, p6, p7;
...
}
abstract class RingBufferFields extends RingBufferPad
{
...
}
abstract class RingBufferPad {
protected long p1, p2, p3, p4, p5, p6, p7;
}
四、@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;
...
}
五、速度測驗
將 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開發手冊(嵩山版)》最新發布,速速下載!
覺得不錯,別忘了隨手點贊+轉發哦!
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/256201.html
標籤:其他
上一篇:instanceof和型別轉換
