Java8使用@sun.misc.Contended避免偽共享

博客鏈接:http://www.ideabuffer.cn/2017/05/12/Java8使用-sun-misc-Contended避免偽共享/


什么是偽共享

緩存系統(tǒng)中是以緩存行(cache line)為單位存儲的乞封。緩存行是2的整數(shù)冪個連續(xù)字節(jié)惧浴,一般為32-256個字節(jié)。最常見的緩存行大小是64個字節(jié)脂矫。當多線程修改互相獨立的變量時恩沛,如果這些變量共享同一個緩存行在扰,就會無意中影響彼此的性能,這就是偽共享雷客。

緩存行上的寫競爭是運行在SMP系統(tǒng)中并行線程實現(xiàn)可伸縮性最重要的限制因素芒珠。有人將偽共享描述成無聲的性能殺手,因為從代碼中很難看清楚是否會出現(xiàn)偽共享搅裙。

為了讓可伸縮性與線程數(shù)呈線性關系皱卓,就必須確保不會有兩個線程往同一個變量或緩存行中寫。兩個線程寫同一個變量可以在代碼中發(fā)現(xiàn)部逮。為了確定互相獨立的變量是否共享了同一個緩存行娜汁,就需要了解緩存行和對象的內(nèi)存布局,有關緩存行和對象內(nèi)存布局可以參考我的另外兩篇文章理解CPU CacheJava對象內(nèi)存布局兄朋。

下面的圖說明了偽共享的問題:

假設在核心1上運行的線程想更新變量X掐禁,同時核心2上的線程想要更新變量Y。不幸的是,這兩個變量在同一個緩存行中傅事。每個線程都要去競爭緩存行的所有權來更新變量缕允。如果核心1獲得了所有權,緩存子系統(tǒng)將會使核心2中對應的緩存行失效蹭越。當核心2獲得了所有權然后執(zhí)行更新操作障本,核心1就要使自己對應的緩存行失效。這會來來回回的經(jīng)過L3緩存响鹃,大大影響了性能驾霜。如果互相競爭的核心位于不同的插槽,就要額外橫跨插槽連接买置,問題可能更加嚴重寄悯。

避免偽共享

假設有一個類中,只有一個long類型的變量:

public final static class VolatileLong {
    public volatile long value = 0L;
}

這時定義一個VolatileLong類型的數(shù)組堕义,然后讓多個線程同時并發(fā)訪問這個數(shù)組,這時可以想到脆栋,在多個線程同時處理數(shù)據(jù)時倦卖,數(shù)組中的多個VolatileLong對象可能存在同一個緩存行中,通過上文可知椿争,這種情況就是偽共享怕膛。

怎么樣避免呢?在Java 7之前秦踪,可以在屬性的前后進行padding褐捻,例如:

public final static class VolatileLong {
    volatile long p0, p1, p2, p3, p4, p5, p6;
    public volatile long value = 0;
    volatile long q0, q1, q2, q3, q4, q5, q6;
}

通過Java對象內(nèi)存布局文章中結(jié)尾對paddign的分析可知,由于都是long類型的變量椅邓,這里就是按照聲明的順序分配內(nèi)存柠逞,那么這可以保證在同一個緩存行中只有一個VolatileLong對象。

__ 這里有一個問題:據(jù)說Java7優(yōu)化了無用字段景馁,會使這種形式的補位無效板壮,但經(jīng)過測試,無論是在JDK 1.7 還是 JDK 1.8中合住,這種形式都是有效的绰精。網(wǎng)上有關偽共享的文章基本都是來自Martin的兩篇博客,這種優(yōu)化方式也是在他的博客中提到的透葛。但國內(nèi)的文章貌似根本就沒有驗證過而直接引用了此觀點笨使,這也確實迷惑了一大批同學!__

在Java 8中僚害,提供了@sun.misc.Contended注解來避免偽共享硫椰,原理是在使用此注解的對象或字段的前后各增加128字節(jié)大小的padding,使用2倍于大多數(shù)硬件緩存行的大小來避免相鄰扇區(qū)預取導致的偽共享沖突。具體可以參考http://mail.openjdk.java.net/pipermail/hotspot-dev/2012-November/007309.html最爬。

下面用代碼來看一下加padding和不加的效果:

運行環(huán)境:JDK 1.8涉馁,macOS 10.12.4,2.2 GHz Intel Core i7爱致,四核-八線程

public class FalseSharing implements Runnable {

    public final static int NUM_THREADS = 4; // change
    public final static long ITERATIONS = 500L * 1000L * 1000L;
    private final int arrayIndex;

    private static VolatileLong[] longs = new VolatileLong[NUM_THREADS];
//    private static VolatileLong2[] longs = new VolatileLong2[NUM_THREADS];
//    private static VolatileLong3[] longs = new VolatileLong3[NUM_THREADS];

    static {
        for (int i = 0; i < longs.length; i++) {
            longs[i] = new VolatileLong();
        }
    }

    public FalseSharing(final int arrayIndex) {
        this.arrayIndex = arrayIndex;
    }

    public static void main(final String[] args) throws Exception {
        long start = System.nanoTime();
        runTest();
        System.out.println("duration = " + (System.nanoTime() - start));
    }

    private static void runTest() throws InterruptedException {
        Thread[] threads = new Thread[NUM_THREADS];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new FalseSharing(i));
        }

        for (Thread t : threads) {
            t.start();
        }

        for (Thread t : threads) {
            t.join();
        }
    }

    public void run() {
        long i = ITERATIONS + 1;
        while (0 != --i) {
            longs[arrayIndex].value = i;
        }
    }

    public final static class VolatileLong {
        public volatile long value = 0L;
    }

    // long padding避免false sharing
    // 按理說jdk7以后long padding應該被優(yōu)化掉了烤送,但是從測試結(jié)果看padding仍然起作用
    public final static class VolatileLong2 {
        volatile long p0, p1, p2, p3, p4, p5, p6;
        public volatile long value = 0L;
        volatile long q0, q1, q2, q3, q4, q5, q6;
    }

    /**
     * jdk8新特性,Contended注解避免false sharing
     * Restricted on user classpath
     * Unlock: -XX:-RestrictContended
     */
    @sun.misc.Contended
    public final static class VolatileLong3 {
        public volatile long value = 0L;
    }
}

VolatileLong對象只有一個long類型的字段糠悯,VolatileLong2加了padding帮坚,下面分別執(zhí)行看下時間:

duration = 57293259577
duration = 4679059000

沒加padding時用了大概57秒,加padding后用時大概4.6秒互艾,可見加padding后有效果了试和。

在Java8中提供了@sun.misc.Contended來避免偽共享,例如這里的VolatileLong3纫普,在運行時需要設置JVM啟動參數(shù)-XX:-RestrictContended阅悍,運行一下結(jié)果如下:

duration = 4756952426

結(jié)果與加padding的時間差不多。

下面看一下VolatileLong對象在運行時的內(nèi)存大凶蚣凇(參考Java對象內(nèi)存布局):

再來看下VolatileLong2對象在運行時的內(nèi)存大薪谑印:

因為多了14個long類型的變量,所以24+8*14=136字節(jié)假栓。

下面再來看下使用@sun.misc.Contended注解后的對象內(nèi)存大醒靶小:

在堆內(nèi)存中并沒有看到對變量進行padding,大小與VolatileLong對象是一樣的匾荆。

這就奇怪了拌蜘,看起來與VolatileLong沒什么不一樣,但看一下內(nèi)存的地址牙丽,用十六進制算一下简卧,兩個VolatileLong對象地址相差24字節(jié),而兩個VolatileLong3對象地址相差280字節(jié)烤芦。這就是前面提到的@sun.misc.Contended注解會在對象或字段的前后各增加128字節(jié)大小的padding贞滨,那么padding的大小就是256字節(jié),再加上對象的大小24字節(jié)拍棕,結(jié)果就是280字節(jié)晓铆,所以確實是增加padding了。

八線程運行比四線程運行還快绰播?

根據(jù)上面的代碼骄噪,把NUM_THREADS改為8,測試看下結(jié)果:

VolatileLong:  44305002641
VolatileLong2: 7100172492
VolatileLong3: 7335024041

可以看到蠢箩,加了padding和@sun.misc.Contended注解的運行時間多了不到1倍链蕊,而VolatileLong運行的時間比線程數(shù)是4的時候還要短事甜,這是為什么呢?

再說一下滔韵,我的CPU是四核八線程逻谦,每個核有一個L1 Cache,那么我的環(huán)境一共有4個L1 Cache陪蜻,所以邦马,2個CPU線程會共享同一個L1 Cache;由于VolatileLong對象占用24字節(jié)內(nèi)存宴卖,而代碼中VolatileLong對象是保存在數(shù)組中的滋将,所以內(nèi)存是連續(xù)的,2個VolatileLong對象的大小是48字節(jié)症昏,這樣一來随闽,對于緩存行大小是64字節(jié)來說,每個緩存行只能存放2個VolatileLong對象肝谭。

通過上面的分析可知掘宪,偽共享發(fā)生在L3 Cache,如果每個核操作的數(shù)據(jù)不在同一個緩存行中攘烛,那么就會避免偽共享的發(fā)生添诉,所以,8個線程的情況下其實是CPU線程共享了L1 Cache医寿,所以執(zhí)行的時間可能比4線程的情況還要短。下面看下執(zhí)行時4線程和8線程的CPU使用情況:

可以看到蘑斧,在4線程時靖秩,線程被平均分配到了4個核中,這樣一來竖瘾,L1 Cache肯定是不能共享的沟突,這時會發(fā)生偽共享;而8線程時捕传,每個核都使用了2個線程惠拭,這時L1 Cache是可以共享的,這在一定程度上能減少偽共享的發(fā)生庸论,從而時間會變短(也不一定职辅,但總體來說8線程的情況與4線程的運行時間幾乎不會向加padding和注解的方式差那么多)。

在Windows上情況就不太一樣了聂示,在雙核四線程的CPU上域携,測試結(jié)果并不和mac中一樣,在不加padding和注解時鱼喉,2線程和4線程執(zhí)行的時間都是將近差了1倍秀鞭,看下使用2個線程在Windows中執(zhí)行的時候CPU的使用情況:

雖然只使用了2個線程趋观,但從圖像上來看,似乎都在工作锋边,即使把線程數(shù)量設置為1也是這種情況皱坛。這應該是Windows和UNIX對CPU線程調(diào)度的方式不一樣,具體我現(xiàn)在也不太清楚他們之間的差別豆巨,希望有知道的同學告知剩辟,感謝。

@sun.misc.Contended注解

上文中將@sun.misc.Contended注解用在了對象上搀矫,@sun.misc.Contended注解還可以指定某個字段抹沪,并且可以為字段進行分組,下面通過代碼來看下:

/**
 * VM Options: 
 * -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
 * -XX:-RestrictContended
 */
public class ContendedTest {

    byte a;
    @sun.misc.Contended("a")
    long b;
    @sun.misc.Contended("a")
    long c;
    int d;

    private static Unsafe UNSAFE;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            UNSAFE = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println("offset-a: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("a")));
        System.out.println("offset-b: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("b")));
        System.out.println("offset-c: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("c")));
        System.out.println("offset-d: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("d")));

        ContendedTest contendedTest = new ContendedTest();

        // 打印對象的shallow size
        System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(contendedTest) + " bytes");
        // 打印對象的 retained size
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(contendedTest) + " bytes");
    }

}

這里還是使用到了classmexer.jar瓤球,可以參考Java對象內(nèi)存布局中的說明融欧。

這里在變量b和c中使用了@sun.misc.Contended注解,并將這兩個變量分為1組卦羡,執(zhí)行結(jié)果如下:

offset-a: 16
offset-b: 152
offset-c: 160
offset-d: 12
Shallow Size: 296 bytes
Retained Size: 296 bytes

可見int類型的變量的偏移地址是12噪馏,也就是在對象頭后面,因為它正好是4個字節(jié)绿饵,然后是變量a欠肾。@sun.misc.Contended注解的變量會加到對象的最后面,這里就是b和c了拟赊,那么b的偏移地址是152刺桃,之前說過@sun.misc.Contended注解會在變量前后各加128字節(jié),而byte類型的變量a分配完內(nèi)存后這時起始地址應該是從17開始吸祟,因為byte類型占1字節(jié)瑟慈,那么應該補齊到24,所以b的起始地址是24+128=152屋匕,而c的前面并不用加128字節(jié)葛碧,因為b和c被分為了同一組。

我們算一下c分配完內(nèi)存后过吻,這時的地址應該到了168进泼,然后再加128字節(jié),最后大小就是296纤虽。內(nèi)存結(jié)構(gòu)如下:

| d:12~16 | --- | a:16~17 | --- | 17~24 | --- | 24~152 | --- | b:152~160 | --- | c:160~168 | --- | 168~296 |

現(xiàn)在把b和c分配到不同的組中乳绕,代碼做如下修改:

/**
 * VM Options:
 * -javaagent:/Users/sangjian/dev/source-files/classmexer-0_03/classmexer.jar
 * -XX:-RestrictContended
 */
public class ContendedTest {

    byte a;
    @sun.misc.Contended("a")
    long b;
    @sun.misc.Contended("b")
    long c;
    int d;

    private static Unsafe UNSAFE;

    static {
        try {
            Field f = Unsafe.class.getDeclaredField("theUnsafe");
            f.setAccessible(true);
            UNSAFE = (Unsafe) f.get(null);
        } catch (NoSuchFieldException e) {
            e.printStackTrace();
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws NoSuchFieldException {
        System.out.println("offset-a: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("a")));
        System.out.println("offset-b: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("b")));
        System.out.println("offset-c: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("c")));
        System.out.println("offset-d: " + UNSAFE.objectFieldOffset(ContendedTest.class.getDeclaredField("d")));

        ContendedTest contendedTest = new ContendedTest();

        // 打印對象的shallow size
        System.out.println("Shallow Size: " + MemoryUtil.memoryUsageOf(contendedTest) + " bytes");
        // 打印對象的 retained size
        System.out.println("Retained Size: " + MemoryUtil.deepMemoryUsageOf(contendedTest) + " bytes");
    }

}

運行結(jié)果如下:

offset-a: 16
offset-b: 152
offset-c: 288
offset-d: 12
Shallow Size: 424 bytes
Retained Size: 424 bytes

可以看到,這時b和c中增加了128字節(jié)的padding逼纸,結(jié)構(gòu)也就變成了:

| d:12~16 | --- | a:16~17 | --- | 17~24 | --- | 24~152 | --- | b:152~160 | --- | 160~288 | --- | c:288~296 | --- | 296~424 |

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末刷袍,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子樊展,更是在濱河造成了極大的恐慌呻纹,老刑警劉巖堆生,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異雷酪,居然都是意外死亡淑仆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進店門哥力,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蔗怠,“玉大人,你說我怎么就攤上這事吩跋∧洌” “怎么了?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵锌钮,是天一觀的道長桥温。 經(jīng)常有香客問我,道長梁丘,這世上最難降的妖魔是什么侵浸? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任,我火速辦了婚禮氛谜,結(jié)果婚禮上掏觉,老公的妹妹穿的比我還像新娘。我一直安慰自己值漫,他們只是感情好澳腹,可當我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著杨何,像睡著了一般酱塔。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上晚吞,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天,我揣著相機與錄音谋国,去河邊找鬼槽地。 笑死,一個胖子當著我的面吹牛芦瘾,可吹牛的內(nèi)容都是我干的捌蚊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼近弟,長吁一口氣:“原來是場噩夢啊……” “哼缅糟!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起祷愉,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤窗宦,失蹤者是張志新(化名)和其女友劉穎赦颇,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體赴涵,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡媒怯,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了髓窜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扇苞。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖寄纵,靈堂內(nèi)的尸體忽然破棺而出鳖敷,到底是詐尸還是另有隱情,我是刑警寧澤程拭,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布定踱,位于F島的核電站,受9級特大地震影響哺壶,放射性物質(zhì)發(fā)生泄漏屋吨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一山宾、第九天 我趴在偏房一處隱蔽的房頂上張望至扰。 院中可真熱鬧,春花似錦资锰、人聲如沸敢课。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽直秆。三九已至,卻和暖如春鞭盟,著一層夾襖步出監(jiān)牢的瞬間圾结,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工齿诉, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留筝野,地道東北人。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓粤剧,卻偏偏與公主長得像歇竟,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子抵恋,可洞房花燭夜當晚...
    茶點故事閱讀 45,675評論 2 359

推薦閱讀更多精彩內(nèi)容

  • 從三月份找實習到現(xiàn)在焕议,面了一些公司,掛了不少弧关,但最終還是拿到小米盅安、百度唤锉、阿里、京東宽堆、新浪腌紧、CVTE、樂視家的研發(fā)崗...
    時芥藍閱讀 42,277評論 11 349
  • Java8張圖 11畜隶、字符串不變性 12壁肋、equals()方法、hashCode()方法的區(qū)別 13籽慢、...
    Miley_MOJIE閱讀 3,709評論 0 11
  • 1. Java基礎部分 基礎部分的順序:基本語法浸遗,類相關的語法,內(nèi)部類的語法箱亿,繼承相關的語法跛锌,異常的語法,線程的語...
    子非魚_t_閱讀 31,664評論 18 399
  • Disruptor框架學習(2)--為啥這么快 在上一篇中届惋,筆者闡述了Disruptor的代碼實現(xiàn)和數(shù)據(jù)結(jié)構(gòu)髓帽。在說...
    賈博巖閱讀 2,209評論 0 11
  • 從小侄女出生到現(xiàn)在已經(jīng)兩歲了,今天她好逗我嘴上有點東西脑豹,她嘟嘟的跑過去拿來衛(wèi)生紙給我擦嘴郑藏,其實蠻感動的啦,這么小個...
    崴崴閱讀 143評論 0 0