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

一、原子性

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

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

上面四個(gè)操作,有哪個(gè)幾個(gè)是原子操作,那幾個(gè)不是?如果不是很理解,可能會(huì)認(rèn)為都是原子性操作,其實(shí)只有1才是原子操作,其余均不是。

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

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

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é)果會(huì)是小于等于10000,在某些情況下與我們所期望的結(jié)果10000不符合,并發(fā)的情況下導(dǎo)致bug的產(chǎn)生。

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

二、可見性

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


????對于可見性,Java提供了volatile關(guān)鍵字來保證可見性。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會(huì)保證修改的值會(huì)立即被更新到主存,當(dāng)有其他線程需要讀取時(shí),它會(huì)去內(nèi)存中讀取新值。而普通的共享變量不能保證可見性,因?yàn)槠胀ü蚕碜兞勘恍薷闹?,什么時(shí)候被寫入主存是不確定的,當(dāng)其他線程去讀取時(shí),此時(shí)內(nèi)存中可能還是原來的舊值,因此無法保證可見性。另外,通過synchronized和Lock也能夠保證可見性,synchronized和Lock能保證同一時(shí)刻只有一個(gè)線程獲取鎖然后執(zhí)行同步代碼,并且在釋放鎖之前會(huì)將對變量的修改刷新到主存當(dāng)中。因此可以保證可見性。

三、有序性(指令重排)


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

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

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

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");
????}
}

運(yùn)行上面代碼執(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)該是每次都會(huì)打印doSOmething,可是這里會(huì)報(bào)空指針異常,出現(xiàn)這種情況的原因就是因?yàn)橹噶钪嘏艑?dǎo)致,上面語句1和語句2最終執(zhí)行順序可能會(huì)變?yōu)檎Z句2先執(zhí)行,語句1還未執(zhí)行,此時(shí)剛有有一個(gè)線程獨(dú)到了isInit的值為true,此時(shí)通過對象取調(diào)用方法就報(bào)空指針,因?yàn)榇藭r(shí)SerialTest對象還未被實(shí)例化。

指令重排序不會(huì)影響單個(gè)線程的執(zhí)行,但是會(huì)影響到線程并發(fā)執(zhí)行的正確性。也就是說,要想并發(fā)程序正確地執(zhí)行,必須要保證原子性、可見性以及有序性。只要有一個(gè)沒有被保證,就有可能會(huì)導(dǎo)致程序運(yùn)行不正確。

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


happens-before原則


?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容