Java 緩存行和僞共享
最近看了一本書,因爲以前不太瞭解底層原理,所以這塊比較薄弱,所以通過本文做下記錄和總結。
1.緩存行和僞共享的概念
1.1 概念闡述
在計算機系統中,內存是以【緩存行】爲單位存儲的,一個緩存行存儲的字節是2的倍數。不同機器上,緩存行大小也不一樣,通常來說爲64字節。
僞共享是指:在多個線程同時讀寫同一個【緩存行】上的不同數據時,儘管這些變量之間沒有任何關係,但是在多線程之間仍然需要同步,從而導致性能下降。在多核處理器中,僞共享是影響性能的主要因素之一,通常稱之爲:“性能殺手”。
1.2 圖解
可能上述文字闡述的不是很直白,可以通過這個圖理解下:
線程a 在CPU1上讀寫變量X, 同時線程b上讀寫變量Y,但是巧合的是變量 X , Y 在同一個緩存行上,那邊線程a,b就要互相競爭獲取該緩存行的讀寫權限,纔可以進行讀寫。
假如 線程a在內核1上 獲取了緩存行的讀寫權限,進行了操作,就會導致其他內核中的x變量和y變量同時失效,那麼線程b 就必須刷新它的緩存後才能在內核2上獲取緩存行的讀寫權限。 這就導致了這個緩存行在不同的線程之間多次通過 L3緩存進行交換最新複製的數據,極大的影響了CPU性能,如果CPU不在同一個槽上,性能更糟糕。
2、 JVM內存模型——Java對象結構
2.1 Java對象頭
Mark Word記錄了對象和鎖有關的信息,當這個對象被synchronized關鍵字當成同步鎖時,圍繞這個鎖的一系列操作都和Mark Word有關。
所有java 對象都有8字節的對象頭,前4個字節用於保存對象的哈希碼(前3個字節)和對象鎖狀態(後一個字節),假如對象處於上鎖狀態,這4個字節都會被拿到對象外,並用指針進行連接。
剩下的4個字節用來存儲對象所屬類的引用。
另外,對於數組而言,還有一個保存數組大小的變量,也是4個字節。
2.2 實例對象
對象的實例就是我們在Java對象中看到的屬性及其值。
2.3 對齊填充
JVM 要求Java的對象佔用的內存大小必須是8bit的整數倍,所以後面有幾個字節用於把對象的大小補齊值8bit 倍數。
每個java 對象都會對齊到8字節的倍數,不夠會進行填充,爲了保證效率,Java 編譯器通過字段類型進行了排序:
順序 | 類型 | 字節數量(字節) |
---|---|---|
1 | double | 8 |
2 | long | 8 |
3 | int | 4 |
4 | float | 4 |
5 | Short | 2 |
6 | char | 2 |
7 | boolean | 1 |
8 | byte | 1 |
9 | 對象引用 | 4或者8 |
10 | 子類字段 | 重新排序 |
3、Demo驗證
這裏採用書本的樣例,可以直接運行,可以調整線程數量 來觀摩對性能的影響。
/**
* @author zhanghuilong
* @desc Java中緩存行,僞共享的理解
* @since 2019/06/12
*/
public class FalseSharingDemo {
// 測試使用線程數
private final static int NUM_THREADS= 4;
// 測試次數
private final static int NUM_TEST_TIMES= 10;
// 無填充 無緩存行對齊的對象類 普通熱變量
static class PlainHotVariable {
// 1個long 類型變量 佔用內存 1*8 = 8 字節,
public volatile long value = 0L;
}
// 有填充 有緩存行對其的對象類
static class AlignHotVariable extends PlainHotVariable{
// 用於填充,6個long 類型的變量 ,總佔用內存爲 6*8 = 48 字節
// 加上繼承父類的一個變量value,那麼總共該對象 佔用內存爲:8字節對象頭 + 8字節父類變量+ 6*8字節填充變量 = 64字節,
// 正好滿足一個對象全部在一個緩存行中,消除了僞競爭問題
public long p1,p2,p3,p4,p5,p6;
}
// 競爭者
static final class CompetitorThread extends Thread {
//迭代次數
private final static long ITERATIONS = 500L * 1000L * 1000L;
private PlainHotVariable plainHotVariable;
public CompetitorThread(final PlainHotVariable plainHotVariable) {
this.plainHotVariable = plainHotVariable;
}
@Override
public void run() {
for (int i=0;i<ITERATIONS;i++){
plainHotVariable.value = i;
}
}
}
public static long runOneTest(PlainHotVariable[] plainHotVariables) throws Exception{
//開啓多個線程進行測試
CompetitorThread[] competitorThreads = new CompetitorThread[plainHotVariables.length];
for (int i = 0; i < plainHotVariables.length; i++) {
competitorThreads[i] = new CompetitorThread(plainHotVariables[i]);
}
final long start = System.nanoTime();
for (Thread thread : competitorThreads){
thread.start();
}
for (Thread thread : competitorThreads){
thread.join();
}
return System.nanoTime() - start;
}
public static boolean runOneCompare(int threadNum)throws Exception{
PlainHotVariable[] plainHotVariables = new PlainHotVariable[threadNum];
for (int i = 0; i < threadNum; i++) {
plainHotVariables[i] = new PlainHotVariable();
}
// 進行無填充 無緩存行對齊測試
long t1 = runOneTest(plainHotVariables);
AlignHotVariable[] alignHotVariables = new AlignHotVariable[threadNum];
for (int i = 0; i < NUM_THREADS; i++) {
alignHotVariables[i] = new AlignHotVariable();
}
// 進行填充 有緩存行對齊的測試
long t2 = runOneTest(alignHotVariables);
System.out.println("無填充 無緩存行對齊Plain:"+ t1);
System.out.println("有填充 有緩存行對齊Plain:"+ t2);
// 返回結果對比
return t1 > t2;
}
public static void runOneSuit(int threadsNum, int testNum) throws Exception{
int expectedCount = 0;
for (int i = 0; i < testNum; i++) {
if (runOneCompare(threadsNum)){
expectedCount++;
}
}
//計算有填充 有緩存對其的測試場景下響應時間更短的概率
System.out.println("Radio (Plain < Align ):" + expectedCount * 100D / testNum +"%");
}
public static void main(String[] args) throws Exception{
runOneSuit(NUM_THREADS, NUM_TEST_TIMES);
}
}
測試結果:
無填充 無緩存行對齊Plain:17638579323
有填充 有緩存行對齊Plain:7270967980
無填充 無緩存行對齊Plain:20392022924
有填充 有緩存行對齊Plain:7521045611
無填充 無緩存行對齊Plain:12537116135
有填充 有緩存行對齊Plain:7369732614
無填充 無緩存行對齊Plain:12498607757
有填充 有緩存行對齊Plain:7419091587
無填充 無緩存行對齊Plain:12198918237
有填充 有緩存行對齊Plain:7422019467
無填充 無緩存行對齊Plain:11988160497
有填充 有緩存行對齊Plain:7532538573
無填充 無緩存行對齊Plain:12223106752
有填充 有緩存行對齊Plain:7340185594
無填充 無緩存行對齊Plain:16388926182
有填充 有緩存行對齊Plain:7300794683
無填充 無緩存行對齊Plain:12338497888
有填充 有緩存行對齊Plain:7804763605
無填充 無緩存行對齊Plain:12393832355
有填充 有緩存行對齊Plain:7360913752
Radio (Plain < Align ):100.0%
4、僞共享 解決方案
此條信息也是碰巧在這篇文章中看到的:https://blog.csdn.net/hanmindaxiongdi/article/details/81159314
Java8中已經提供了官方的解決方案,Java8中新增了一個註解:@sun.misc.Contended。加上這個註解的類會自動補齊緩存行,需要注意的是此註解默認是無效的,需要在jvm啓動時設置-XX:-RestrictContended纔會生效。
對應到代碼裏在AlignHotVariable 類上加上註解,並且去掉填充6個對象,運行時在jvm裏開啓響應參數即可:
// 有填充 有緩存行對其的對象類
@sun.misc.Contended
static class AlignHotVariable extends PlainHotVariable{
// 用於填充,6個long 類型的變量 ,總佔用內存爲 6*8 = 48 字節
// 加上繼承父類的一個變量value,那麼總共該對象 佔用內存爲:8字節對象頭 + 8字節父類變量+ 6*8字節填充變量 = 64字節,
// 正好滿足一個對象全部在一個緩存行中,消除了僞競爭問題
// public long p1,p2,p3,p4,p5,p6;
}
以下是測試結果和 有填充項的基本一致:
無填充 無緩存行對齊Plain:19445985843
有填充 有緩存行對齊Plain:6996708098
無填充 無緩存行對齊Plain:12654238078
有填充 有緩存行對齊Plain:8071548517
無填充 無緩存行對齊Plain:19983041578
有填充 有緩存行對齊Plain:7074076269
無填充 無緩存行對齊Plain:17710330823
有填充 有緩存行對齊Plain:7030274857
無填充 無緩存行對齊Plain:20281301886
有填充 有緩存行對齊Plain:7048077452
無填充 無緩存行對齊Plain:19447443573
有填充 有緩存行對齊Plain:7066423588
無填充 無緩存行對齊Plain:20154352370
有填充 有緩存行對齊Plain:7052431719
無填充 無緩存行對齊Plain:18240658823
有填充 有緩存行對齊Plain:6996498595
無填充 無緩存行對齊Plain:19922299237
有填充 有緩存行對齊Plain:7094801775
無填充 無緩存行對齊Plain:17513743086
有填充 有緩存行對齊Plain:7002876176
Radio (Plain < Align ):100.0%