由于各種硬件及操作系統(tǒng)的內(nèi)存訪問差異,java虛擬機使用java內(nèi)存模型(java Memory Model,JMM)來規(guī)范java對內(nèi)存的訪問。這套模型在jdk 1.2中開始建立,經(jīng)jdk 1.5的修訂,現(xiàn)已逐步完善起來。
什么是java內(nèi)存模型,為什么會有這個模型?關(guān)于這個問題,就不得不從并發(fā)的問題講起。在多核系統(tǒng)中,處理器一般設(shè)置緩存來加速數(shù)據(jù)的讀取,緩存大大提升了程序性能,卻也帶來了“緩存一致性”的新問題。比如,當多個處理器寫同一塊主內(nèi)存時,以誰的緩存數(shù)據(jù)為準?讀取、寫入內(nèi)存的變量需遵循怎樣保證線程安全?針對這些問題,java設(shè)計了一套內(nèi)存模型以用來定義程序中各個變量的訪問規(guī)則。
java的內(nèi)存模型采用的是共享內(nèi)存的線程通信機制。線程之間的共享變量存儲在主內(nèi)存中,每個線程都有一個私有的本地內(nèi)存,本地內(nèi)存存儲了共享變量的副本。
圖片來自《深入理解java虛擬機 第2版》
關(guān)于共享變量,可以對應(yīng)為存儲在堆內(nèi)存的實例變量、類變量及數(shù)組元素(堆內(nèi)存是線程共享的)。私有變量可對應(yīng)虛擬機棧中的局部變量。事實上,他們是java內(nèi)存不同層次的劃分,并沒有一定聯(lián)系。
要完成主內(nèi)存與工作內(nèi)存的交互操作,需遵守一定的規(guī)則。java內(nèi)存模型定義了相當嚴謹而復(fù)雜的訪問規(guī)則。主要有8種原子性的操作。分別是:lock(鎖定)、unlock(解鎖)、read(讀取)、load(載入)、use(使用)、assign(賦值)、store(存儲)、write(寫入)。
內(nèi)存交互時,必須使用以上幾種操作搭配完成,且這8種操作要滿足一定規(guī)則。如read和load,store和write必須成對出現(xiàn);對變量實施use、store時,必須先執(zhí)行assign和load操作。
幸好,這些難以記憶的規(guī)則有一個等效判定的原則,即先行發(fā)生原則。
程序次序規(guī)則:在一個線程中,程序控制流前面的操作先行發(fā)生于后面的操作。監(jiān)視器鎖規(guī)則:一個unlock操作先行發(fā)生于對同一個鎖的lock操作。volatile變量規(guī)則:對于一個volatile變量,寫操作先行發(fā)生于對這個變量的讀操作。傳遞性:如果操作A先行發(fā)生于操作B,操作B先行發(fā)生于操作C,則操作A先行發(fā)生于操作C。我們知道java的多線程通信采用共享內(nèi)存的方式。線程對變量的所有操作都要在工作內(nèi)存中進行,不能直接訪問主內(nèi)存。線程間變量傳遞均需主內(nèi)存間接完成。
則,線程A要與線程B通信(比如B線程要讀取A線程經(jīng)操作后的值),需要:
線程A修改本地內(nèi)存A的值,并將其寫入主內(nèi)存的共享變量。線程B到主內(nèi)存讀取線程A修改后的值。前面我們提到的8種原子操作都是原子性的,這樣可以保證對基本數(shù)據(jù)類型的訪問讀寫是原子性的。這里有個例外是JVM沒有強制規(guī)定long、double一定是原子操作。但幾乎所有的商業(yè)JVM都實現(xiàn)了long、double的原子操作。
可見性是指,當一個線程修改了共享變量的值,其他變量能得知這個修改。
這里需要引出本文第二個關(guān)鍵點:volatile。volatile有兩個語義。這里用其可見性語義。經(jīng)volatile修飾的變量保證新值能立即同步到主內(nèi)存中,每次使用前立即從主內(nèi)存刷新。保證了多線程操作時變量的可見性。后面會有更詳細解釋。
除volatile外,synchronized和final也能實現(xiàn)可見性。 synchronized的可見性由“對一個變量執(zhí)行unlock前,必須先把此變量同步回主內(nèi)存”。獲得。
final關(guān)鍵字的可見性指:被final修飾的字段在構(gòu)造器中初始完成,則其他線程就能看到final的值。
java程序本身具有的有序性可以總結(jié)為:如果在同一線程觀察,所有操作都是有序的。而如果在一個線程觀察另一線程,所有操作都是無序的。前部分指在單線程環(huán)境中程序的順序性,后部分說的無序是指“指令的重排序”和“工作內(nèi)存與主內(nèi)存的同步延遲”。
編譯器能夠自由的以優(yōu)化的名義去改變指令順序。在特定的環(huán)境下,處理器可能會次序顛倒的執(zhí)行指令。是為指令的重排序。在單線程環(huán)境中,程序執(zhí)行結(jié)果不會受到指令重排序的影響。
但有時,我們在多線程情況下,并不希望發(fā)生指令重排序來影響并發(fā)結(jié)果。
java提供了volatile和synchronized來保證線程之間操作的有序性。volatile含有禁止指令重排序的語義(即它的第二個語義),synchronized規(guī)定一個變量在同一時刻只允許一條線程對其lock操作,也就是說同一個鎖的兩個同步塊只能串行進入。禁止了指令的重排序。
關(guān)于指令重排序,下文還有更多解釋。
介紹完java內(nèi)存模型的3個特征,現(xiàn)在來詳細介紹volatile及它代表的語義。
準確來說,volatile是java提供的輕量的同步機制。它有兩個特性: 1. 保證修飾的變量對所有線程的可見性。 2. 禁止指令的重排序優(yōu)化。
根據(jù)上面的介紹,我們對可見性及禁止重排序背后的順序性都不陌生。下面我們來詳細說明下。
volatile變量對所有線程是立即可見的,對volatile變量的寫操作都能立即反應(yīng)到其他線程中。
volatile boolean flag;public void shundown(){ flag = true;}public void doWork(){ while(!flag){ doSomething(); } }上面的例子即是volatile的典型應(yīng)用。任一線程調(diào)用了shundown()方法,都能保證所有線程執(zhí)行doWork()時doSomething()方法不執(zhí)行。
假設(shè)flag
不是由volatile修飾,則不能保證內(nèi)存可見性,當某個線程修改了flag
的值后,其他線程不一定會馬上看到或根本看不到,就會引起錯誤。
需注意的是,volatile變量保證可見性時,需滿足以下規(guī)則:
運算結(jié)果不依賴變量的當前值,或保證只有單一線程修改變量值。(如i++,運算依賴當前值,就不滿足)變量不需要與其他狀態(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++
的可見性。race++
操作實際上有1.讀取race的值;2.對race加1;3.修改race的值
3步操作,而volatile顯然不能保證這些操作的原子性。
指令重排序的語句需遵守一個規(guī)則,即as-if-serial語義:
所有操作都可以為了優(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); } } }}一個線程執(zhí)行a = 1;x = b;
,另一個線程執(zhí)行b = 1;y = a;
,由于a、x,b、y不存在依賴關(guān)系,所以有可能發(fā)生先執(zhí)行x=b
,然后a=1
的指令重排序,經(jīng)試驗,在多次循環(huán)后出現(xiàn)x=b;b=1;y=a;a=1;
的線程交替執(zhí)行結(jié)果。即x=0;y=0
。
這說明發(fā)生了指令重排序,將a,b,x,y用volatile修飾后,運行多次也沒有出現(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();
并不是一個原子操作。可分解為:
2操作依賴1操作,但3操作并不依賴2操作,也就是說,上述操作的順序可能為1-2-3,也可能為1-3-2,若是后者,當instance
不為空時也可能沒有正確初始化對象,而導致錯誤。
參考
《深入理解java虛擬機 第2版》 java內(nèi)存模型FAQ深入理解Java內(nèi)存模型(一)——基礎(chǔ)Java內(nèi)存訪問重排序的研究新聞熱點
疑難解答