JVM內(nèi)存模型 - 主內(nèi)存和線程獨(dú)立的工作內(nèi)存
Java內(nèi)存模型規(guī)定简珠,對于多個線程共享的變量外盯,存儲在主內(nèi)存當(dāng)中,每個線程都有自己獨(dú)立的工作內(nèi)存挺尿,線程只能訪問自己的工作內(nèi)存奏黑,不可以訪問其它線程的工作內(nèi)存。工作內(nèi)存中保存了主內(nèi)存共享變量的副本编矾,線程要操作這些共享變量熟史,只能通過操作工作內(nèi)存中的副本來實現(xiàn),操作完畢之后再同步回到主內(nèi)存當(dāng)中窄俏。
如何保證多個線程操作主內(nèi)存的數(shù)據(jù)完整性是一個難題蹂匹,Java內(nèi)存模型也規(guī)定了工作內(nèi)存與主內(nèi)存之間交互的協(xié)議,首先是定義了8種原子操作:
(1) lock:將主內(nèi)存中的變量鎖定凹蜈,為一個線程所獨(dú)占
(2) unclock:將lock加的鎖定解除限寞,此時其它的線程可以有機(jī)會訪問此變量
(3) read:將主內(nèi)存中的變量值讀到工作內(nèi)存當(dāng)中
(4) load:將read讀取的值保存到工作內(nèi)存中的變量副本中。
(5) use:將值傳遞給線程的代碼執(zhí)行引擎
(6) assign:將執(zhí)行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲到主內(nèi)存中踪区。
(8) write:將store存儲的值寫入到主內(nèi)存的共享變量當(dāng)中昆烁。
- 內(nèi)存可見性
1.1 概念
通過上面Java內(nèi)存模型的概述,我們會注意到這么一個問題缎岗,每個線程在獲取鎖之后會在自己的工作內(nèi)存來操作共享變量静尼,操作完成之后將工作內(nèi)存中的副本回寫到主內(nèi)存,并且在其它線程從主內(nèi)存將變量同步回自己的工作內(nèi)存之前,共享變量的改變對其是不可見的鼠渺。
1.2 內(nèi)存可見性帶來的問題
很多時候我們需要一個線程對共享變量的改動鸭巴,其它線程也需要立即得知這個改動該怎么辦呢?比如以下的情景拦盹,有一個全局的狀態(tài)變量open:
boolean open=true;
這個變量用來描述對一個資源的打開關(guān)閉狀態(tài)鹃祖,true表示打開,false表示關(guān)閉普舆,假設(shè)有一個線程A,在執(zhí)行一些操作后將open修改為false:
//線程A
resource.close();
open = false;
線程B隨時關(guān)注open的狀態(tài)恬口,當(dāng)open為true的時候通過訪問資源來進(jìn)行一些操作:
//線程B
while(open) {
doSomethingWithResource(resource);
}
當(dāng)A把資源關(guān)閉的時候,open變量對線程B不可見沼侣,如果此時open變量的改動尚未同步到線程B的工作內(nèi)存中,那么線程B就會用一個已經(jīng)關(guān)閉了的資源去做一些操作祖能,因此產(chǎn)生錯誤。
1.3 volatile關(guān)鍵字
所以對于上面的情景蛾洛,要求一個線程對open的改變养铸,其他的線程能夠立即可見,Java為此提供了volatile關(guān)鍵字轧膘,在聲明open變量的時候加入volatile關(guān)鍵字就可以保證open的內(nèi)存可見性钞螟,即open的改變對所有的線程都是立即可見的。
volatile保證可見性的原理是在每次訪問變量時都會進(jìn)行一次刷新谎碍,因此每次訪問都是主內(nèi)存中最新的版本鳞滨。所以volatile關(guān)鍵字的作用之一就是保證變量修改的實時可見性。
- 指令重排
2.1 概念
指令重排序是JVM為了優(yōu)化指令椿浓,提高程序運(yùn)行效率太援。指令重排序包括編譯器重排序和運(yùn)行時重排序。JVM規(guī)范規(guī)定扳碍,指令重排序可以在不影響單線程程序執(zhí)行結(jié)果前提下進(jìn)行提岔。
2.2 指令重排帶來的問題
例子1:簡單指令重排
假設(shè)有這么兩個共享變量a和b:
private int a;
private int b;
在線程A中有兩條語句對這兩個共享變量進(jìn)行賦值操作:
a = 1;
b = 2;
假設(shè)當(dāng)線程A對a進(jìn)行復(fù)制操作的時候發(fā)現(xiàn)這個變量在主內(nèi)存已經(jīng)被其它的線程加了訪問鎖,那么此時線程A怎么辦笋敞?等待釋放鎖碱蒙?不,等待太浪費(fèi)時間了夯巷,它會去嘗試進(jìn)行b的賦值操作赛惩,b這時候沒被人占用,因此就會先為b賦值趁餐,再去為a賦值喷兼,那么執(zhí)行的順序就變成了:
b = 2;
a = 1;
例子2:A線程指令重排導(dǎo)致B線程出錯
對于在同一個線程內(nèi),這樣的改變是不會對邏輯產(chǎn)生影響的后雷,但是在多線程的情況下指令重排序會帶來問題季惯》透鳎看下面這個情景:
在線程A中:
context = loadContext();
inited = true;
在線程B中:
while(!inited ){ //根據(jù)線程A中對inited變量的修改決定是否使用context變量
sleep(100);
}
doSomethingwithconfig(context);
假設(shè)線程A中發(fā)生了指令重排序:
inited = true;
context = loadContext();
那么B中很可能就會拿到一個尚未初始化或尚未初始化完成的context,從而引發(fā)程序錯誤。
例子3:指令重排導(dǎo)致單例模式失效
我們都知道一個經(jīng)典的懶加載方式的單例模式:
public class Singleton {
private static Singleton instance = null;
private Singleton() { }
public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
看似簡單的一段賦值語句:instance = new Singleton();勉抓,其實JVM內(nèi)部已經(jīng)轉(zhuǎn)換為多條指令:
memory = allocate(); //1:分配對象的內(nèi)存空間
ctorInstance(memory); //2:初始化對象
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址
但是經(jīng)過重排序后如下:
memory = allocate(); //1:分配對象的內(nèi)存空間
instance = memory; //3:設(shè)置instance指向剛分配的內(nèi)存地址贾漏,此時對象還沒被初始化
ctorInstance(memory); //2:初始化對象
可以看到指令重排之后,instance指向分配好的內(nèi)存放在了前面藕筋,而這段內(nèi)存的初始化被排在了后面纵散,在線程A初始化完成這段內(nèi)存之前,線程B雖然進(jìn)不去同步代碼塊隐圾,但是在同步代碼塊之前的判斷就會發(fā)現(xiàn)instance不為空伍掀,此時線程B獲得instance對象進(jìn)行使用就可能發(fā)生錯誤。
2.3 volatile關(guān)鍵字
除了前面內(nèi)存可見性中講到的volatile關(guān)鍵字可以保證變量修改的可見性之外暇藏,還有另一個重要的作用:在JDK1.5之后硕盹,可以使用volatile變量禁止指令重排序。
例子2和例子3中的變量以關(guān)鍵字volatile修飾之后叨咖,就會組織JVM對其相關(guān)代碼進(jìn)行指令重排,這樣就能夠按照既定的順序指執(zhí)行啊胶。
總結(jié)
相對于synchronized塊的代碼鎖甸各,volatile應(yīng)該是提供了一個輕量級的針對共享變量的鎖,當(dāng)我們在多個線程間使用共享變量進(jìn)行通信的時候需要考慮將共享變量用volatile來修飾焰坪。