Java 緩存行 和 僞共享

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%
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章