由于各種硬件及操作系統(tǒng)的內(nèi)存訪(fǎng)問(wèn)差異,java虛擬機(jī)使用java內(nèi)存模型(java Memory Model,JMM)來(lái)規(guī)范java對(duì)內(nèi)存的訪(fǎng)問(wèn)。這套模型在jdk 1.2中開(kāi)始建立,經(jīng)jdk 1.5的修訂,現(xiàn)已逐步完善起來(lái)。
什么是java內(nèi)存模型,為什么會(huì)有這個(gè)模型?關(guān)于這個(gè)問(wèn)題,就不得不從并發(fā)的問(wèn)題講起。在多核系統(tǒng)中,處理器一般設(shè)置緩存來(lái)加速數(shù)據(jù)的讀取,緩存大大提升了程序性能,卻也帶來(lái)了“緩存一致性”的新問(wèn)題。比如,當(dāng)多個(gè)處理器寫(xiě)同一塊主內(nèi)存時(shí),以誰(shuí)的緩存數(shù)據(jù)為準(zhǔn)?讀取、寫(xiě)入內(nèi)存的變量需遵循怎樣保證線(xiàn)程安全?針對(duì)這些問(wèn)題,java設(shè)計(jì)了一套內(nèi)存模型以用來(lái)定義程序中各個(gè)變量的訪(fǎng)問(wèn)規(guī)則。
java的內(nèi)存模型采用的是共享內(nèi)存的線(xiàn)程通信機(jī)制。線(xiàn)程之間的共享變量存儲(chǔ)在主內(nèi)存中,每個(gè)線(xiàn)程都有一個(gè)私有的本地內(nèi)存,本地內(nèi)存存儲(chǔ)了共享變量的副本。 
圖片來(lái)自《深入理解java虛擬機(jī) 第2版》
關(guān)于共享變量,可以對(duì)應(yīng)為存儲(chǔ)在堆內(nèi)存的實(shí)例變量、類(lèi)變量及數(shù)組元素(堆內(nèi)存是線(xiàn)程共享的)。私有變量可對(duì)應(yīng)虛擬機(jī)棧中的局部變量。事實(shí)上,他們是java內(nèi)存不同層次的劃分,并沒(méi)有一定聯(lián)系。
要完成主內(nèi)存與工作內(nèi)存的交互操作,需遵守一定的規(guī)則。java內(nèi)存模型定義了相當(dāng)嚴(yán)謹(jǐn)而復(fù)雜的訪(fǎng)問(wèn)規(guī)則。主要有8種原子性的操作。分別是:lock(鎖定)、unlock(解鎖)、read(讀取)、load(載入)、use(使用)、assign(賦值)、store(存儲(chǔ))、write(寫(xiě)入)。
內(nèi)存交互時(shí),必須使用以上幾種操作搭配完成,且這8種操作要滿(mǎn)足一定規(guī)則。如read和load,store和write必須成對(duì)出現(xiàn);對(duì)變量實(shí)施use、store時(shí),必須先執(zhí)行assign和load操作。
幸好,這些難以記憶的規(guī)則有一個(gè)等效判定的原則,即先行發(fā)生原則。
程序次序規(guī)則:在一個(gè)線(xiàn)程中,程序控制流前面的操作先行發(fā)生于后面的操作。監(jiān)視器鎖規(guī)則:一個(gè)unlock操作先行發(fā)生于對(duì)同一個(gè)鎖的lock操作。volatile變量規(guī)則:對(duì)于一個(gè)volatile變量,寫(xiě)操作先行發(fā)生于對(duì)這個(gè)變量的讀操作。傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,則操作A先行發(fā)生于操作C。我們知道java的多線(xiàn)程通信采用共享內(nèi)存的方式。線(xiàn)程對(duì)變量的所有操作都要在工作內(nèi)存中進(jìn)行,不能直接訪(fǎng)問(wèn)主內(nèi)存。線(xiàn)程間變量傳遞均需主內(nèi)存間接完成。

則,線(xiàn)程A要與線(xiàn)程B通信(比如B線(xiàn)程要讀取A線(xiàn)程經(jīng)操作后的值),需要:
線(xiàn)程A修改本地內(nèi)存A的值,并將其寫(xiě)入主內(nèi)存的共享變量。線(xiàn)程B到主內(nèi)存讀取線(xiàn)程A修改后的值。前面我們提到的8種原子操作都是原子性的,這樣可以保證對(duì)基本數(shù)據(jù)類(lèi)型的訪(fǎng)問(wèn)讀寫(xiě)是原子性的。這里有個(gè)例外是JVM沒(méi)有強(qiáng)制規(guī)定long、double一定是原子操作。但幾乎所有的商業(yè)JVM都實(shí)現(xiàn)了long、double的原子操作。
可見(jiàn)性是指,當(dāng)一個(gè)線(xiàn)程修改了共享變量的值,其他變量能得知這個(gè)修改。
這里需要引出本文第二個(gè)關(guān)鍵點(diǎn):volatile。volatile有兩個(gè)語(yǔ)義。這里用其可見(jiàn)性語(yǔ)義。經(jīng)volatile修飾的變量保證新值能立即同步到主內(nèi)存中,每次使用前立即從主內(nèi)存刷新。保證了多線(xiàn)程操作時(shí)變量的可見(jiàn)性。后面會(huì)有更詳細(xì)解釋。
除volatile外,synchronized和final也能實(shí)現(xiàn)可見(jiàn)性。 synchronized的可見(jiàn)性由“對(duì)一個(gè)變量執(zhí)行unlock前,必須先把此變量同步回主內(nèi)存”。獲得。
final關(guān)鍵字的可見(jiàn)性指:被final修飾的字段在構(gòu)造器中初始完成,則其他線(xiàn)程就能看到final的值。
java程序本身具有的有序性可以總結(jié)為:如果在同一線(xiàn)程觀察,所有操作都是有序的。而如果在一個(gè)線(xiàn)程觀察另一線(xiàn)程,所有操作都是無(wú)序的。前部分指在單線(xiàn)程環(huán)境中程序的順序性,后部分說(shuō)的無(wú)序是指“指令的重排序”和“工作內(nèi)存與主內(nèi)存的同步延遲”。
編譯器能夠自由的以?xún)?yōu)化的名義去改變指令順序。在特定的環(huán)境下,處理器可能會(huì)次序顛倒的執(zhí)行指令。是為指令的重排序。在單線(xiàn)程環(huán)境中,程序執(zhí)行結(jié)果不會(huì)受到指令重排序的影響。
但有時(shí),我們?cè)诙嗑€(xiàn)程情況下,并不希望發(fā)生指令重排序來(lái)影響并發(fā)結(jié)果。
java提供了volatile和synchronized來(lái)保證線(xiàn)程之間操作的有序性。volatile含有禁止指令重排序的語(yǔ)義(即它的第二個(gè)語(yǔ)義),synchronized規(guī)定一個(gè)變量在同一時(shí)刻只允許一條線(xiàn)程對(duì)其lock操作,也就是說(shuō)同一個(gè)鎖的兩個(gè)同步塊只能串行進(jìn)入。禁止了指令的重排序。
關(guān)于指令重排序,下文還有更多解釋。
介紹完java內(nèi)存模型的3個(gè)特征,現(xiàn)在來(lái)詳細(xì)介紹volatile及它代表的語(yǔ)義。
準(zhǔn)確來(lái)說(shuō),volatile是java提供的輕量的同步機(jī)制。它有兩個(gè)特性: 1. 保證修飾的變量對(duì)所有線(xiàn)程的可見(jiàn)性。 2. 禁止指令的重排序優(yōu)化。
根據(jù)上面的介紹,我們對(duì)可見(jiàn)性及禁止重排序背后的順序性都不陌生。下面我們來(lái)詳細(xì)說(shuō)明下。
volatile變量對(duì)所有線(xiàn)程是立即可見(jiàn)的,對(duì)volatile變量的寫(xiě)操作都能立即反應(yīng)到其他線(xiàn)程中。
volatile boolean flag;public void shundown(){ flag = true;}public void doWork(){ while(!flag){ doSomething(); } }上面的例子即是volatile的典型應(yīng)用。任一線(xiàn)程調(diào)用了shundown()方法,都能保證所有線(xiàn)程執(zhí)行doWork()時(shí)doSomething()方法不執(zhí)行。
假設(shè)flag 不是由volatile修飾,則不能保證內(nèi)存可見(jiàn)性,當(dāng)某個(gè)線(xiàn)程修改了flag的值后,其他線(xiàn)程不一定會(huì)馬上看到或根本看不到,就會(huì)引起錯(cuò)誤。
需注意的是,volatile變量保證可見(jiàn)性時(shí),需滿(mǎn)足以下規(guī)則:
運(yùn)算結(jié)果不依賴(lài)變量的當(dāng)前值,或保證只有單一線(xiàn)程修改變量值。(如i++,運(yùn)算依賴(lài)當(dāng)前值,就不滿(mǎn)足)變量不需要與其他狀態(tài)變量共同參與不變約束。public class TestThread2 { public static volatile int race = 0; public static void increase(){ race++; } PRivate static final int THREADS_COUNT =20; public static void main(String[] args) { Thread[] threads = new Thread[THREADS_COUNT]; for(int i=0;i<THREADS_COUNT;i++){ threads[i] = new Thread(()->{ for(int j=0;j<1000;j++){ increase(); } }); threads[i].start(); } System.out.println(race); }}如上例,若正確并發(fā),則最后應(yīng)輸出20*1000=20000,可結(jié)果總輸出小于20000的結(jié)果,且每次都不相同。原因就在于volatile不能保證 race++的可見(jiàn)性。race++ 操作實(shí)際上有1.讀取race的值;2.對(duì)race加1;3.修改race的值3步操作,而volatile顯然不能保證這些操作的原子性。
指令重排序的語(yǔ)句需遵守一個(gè)規(guī)則,即as-if-serial語(yǔ)義:
所有操作都可以為了優(yōu)化而重排序,但必須保證重排序的結(jié)果和程序執(zhí)行結(jié)果一致。
這里給出重排序的例子
public class Test { private static int x = 0, y = 0; private static int a = 0, b =0; public static void main(String[] args) throws InterruptedException { int i = 0; while(true) { x = 0; y = 0; a = 0; b = 0; i++; Thread first = new Thread(()->{a = 1;x = b;}); Thread second = new Thread(()->{b = 1;y = a;}); first.start();second.start(); first.join();second.join(); String result = "第" + i + "次 (" + x + "," + y + ")"; if(x == 0 && y == 0) { System.err.println(result); break; } else { System.out.println(result); } } }}一個(gè)線(xiàn)程執(zhí)行a = 1;x = b;,另一個(gè)線(xiàn)程執(zhí)行b = 1;y = a;,由于a、x,b、y不存在依賴(lài)關(guān)系,所以有可能發(fā)生先執(zhí)行x=b,然后a=1的指令重排序,經(jīng)試驗(yàn),在多次循環(huán)后出現(xiàn)x=b;b=1;y=a;a=1;的線(xiàn)程交替執(zhí)行結(jié)果。即x=0;y=0。

這說(shuō)明發(fā)生了指令重排序,將a,b,x,y用volatile修飾后,運(yùn)行多次也沒(méi)有出現(xiàn)重排序情況。 
單例模式中的“雙重檢查加鎖”模式如下所示
public class SingletonTest { private volatile static SingletonTest instance = null; private SingletonTest() { } public static SingletonTest getInstance() { if(instance == null) { synchronized (SingletonTest.class){ if(instance == null) { instance = new SingletonTest(); //非原子操作 } } } return instance; }}上面代碼大家都不陌生,可為什么instance一定要volatile修飾呢?這是由于instance = new SingletonTest();并不是一個(gè)原子操作。可分解為:
2操作依賴(lài)1操作,但3操作并不依賴(lài)2操作,也就是說(shuō),上述操作的順序可能為1-2-3,也可能為1-3-2,若是后者,當(dāng)instance不為空時(shí)也可能沒(méi)有正確初始化對(duì)象,而導(dǎo)致錯(cuò)誤。
參考
《深入理解java虛擬機(jī) 第2版》 java內(nèi)存模型FAQ深入理解Java內(nèi)存模型(一)——基礎(chǔ)Java內(nèi)存訪(fǎng)問(wèn)重排序的研究新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注