偽共享與volatile

volatile

先來說說volatie的作用

  • 禁止指令重排
  • 保證變量的可見性,但是不能保證互斥性

具體實(shí)現(xiàn)是采用了內(nèi)存屏障

在《并發(fā)編程藝術(shù)》這本書中說到被volatile修飾的變量進(jìn)行寫操作的時(shí)候星虹,會(huì)多出一行l(wèi)ock前綴的指令,觸發(fā)兩件事

  • 將當(dāng)前處理器的緩存行數(shù)據(jù)寫回到系統(tǒng)內(nèi)存
  • 這個(gè)寫回到內(nèi)存的操作會(huì)使其他CPU里的緩存了該內(nèi)存地址的數(shù)據(jù)無效

對(duì)象大小

我們知道java對(duì)象頭的大小在32系統(tǒng)下面是8B隙弛,但是在64位系統(tǒng)下面就是16B,但是在java8里面笛匙,默認(rèn)開啟了指針壓縮洞豁,所以是12B,但是我們都知道是以一個(gè)字寬為單位的茸歧,所以padding 4B倦炒,我們導(dǎo)入個(gè)小工具

        <dependency>
            <groupId>org.apache.lucene</groupId>
            <artifactId>lucene-core</artifactId>
            <version>4.2.0</version>
        </dependency>

然后測(cè)試:

class SharingInt {
    volatile int value;
}
System.out.println("object size:"+RamUsageEstimator.sizeOf(new Object()));
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
結(jié)果:
object size:16
sharingInt size:16

可以看出沒有實(shí)例域的Object是16B,有一個(gè)int實(shí)例域的SharingInt也是16B软瞎,是因?yàn)槟J(rèn)開啟了指針壓縮逢唤,int在java中是占用4B,所以12B+4B=16B涤浇,這借用一下R大的回復(fù)


偽共享

cpu高速緩存中的最小單位是緩存行鳖藕,它的大小一般為32B,64B,128B,265B,現(xiàn)在電腦最常見的緩存行就64B的只锭。當(dāng)多個(gè)線程訪問修改獨(dú)立的變量的時(shí)候著恩,恰好這些變量?jī)?nèi)存地址很接近,同在一條緩存行上面蜻展,由于MESI協(xié)議的原因喉誊,就會(huì)無意之間影響了性能

我們來看一個(gè)例子

class SharingInt {
    volatile int value;
//     long p1, p2, p3, p4, p5, p6;     
}
public class CacheLine extends Thread {

    private final SharingInt[] shares;
    private final int index;

    public CacheLine(SharingInt[] shares, int index) {
        this.shares = shares;
        this.index = index;
    }

    /**
     *      maven 導(dǎo)入小工具
     *      <dependency>
     *          <groupId>org.apache.lucene</groupId>
     *          <artifactId>lucene-core</artifactId>
     *          <version>4.2.0</version>
     *      </dependency>
     *
     */
    public static void main(String[] args) throws InterruptedException {
//        System.out.println(RamUsageEstimator.sizeOf(new SharingInt()));
        for (int i = 0; i < 10; i++) {
            test();
        }
    }

    private static void test() throws InterruptedException {
        //cpu 并行處理
        int size = Runtime.getRuntime().availableProcessors();
        SharingInt[] shares = new SharingInt[size];
        for (int i = 0; i < size; i++) {
            shares[i] = new SharingInt();
        }
        Thread[] threads = new Thread[size];
        for (int i = 0; i < size; i++) {
            threads[i] = new CacheLine(shares, i);
        }
        for (Thread t : threads) {
            t.start();
        }
        long start = System.currentTimeMillis();
        for (Thread t : threads) {
            t.join();
        }
        long end = System.currentTimeMillis();
        System.out.printf("用時(shí): %dms\n", end - start);
    }


    @Override
    public void run() {
        for (int i = 0; i < 100000000; i++) {
            shares[index].value++;
        }
    }
}

代碼很簡(jiǎn)單,N(與CPU核心相同)條線程共享同一個(gè)數(shù)組纵顾,讓1~N條線程分別訪問同一個(gè)數(shù)組的不同下標(biāo)伍茄,互不干擾,每個(gè)線程循環(huán)1億次讀寫操作(shares[index].v++)


我的電腦是4核8線程64位的系統(tǒng)施逾,運(yùn)行結(jié)果如下:

用時(shí): 10531ms
用時(shí): 9665ms
用時(shí): 9668ms
用時(shí): 9974ms
用時(shí): 10364ms
用時(shí): 10250ms
用時(shí): 10342ms
用時(shí): 10982ms
用時(shí): 10604ms
用時(shí): 10931ms

然后再去掉SharingInt里面的注釋幻林,再跑一遍

用時(shí): 3735ms
用時(shí): 4082ms
用時(shí): 4007ms
用時(shí): 1376ms
用時(shí): 3860ms
用時(shí): 3685ms
用時(shí): 4366ms
用時(shí): 1341ms
用時(shí): 3039ms
用時(shí): 3777ms

為什么會(huì)有那么大的差距呢贞盯?是因?yàn)閭喂蚕淼木壒十?dāng)?shù)谝粭l線程返回index=0的時(shí)候


  1. 假設(shè)線程1音念,線程2分別在Core1沪饺,Core2中獲取到時(shí)間令牌,然后都會(huì)加載Cache Line 1闷愤,這時(shí)候Cache Line 1的狀態(tài)是S(共享)
  2. 然后可能線程1先修改了index=0的SharingInt.value整葡,然后Cache Line 1 從 S變?yōu)?strong>M(修改),然后根據(jù)volatile的語義讥脐,然后立馬把Cache Line 寫回到主存遭居,然后Cache Line 1 的狀態(tài)置從M變?yōu)?strong>I(無效)
  3. 然后等到Core2 需要修改index=1的SharingInt.value時(shí),發(fā)現(xiàn)Cache Line 1 的狀態(tài)為I(無效)旬渠,然后直又從主存讀取Cache Line 1進(jìn)來俱萍,然后把狀態(tài)變?yōu)?strong>E獨(dú)享,然后修改value之后告丢,又將Cache Line 刷新回主存枪蘑。

以上就是MESI緩存一致性協(xié)議的工作過程,可以看出一條一樣數(shù)據(jù)被多讀進(jìn)一次CPU 的cache岖免,所以這個(gè)操作就消耗了時(shí)間

避免偽共享

避免偽共享的兩種方式:
1.增大對(duì)象的空間岳颇,使得需要訪問的數(shù)據(jù)不在同一個(gè)Cache Line上面,典型的空間換時(shí)間的方法

  1. 在每個(gè)線程添加本地副本颅湘,等待完全修改完成后再寫回主存

padding:

修改SharingInt

class SharingInt {
    volatile int value;
     long p1, p2, p3, p4, p5, p6;
}
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
sharingInt size:64

這樣一個(gè)SharingInt對(duì)象就填充了一個(gè)緩存行了话侧,在java中一個(gè)long就是8B,加多6個(gè)剛剛好64B闯参,這樣子各個(gè)線程對(duì)相對(duì)應(yīng)的對(duì)象修改就不會(huì)在不同的緩存行


上面追加之后的結(jié)果之后雖然快了很多瞻鹏,但是你會(huì)發(fā)現(xiàn)有一些1秒多有一些需要3秒多甚至4秒。這就是這種方式的不好之處鹿寨,因?yàn)閭€(gè)人的操作系統(tǒng)或者CPU架構(gòu)都可能不一樣新博,

java7會(huì)優(yōu)化這種字節(jié)追加方式而導(dǎo)致失效,但是查看java8編譯的字節(jié)碼來看释移,并沒有優(yōu)化掉,但是沒有辦法穩(wěn)定下來

以繼承的方式避免優(yōu)化

我們修改一下SharingInt

class Temp{
    long p1,p2,p3,p4,p5,p6;
}
class SharingInt extends Temp{
    volatile int value;
}
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
sharingInt size:72

有人可能會(huì)問為什么多了8B叭披,因?yàn)槭抢^承關(guān)系,子類會(huì)多一個(gè)Reference類型玩讳,Reference類型在java中占4B涩蜘,然后padding 4B就剛剛好72B
運(yùn)行結(jié)果

用時(shí): 1623ms
用時(shí): 1305ms
用時(shí): 1295ms
用時(shí): 1307ms
用時(shí): 1279ms
用時(shí): 1286ms
用時(shí): 1277ms
用時(shí): 1269ms
用時(shí): 1279ms
用時(shí): 1312ms

雖然穩(wěn)定了優(yōu)化但是這樣某一天java又進(jìn)行了一系列的優(yōu)化也許也不行了,但是在在java8給出了官方的實(shí)現(xiàn)

@Contended

在2012年openjdk的JEP-142說到使用這個(gè)注解可以自動(dòng)追加合適的大小padding

這個(gè)注解需要是用在用戶代碼上面(非bootstrap class loader或者extension class loader所加載的類)熏纯,并且需要添加-XX:-RestrictContended啟動(dòng)參數(shù)

我們修改SharingInt

class SharingInt {
    @Contended
    volatile int value;
}
System.out.println("sharingInt size:"+RamUsageEstimator.sizeOf(new SharingInt()));
sharingInt size:144

我們看到SharingInt被追加128B的padding同诫,在JEP-142中提及

Note that we use 128 bytes, twice the cache line size on most hardware
to adjust for adjacent sector prefetchers extending the false sharing
collisions to two cache lines.

padding的大小定義為目前大多數(shù)CPU的Cache Line 大小的2倍,就是128B

分組功能:

There are cases where you want to separate the group of fields that
are experiencing contention with everything else but not pairwise. This
is the usual thing for some of the code updating two fields at once.
While marking both with @Contended would be sufficient, we can optimize
the memory footprint by not applying padding between them. In order to
demarcate these groups, we have the parameter in the annotation
describing the equivalence class for contention group.

意思就是如果兩個(gè)字段a樟澜,b都被一個(gè)CPU修改误窖,雖然各自追加padding就足夠了叮盘,但是jvm可以將a,b字段優(yōu)化在一個(gè)Cache Line上面
我們看一個(gè)例子:

class VolatileLong {
    @Contended("1")
    public volatile long value1 = 0L;
    @Contended("1")
    public volatile long value2 = 0L;

    @Contended("2")
    public volatile long value3 = 0L;
    @Contended("2")
    public volatile long value4 = 0L;
}

public final class ContendedTest implements Runnable {
    private final VolatileLong volatileLong;
    private final int id;

    public ContendedTest(int id,VolatileLong volatileLong) {
        this.id = id;
        this.volatileLong = volatileLong;
    }
  //-XX:-RestrictContended
    public static void main(final String[] args) throws Exception {
        runTest();
    }

    private static void runTest() throws InterruptedException {
        VolatileLong volatileLong = new VolatileLong();
        Thread t0 = new Thread(new ContendedTest(1,volatileLong));
        Thread t1 = new Thread(new ContendedTest(2,volatileLong));
        final long start = System.currentTimeMillis();
        t0.start();
        t1.start();
        t0.join();
        t1.join();
        System.out.println("用時(shí):" + (System.currentTimeMillis() - start)+"ms");
    }
    @Override
    public void run() {
        long i = 500000000;
        if (1 == id) {
            while (0 != i--) {
                volatileLong.value1 = i;
                volatileLong.value2 = i;
            }
        } else if (2 == id) {
            while (0 != i--) {
                volatileLong.value3 = i;
                volatileLong.value4 = i;
            }
        }
    }
}

運(yùn)行結(jié)果:用時(shí):6151ms
代碼很簡(jiǎn)單,兩個(gè)線程分別對(duì)兩個(gè)long變量賦值霹俺,重復(fù)5億次使用了6s的時(shí)間
我們將run()方法替換如下再跑一遍

    @Override
    public void run() {
        long i = 500000000;
        if (1 == id) {
            while (0 != i--) {
                volatileLong.value1 = i;
                volatileLong.value3 = i;
            }
        } else if (2 == id) {
            while (0 != i--) {
                volatileLong.value2 = i;
                volatileLong.value4 = i;
            }
        }
    }

運(yùn)行結(jié)果:用時(shí):23963ms
那是使用了@contended注解分組

  • value1,value2被分配到了一條Cache Line
  • value3 value4被分配到了一條Cache Line

兩條線程相互修改對(duì)方的Cache Line柔吼,又要從主存里面重新讀取最新的數(shù)據(jù),所以這件花費(fèi)了大量的時(shí)間

本地變量副本

在JMM(java Memory Model)中丙唧,每一個(gè)線程都會(huì)有一個(gè)線程副本愈魏,每一次修改完之后不會(huì)立馬刷新回主存,而是等處理完之后才刷新會(huì)主存


我們改一下上面的VolatileLong

class VolatileLong {
    @Contended("1")
    public long value1 = 0L;
    @Contended("1")
    public long value2 = 0L;

    @Contended("2")
    public long value3 = 0L;
    @Contended("2")
    public long value4 = 0L;
}

我們分別使用兩種run方法去執(zhí)行想际,兩個(gè)方法的耗時(shí)
第一中run方法

用時(shí):398ms

第二種run方法

用時(shí):2871ms

雖然有差距培漏,但是也沒有之前那么嚴(yán)重了,所以使用volatile需要謹(jǐn)慎

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末胡本,一起剝皮案震驚了整個(gè)濱河市牌柄,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌侧甫,老刑警劉巖珊佣,帶你破解...
    沈念sama閱讀 217,406評(píng)論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異闺骚,居然都是意外死亡彩扔,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,732評(píng)論 3 393
  • 文/潘曉璐 我一進(jìn)店門僻爽,熙熙樓的掌柜王于貴愁眉苦臉地迎上來虫碉,“玉大人,你說我怎么就攤上這事胸梆《嘏酰” “怎么了?”我有些...
    開封第一講書人閱讀 163,711評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵碰镜,是天一觀的道長(zhǎng)兢卵。 經(jīng)常有香客問我,道長(zhǎng)绪颖,這世上最難降的妖魔是什么秽荤? 我笑而不...
    開封第一講書人閱讀 58,380評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮柠横,結(jié)果婚禮上窃款,老公的妹妹穿的比我還像新娘。我一直安慰自己牍氛,他們只是感情好晨继,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,432評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著搬俊,像睡著了一般紊扬。 火紅的嫁衣襯著肌膚如雪蜒茄。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,301評(píng)論 1 301
  • 那天餐屎,我揣著相機(jī)與錄音檀葛,去河邊找鬼。 笑死啤挎,一個(gè)胖子當(dāng)著我的面吹牛驻谆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庆聘,決...
    沈念sama閱讀 40,145評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼勺卢!你這毒婦竟也來了伙判?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,008評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤黑忱,失蹤者是張志新(化名)和其女友劉穎宴抚,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體甫煞,經(jīng)...
    沈念sama閱讀 45,443評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡菇曲,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,649評(píng)論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抚吠。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片常潮。...
    茶點(diǎn)故事閱讀 39,795評(píng)論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖楷力,靈堂內(nèi)的尸體忽然破棺而出喊式,到底是詐尸還是另有隱情,我是刑警寧澤萧朝,帶...
    沈念sama閱讀 35,501評(píng)論 5 345
  • 正文 年R本政府宣布岔留,位于F島的核電站,受9級(jí)特大地震影響检柬,放射性物質(zhì)發(fā)生泄漏献联。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,119評(píng)論 3 328
  • 文/蒙蒙 一何址、第九天 我趴在偏房一處隱蔽的房頂上張望里逆。 院中可真熱鬧,春花似錦头朱、人聲如沸运悲。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,731評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽班眯。三九已至希停,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間署隘,已是汗流浹背宠能。 一陣腳步聲響...
    開封第一講書人閱讀 32,865評(píng)論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留磁餐,地道東北人违崇。 一個(gè)月前我還...
    沈念sama閱讀 47,899評(píng)論 2 370
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像诊霹,于是被迫代替她去往敵國和親羞延。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,724評(píng)論 2 354

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