JMM之原子性恤溶、可見性、有序性(指令重排)

一帜羊、原子性

????原子性操作指相應(yīng)的操作是單一不可分割的操作咒程。在我們學(xué)化學(xué)這門課程的時候,對于里面講到的原子性相信大家都非常明白,原子是微觀世界中最小的不可再進行分割的單元,原子是最小的粒子忘伞。java里面的原子性操作也是如此,它代表著一個操作不能再進行分割是最小的執(zhí)行單元饥瓷,或者一系列操作要么全部成功執(zhí)行,要么全部執(zhí)行失敗痹籍,不允許中間某一些成功失敗呢铆,類比如事物控制,要么全部提交要么全部回滾蹲缠。 ????下面根據(jù)幾個粒子來分析下原子性操作:

i?=?0;???????//1
j?=?i?;??????//2
i++;?????????//3
i?=?j?+?1;???//4

上面四個操作棺克,有哪個幾個是原子操作悠垛,那幾個不是?如果不是很理解娜谊,可能會認為都是原子性操作确买,其實只有1才是原子操作,其余均不是因俐。

1在Java中拇惋,對基本數(shù)據(jù)類型的變量和賦值操作都是原子性操作周偎;?
2中包含了兩個操作:讀取i抹剩,將i值賦值給j?
3中包含了三個操作:讀取i值、i?+?1?蓉坎、將+1結(jié)果賦值給i澳眷;?
4中同三一樣

在單線程環(huán)境下我們可以認為整個步驟都是原子性操作,但是在多線程環(huán)境下則不同蛉艾,Java只保證了基本數(shù)據(jù)類型的變量和賦值操作才是原子性的(注:在32位的JDK環(huán)境下钳踊,對64位數(shù)據(jù)的讀取不是原子性操作*,如long勿侯、double)拓瞪。在多線程環(huán)境中,非原子操作可能會受其他線程的干擾助琐,例如第3個操作祭埂,i在加1之后將結(jié)果賦值給i,在賦值給i回寫主內(nèi)存的時候可能會被其他線程搶先回寫兵钮,導(dǎo)致此次執(zhí)行失敗丟失了本次計算結(jié)果(這里會涉及到原子性操作蛆橡,下面會進行講解)。

public?class?AtomicTest?{

????private?int?i?=?0;

????public?void?add()?{
????????i++;
????}

????public?static?void?main(String[]?args)?{????????
????????for?(int?t?=?0;?t?<?10;?t++)?{
????????????AtomicTest?test?=?new?AtomicTest();
????????????Thread[]?threads?=?new?Thread[10];???????????
????????????for?(int?i?=?0;?i?<?threads.length;?i++)?{
????????????????threads[i]?=?new?Thread(()?->?{????????????????????
????????????????for?(int?k?=?0;?k?<?1000;?k++)?{
????????????????????????test.add();
????????????????????}
????????????????});???????????????
????????????????threads[i].start();
????????????}????????????
????????????Arrays.stream(threads).forEach(th?->?{????????????????
????????????try?{
????????????????????th.join();
????????????????}?catch?(InterruptedException?e)?{
????????????????????e.printStackTrace();
????????????????}
????????????});????????????
????????????System.out.println("第"?+?(t?+?1)?+?"次執(zhí)行結(jié)果:"?+?test.i);
????????}
????}
}
第1次執(zhí)行結(jié)果:8987第2次執(zhí)行結(jié)果:8970第3次執(zhí)行結(jié)果:6820第4次執(zhí)行結(jié)果:9841第5次執(zhí)行結(jié)果:10000第6次執(zhí)行結(jié)果:7766第7次執(zhí)行結(jié)果:8105第8次執(zhí)行結(jié)果:10000第9次執(zhí)行結(jié)果:10000第10次執(zhí)行結(jié)果:10000

最終的執(zhí)行結(jié)果會是小于等于10000掘譬,在某些情況下與我們所期望的結(jié)果10000不符合泰演,并發(fā)的情況下導(dǎo)致bug的產(chǎn)生。

要想在多線程環(huán)境下保證原子性葱轩,則可以通過鎖睦焕、synchronized來確保。volatile是無法保證復(fù)合操作的原子性靴拱。

二垃喊、可見性

????可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值缭嫡,其他線程能夠立即看得到修改的值缔御。CPU在執(zhí)行代碼的時候,為了減少變量訪問的時間消耗可能將代碼中訪問的變量的值緩存到該CPU緩存區(qū)中妇蛀,因此耕突,相應(yīng)的代碼再次訪問該變量的時候笤成,相應(yīng)的值可能從CPU緩存中而不是主內(nèi)存中讀取的。同樣的眷茁,代碼對這些被緩存過的變量的值的修改也可能僅是被寫入CPU緩存區(qū)炕泳,而沒有寫入主內(nèi)存。由于每個CPU都有自己的緩存區(qū)上祈,因此一個CPU緩存區(qū)中的內(nèi)容對于其他CPU而言是不可見的培遵。這就導(dǎo)致了在其他CPU上運行的其他線程可能無法看到其他線程對某個變量值的修改。


????對于可見性登刺,Java提供了volatile關(guān)鍵字來保證可見性籽腕。當一個共享變量被volatile修飾時,它會保證修改的值會立即被更新到主存纸俭,當有其他線程需要讀取時皇耗,它會去內(nèi)存中讀取新值。而普通的共享變量不能保證可見性揍很,因為普通共享變量被修改之后郎楼,什么時候被寫入主存是不確定的,當其他線程去讀取時窒悔,此時內(nèi)存中可能還是原來的舊值呜袁,因此無法保證可見性。另外简珠,通過synchronized和Lock也能夠保證可見性阶界,synchronized和Lock能保證同一時刻只有一個線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會將對變量的修改刷新到主存當中北救。因此可以保證可見性荐操。

三、有序性(指令重排)


????有序性最終表述的現(xiàn)象是CPU是否按照既定代碼順序執(zhí)行依次執(zhí)行指令珍策。編譯器和CPU為了提高指令的執(zhí)行效率可能會進行指令重排序托启,這使得代碼的實際執(zhí)行方式可能不是按照我們所認為的方式進行,在單線程的情況下只要保證最終執(zhí)行結(jié)果正確即可攘宙。如下:

int?i?=?0;????????????//語句1??
boolean?flag?=?false;?//語句2
i?=?1;????????????????//語句3??
flag?=?true;??????????//語句4

上面代碼最終執(zhí)行結(jié)果是i=1屯耸、flag=true,在不影響這個結(jié)果的情況下語句2可能比語句1先執(zhí)行蹭劈,語句4可能比語句3先執(zhí)行疗绣。此種指令重排之后單線程下不會有問題,單如果是在多線程的情況下呢铺韧?

public?class?SerialTest?{
????static?SerialTest?serialTest;
????static?boolean?isInit?=?false;

????public?static?void?main(String[]?args)?{????????
????for(int?i=0;?i<?200;i++)?{
????????????serialTest?=?null;
????????????isInit?=?false;????????????
????????????new?Thread(()->{
????????????????serialTest?=?new?SerialTest();//語句1
????????????????isInit?=?true;????????????????//語句2
????????????}).start();????????????
????????????new?Thread(()->{????????????????
????????????????if(isInit)?{
????????????????????serialTest.doSomething();
????????????????}
????????????}).start();
????????}
????}????
????public?void?doSomething()?{????????
????????System.out.println("doSomething");
????}
}

運行上面代碼執(zhí)行的結(jié)果如下:

Exception?in?thread?"Thread-283"?java.lang.NullPointerException
	at?com.cd.concurrent.SerialTest.lambda$main$1(SerialTest.java:25)
	at?java.lang.Thread.run(Thread.java:748)
......
doSomething
doSomething
doSomething
doSomething
doSomething
Exception?in?thread?"Thread-283"?java.lang.NullPointerException
	at?com.cd.concurrent.SerialTest.lambda$main$1(SerialTest.java:25)
	at?java.lang.Thread.run(Thread.java:748)

我們所期望的結(jié)果應(yīng)該是每次都會打印doSOmething多矮,可是這里會報空指針異常,出現(xiàn)這種情況的原因就是因為指令重排導(dǎo)致,上面語句1和語句2最終執(zhí)行順序可能會變?yōu)檎Z句2先執(zhí)行塔逃,語句1還未執(zhí)行讯壶,此時剛有有一個線程獨到了isInit的值為true,此時通過對象取調(diào)用方法就報空指針湾盗,因為此時SerialTest對象還未被實例化伏蚊。

指令重排序不會影響單個線程的執(zhí)行,但是會影響到線程并發(fā)執(zhí)行的正確性格粪。也就是說躏吊,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性帐萎、可見性以及有序性比伏。只要有一個沒有被保證,就有可能會導(dǎo)致程序運行不正確吓肋。

在Java里面凳怨,可以通過volatile關(guān)鍵字來保證一定的“有序性”。另外可以通過synchronized和Lock來保證有序性是鬼,很顯然,synchronized和Lock保證每個時刻是有一個線程執(zhí)行同步代碼紫新,相當于是讓線程順序執(zhí)行同步代碼均蜜,自然就保證了有序性。另外芒率,Java內(nèi)存模型具備一些先天的“有序性”囤耳,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為 happens-before 原則偶芍。如果兩個操作的執(zhí)行次序無法從happens-before原則推導(dǎo)出來充择,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序匪蟀。


happens-before原則


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市仔沿,隨后出現(xiàn)的幾起案子坐桩,更是在濱河造成了極大的恐慌,老刑警劉巖封锉,帶你破解...
    沈念sama閱讀 210,978評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件绵跷,死亡現(xiàn)場離奇詭異,居然都是意外死亡成福,警方通過查閱死者的電腦和手機碾局,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,954評論 2 384
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來奴艾,“玉大人净当,你說我怎么就攤上這事≡塘剩” “怎么了像啼?”我有些...
    開封第一講書人閱讀 156,623評論 0 345
  • 文/不壞的土叔 我叫張陵,是天一觀的道長潭苞。 經(jīng)常有香客問我忽冻,道長,這世上最難降的妖魔是什么此疹? 我笑而不...
    開封第一講書人閱讀 56,324評論 1 282
  • 正文 為了忘掉前任僧诚,我火速辦了婚禮,結(jié)果婚禮上蝗碎,老公的妹妹穿的比我還像新娘湖笨。我一直安慰自己,他們只是感情好蹦骑,可當我...
    茶點故事閱讀 65,390評論 5 384
  • 文/花漫 我一把揭開白布慈省。 她就那樣靜靜地躺著,像睡著了一般脊串。 火紅的嫁衣襯著肌膚如雪辫呻。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,741評論 1 289
  • 那天琼锋,我揣著相機與錄音放闺,去河邊找鬼。 笑死缕坎,一個胖子當著我的面吹牛怖侦,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 38,892評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼匾寝,長吁一口氣:“原來是場噩夢啊……” “哼搬葬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起艳悔,我...
    開封第一講書人閱讀 37,655評論 0 266
  • 序言:老撾萬榮一對情侶失蹤急凰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后猜年,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體抡锈,經(jīng)...
    沈念sama閱讀 44,104評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,451評論 2 325
  • 正文 我和宋清朗相戀三年乔外,在試婚紗的時候發(fā)現(xiàn)自己被綠了床三。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,569評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡杨幼,死狀恐怖撇簿,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情差购,我是刑警寧澤四瘫,帶...
    沈念sama閱讀 34,254評論 4 328
  • 正文 年R本政府宣布,位于F島的核電站歹撒,受9級特大地震影響莲组,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜暖夭,卻給世界環(huán)境...
    茶點故事閱讀 39,834評論 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望撵孤。 院中可真熱鬧迈着,春花似錦、人聲如沸邪码。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,725評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽闭专。三九已至奴潘,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間影钉,已是汗流浹背画髓。 一陣腳步聲響...
    開封第一講書人閱讀 31,950評論 1 264
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留平委,地道東北人奈虾。 一個月前我還...
    沈念sama閱讀 46,260評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肉微。 傳聞我的和親對象是個殘疾皇子匾鸥,可洞房花燭夜當晚...
    茶點故事閱讀 43,446評論 2 348

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