java內(nèi)存模型(Java Memory Model架诞,JMM)是java虛擬機(jī)規(guī)范定義的垫释,用來(lái)屏蔽掉java程序在各種不同的硬件和操作系統(tǒng)對(duì)內(nèi)存的訪問(wèn)的差異拓挥,這樣就可以實(shí)現(xiàn)java程序在各種不同的平臺(tái)上都能達(dá)到內(nèi)存訪問(wèn)的一致性菇爪∷阈荆可以避免像c++等直接使用物理硬件和操作系統(tǒng)的內(nèi)存模型在不同操作系統(tǒng)和硬件平臺(tái)下表現(xiàn)不同,比如有些c/c++程序可能在windows平臺(tái)運(yùn)行正常凳宙,而在linux平臺(tái)卻運(yùn)行有問(wèn)題熙揍。
物理硬件和內(nèi)存
首先,在單核電腦中氏涩,處理問(wèn)題要簡(jiǎn)單的多届囚。對(duì)內(nèi)存和硬件的要求,各種方面的考慮沒(méi)有在多核的情況下復(fù)雜是尖。電腦中意系,CPU的運(yùn)行計(jì)算速度是非常快的饺汹,而其他硬件比如IO蛔添,網(wǎng)絡(luò)、內(nèi)存讀取等等兜辞,跟cpu的速度比起來(lái)是差幾個(gè)數(shù)量級(jí)的迎瞧。而不管任何操作,幾乎是不可能都在cpu中完成而不借助于任何其他硬件操作逸吵。所以協(xié)調(diào)cpu和各個(gè)硬件之間的速度差異是非常重要的凶硅,要不然cpu就一直在等待,浪費(fèi)資源扫皱。而在多核中足绅,不僅面臨如上問(wèn)題,還有如果多個(gè)核用到了同一個(gè)數(shù)據(jù)啸罢,如何保證數(shù)據(jù)的一致性编检、正確性等問(wèn)題,也是必須要解決的扰才。
目前基于高速緩存的存儲(chǔ)交互很好的解決了cpu和內(nèi)存等其他硬件之間的速度矛盾允懂,多核情況下各個(gè)處理器(核)都要遵循一定的諸如MSI、MESI等協(xié)議來(lái)保證內(nèi)存的各個(gè)處理器高速緩存和主內(nèi)存的數(shù)據(jù)的一致性衩匣。
除了增加高速緩存蕾总,為了使處理器內(nèi)部運(yùn)算單元盡可能被充分利用粥航,處理器還會(huì)對(duì)輸入的代碼進(jìn)行亂序執(zhí)行(Out-Of-Order Execution)優(yōu)化,處理器會(huì)在亂序執(zhí)行之后的結(jié)果進(jìn)行重組生百,保證結(jié)果的正確性递雀,也就是保證結(jié)果與順序執(zhí)行的結(jié)果一致。但是在真正的執(zhí)行過(guò)程中蚀浆,代碼執(zhí)行的順序并不一定按照代碼的書(shū)寫(xiě)順序來(lái)執(zhí)行缀程,可能和代碼的書(shū)寫(xiě)順序不同。
java內(nèi)存模型
雖然java程序所有的運(yùn)行都是在虛擬機(jī)中市俊,涉及到的內(nèi)存等信息都是虛擬機(jī)的一部分杨凑,但實(shí)際也是物理機(jī)的,只不過(guò)是虛擬機(jī)作為最外層的容器統(tǒng)一做了處理摆昧。虛擬機(jī)的內(nèi)存模型撩满,以及多線程的場(chǎng)景下與物理機(jī)的情況是很相似的,可以類(lèi)比參考绅你。
Java內(nèi)存模型的主要目標(biāo)是定義程序中變量的訪問(wèn)規(guī)則伺帘。即在虛擬機(jī)中將變量存儲(chǔ)到主內(nèi)存或者將變量從主內(nèi)存取出這樣的底層細(xì)節(jié)。需要注意的是這里的變量跟我們寫(xiě)java程序中的變量不是完全等同的忌锯。這里的變量是指實(shí)例字段伪嫁,靜態(tài)字段,構(gòu)成數(shù)組對(duì)象的元素汉规,但是不包括局部變量和方法參數(shù)(因?yàn)檫@是線程私有的)礼殊。這里可以簡(jiǎn)單的認(rèn)為主內(nèi)存是java虛擬機(jī)內(nèi)存區(qū)域中的堆,局部變量和方法參數(shù)是在虛擬機(jī)棧中定義的针史。但是在堆中的變量如果在多線程中都使用晶伦,就涉及到了堆和不同虛擬機(jī)棧中變量的值的一致性問(wèn)題了。
Java內(nèi)存模型中涉及到的概念有:
- 主內(nèi)存:java虛擬機(jī)規(guī)定所有的變量(不是程序中的變量)都必須在主內(nèi)存中產(chǎn)生啄枕,為了方便理解婚陪,可以認(rèn)為是堆區(qū)∑底#可以與前面說(shuō)的物理機(jī)的主內(nèi)存相比泌参,只不過(guò)物理機(jī)的主內(nèi)存是整個(gè)機(jī)器的內(nèi)存,而虛擬機(jī)的主內(nèi)存是虛擬機(jī)內(nèi)存中的一部分常空。
- 工作內(nèi)存:java虛擬機(jī)中每個(gè)線程都有自己的工作內(nèi)存沽一,該內(nèi)存是線程私有的為了方便理解,可以認(rèn)為是虛擬機(jī)棧漓糙∠巢可以與前面說(shuō)的高速緩存相比。線程的工作內(nèi)存保存了線程需要的變量在主內(nèi)存中的副本。虛擬機(jī)規(guī)定蝗蛙,線程對(duì)主內(nèi)存變量的修改必須在線程的工作內(nèi)存中進(jìn)行蝇庭,不能直接讀寫(xiě)主內(nèi)存中的變量。不同的線程之間也不能相互訪問(wèn)對(duì)方的工作內(nèi)存捡硅。如果線程之間需要傳遞變量的值哮内,必須通過(guò)主內(nèi)存來(lái)作為中介進(jìn)行傳遞。
這里需要說(shuō)明一下:主內(nèi)存壮韭、工作內(nèi)存與java內(nèi)存區(qū)域中的java堆北发、虛擬機(jī)棧、方法區(qū)并不是一個(gè)層次的內(nèi)存劃分泰涂。這兩者是基本上是沒(méi)有關(guān)系的鲫竞,上文只是為了便于理解,做的類(lèi)比
工作內(nèi)存與主內(nèi)存交互
物理機(jī)高速緩存和主內(nèi)存之間的交互有協(xié)議逼蒙,同樣的,java內(nèi)存中線程的工作內(nèi)存和主內(nèi)存的交互是由java虛擬機(jī)定義了如下的8種操作來(lái)完成的寄疏,每種操作必須是原子性的(double和long類(lèi)型在某些平臺(tái)有例外是牢,參考volatile詳解和非原子性協(xié)定)
java虛擬機(jī)中主內(nèi)存和工作內(nèi)存交互,就是一個(gè)變量如何從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存中陕截,如何把修改后的變量從工作內(nèi)存同步回主內(nèi)存驳棱。
- lock(鎖定):作用于主內(nèi)存的變量,一個(gè)變量在同一時(shí)間只能一個(gè)線程鎖定农曲,該操作表示這條線成獨(dú)占這個(gè)變量
- unlock(解鎖):作用于主內(nèi)存的變量社搅,表示這個(gè)變量的狀態(tài)由處于鎖定狀態(tài)被釋放溢吻,這樣其他線程才能對(duì)該變量進(jìn)行鎖定
- read(讀取):作用于主內(nèi)存變量出革,表示把一個(gè)主內(nèi)存變量的值傳輸?shù)骄€程的工作內(nèi)存剿配,以便隨后的load操作使用
- load(載入):作用于線程的工作內(nèi)存的變量摘盆,表示把read操作從主內(nèi)存中讀取的變量的值放到工作內(nèi)存的變量副本中(副本是相對(duì)于主內(nèi)存的變量而言的)
- use(使用):作用于線程的工作內(nèi)存中的變量贱枣,表示把工作內(nèi)存中的一個(gè)變量的值傳遞給執(zhí)行引擎介返,每當(dāng)虛擬機(jī)遇到一個(gè)需要使用變量的值的字節(jié)碼指令時(shí)就會(huì)執(zhí)行該操作
- assign(賦值):作用于線程的工作內(nèi)存的變量拳氢,表示把執(zhí)行引擎返回的結(jié)果賦值給工作內(nèi)存中的變量缩搅,每當(dāng)虛擬機(jī)遇到一個(gè)給變量賦值的字節(jié)碼指令時(shí)就會(huì)執(zhí)行該操作
- store(存儲(chǔ)):作用于線程的工作內(nèi)存中的變量冻辩,把工作內(nèi)存中的一個(gè)變量的值傳遞給主內(nèi)存猖腕,以便隨后的write操作使用
- write(寫(xiě)入):作用于主內(nèi)存的變量,把store操作從工作內(nèi)存中得到的變量的值放入主內(nèi)存的變量中
如果要把一個(gè)變量從主內(nèi)存?zhèn)鬏數(shù)焦ぷ鲀?nèi)存恨闪,那就要順序的執(zhí)行read和load操作倘感,如果要把一個(gè)變量從工作內(nèi)存回寫(xiě)到主內(nèi)存,就要順序的執(zhí)行store和write操作咙咽。對(duì)于普通變量老玛,虛擬機(jī)只是要求順序的執(zhí)行,并沒(méi)有要求連續(xù)的執(zhí)行,所以如下也是正確的逻炊。對(duì)于兩個(gè)線程互亮,分別從主內(nèi)存中讀取變量a和b的值,并不一樣要read a; load a; read b; load b; 也會(huì)出現(xiàn)如下執(zhí)行順序:read a; read b; load b; load a; (對(duì)于volatile修飾的變量會(huì)有一些其他規(guī)則,后邊會(huì)詳細(xì)列出)余素,對(duì)于這8中操作豹休,虛擬機(jī)也規(guī)定了一系列規(guī)則,在執(zhí)行這8中操作的時(shí)候必須遵循如下的規(guī)則:
- 不允許read和load桨吊、store和write操作之一單獨(dú)出現(xiàn)威根,也就是不允許從主內(nèi)存讀取了變量的值但是工作內(nèi)存不接收的情況,或者不允許從工作內(nèi)存將變量的值回寫(xiě)到主內(nèi)存但是主內(nèi)存不接收的情況
- 不允許一個(gè)線程丟棄最近的assign操作视乐,也就是不允許線程在自己的工作線程中修改了變量的值卻不同步/回寫(xiě)到主內(nèi)存
- 不允許一個(gè)線程回寫(xiě)沒(méi)有修改的變量到主內(nèi)存洛搀,也就是如果線程工作內(nèi)存中變量沒(méi)有發(fā)生過(guò)任何assign操作,是不允許將該變量的值回寫(xiě)到主內(nèi)存
- 變量只能在主內(nèi)存中產(chǎn)生佑淀,不允許在工作內(nèi)存中直接使用一個(gè)未被初始化的變量留美,也就是沒(méi)有執(zhí)行l(wèi)oad或者assign操作。也就是說(shuō)在執(zhí)行use伸刃、store之前必須對(duì)相同的變量執(zhí)行了load谎砾、assign操作
- 一個(gè)變量在同一時(shí)刻只能被一個(gè)線程對(duì)其進(jìn)行l(wèi)ock操作,也就是說(shuō)一個(gè)線程一旦對(duì)一個(gè)變量加鎖后捧颅,在該線程沒(méi)有釋放掉鎖之前景图,其他線程是不能對(duì)其加鎖的,但是同一個(gè)線程對(duì)一個(gè)變量加鎖后碉哑,可以繼續(xù)加鎖挚币,同時(shí)在釋放鎖的時(shí)候釋放鎖次數(shù)必須和加鎖次數(shù)相同。
- 對(duì)變量執(zhí)行l(wèi)ock操作扣典,就會(huì)清空工作空間該變量的值妆毕,執(zhí)行引擎使用這個(gè)變量之前,需要重新load或者assign操作初始化變量的值
- 不允許對(duì)沒(méi)有l(wèi)ock的變量執(zhí)行unlock操作激捏,如果一個(gè)變量沒(méi)有被lock操作设塔,那也不能對(duì)其執(zhí)行unlock操作,當(dāng)然一個(gè)線程也不能對(duì)被其他線程lock的變量執(zhí)行unlock操作
- 對(duì)一個(gè)變量執(zhí)行unlock之前远舅,必須先把變量同步回主內(nèi)存中闰蛔,也就是執(zhí)行store和write操作
當(dāng)然,最重要的還是如開(kāi)始所說(shuō)图柏,這8個(gè)動(dòng)作必須是原子的序六,不可分割的。
針對(duì)volatile修飾的變量蚤吹,會(huì)有一些特殊規(guī)定例诀。
volatile修飾的變量的特殊規(guī)則
關(guān)鍵字volatile可以說(shuō)是java虛擬機(jī)中提供的最輕量級(jí)的同步機(jī)制随抠。java內(nèi)存模型對(duì)volatile專(zhuān)門(mén)定義了一些特殊的訪問(wèn)規(guī)則。這些規(guī)則有些晦澀拗口繁涂,先列出規(guī)則拱她,然后用更加通俗易懂的語(yǔ)言來(lái)解釋?zhuān)?br> 假定T表示一個(gè)線程,V和W分別表示兩個(gè)volatile修飾的變量扔罪,那么在進(jìn)行read秉沼、load、use矿酵、assign唬复、store和write操作的時(shí)候需要滿(mǎn)足如下規(guī)則:
- 只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是load,線程T對(duì)變量V才能執(zhí)行use動(dòng)作全肮;同時(shí)只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是use的時(shí)候線程T對(duì)變量V才能執(zhí)行l(wèi)oad操作敞咧。所以,線程T對(duì)變量V的use動(dòng)作和線程T對(duì)變量V的read辜腺、load動(dòng)作相關(guān)聯(lián)休建,必須是連續(xù)一起出現(xiàn)。也就是在線程T的工作內(nèi)存中哪自,每次使用變量V之前必須從主內(nèi)存去重新獲取最新的值丰包,用于保證線程T能看得見(jiàn)其他線程對(duì)變量V的最新的修改后的值。
- 只有當(dāng)線程T對(duì)變量V執(zhí)行的前一個(gè)動(dòng)作是assign的時(shí)候壤巷,線程T對(duì)變量V才能執(zhí)行store動(dòng)作;同時(shí)只有當(dāng)線程T對(duì)變量V執(zhí)行的后一個(gè)動(dòng)作是store的時(shí)候瞧毙,線程T對(duì)變量V才能執(zhí)行assign動(dòng)作胧华。所以,線程T對(duì)變量V的assign操作和線程T對(duì)變量V的store宙彪、write動(dòng)作相關(guān)聯(lián)矩动,必須一起連續(xù)出現(xiàn)。也即是在線程T的工作內(nèi)存中释漆,每次修改變量V之后必須立刻同步回主內(nèi)存悲没,用于保證線程T對(duì)變量V的修改能立刻被其他線程看到。
- 假定動(dòng)作A是線程T對(duì)變量V實(shí)施的use或assign動(dòng)作男图,動(dòng)作F是和動(dòng)作A相關(guān)聯(lián)的load或store動(dòng)作示姿,動(dòng)作P是和動(dòng)作F相對(duì)應(yīng)的對(duì)變量V的read或write動(dòng)作;類(lèi)似的逊笆,假定動(dòng)作B是線程T對(duì)變量W實(shí)施的use或assign動(dòng)作栈戳,動(dòng)作G是和動(dòng)作B相關(guān)聯(lián)的load或store動(dòng)作,動(dòng)作Q是和動(dòng)作G相對(duì)應(yīng)的對(duì)變量W的read或write動(dòng)作难裆。如果動(dòng)作A先于B子檀,那么P先于Q镊掖。也就是說(shuō)在同一個(gè)線程內(nèi)部,被volatile修飾的變量不會(huì)被指令重排序褂痰,保證代碼的執(zhí)行順序和程序的順序相同亩进。
總結(jié)上面三條規(guī)則,前面兩條可以概括為:volatile類(lèi)型的變量保證對(duì)所有線程的可見(jiàn)性缩歪。第三條為:volatile類(lèi)型的變量禁止指令重排序優(yōu)化归薛。
-
valatile類(lèi)型的變量保證對(duì)所有線程的可見(jiàn)性
可見(jiàn)性是指當(dāng)一個(gè)線程修改了這個(gè)變量的值,新值(修改后的值)對(duì)于其他線程來(lái)說(shuō)是立即可以得知的驶冒。正如上面的前兩條規(guī)則規(guī)定苟翻,volatile類(lèi)型的變量每次值被修改了就立即同步回主內(nèi)存,每次使用時(shí)就需要從主內(nèi)存重新讀取值骗污。返回到前面對(duì)普通變量的規(guī)則中崇猫,并沒(méi)有要求這一點(diǎn),所以普通變量的值是不會(huì)立即對(duì)所有線程可見(jiàn)的需忿。
誤解:volatile變量對(duì)所有線程是立即可見(jiàn)的诅炉,所以對(duì)volatile變量的所有修改(寫(xiě)操作)都立刻能反應(yīng)到其他線程中∥堇澹或者換句話(huà)說(shuō):volatile變量在各個(gè)線程中是一致的涕烧,所以基于volatile變量的運(yùn)算在并發(fā)下是線程安全的。
這個(gè)觀點(diǎn)的論據(jù)是正確的汗洒,但是根據(jù)論據(jù)得出的結(jié)論是錯(cuò)誤的议纯,并不能得出這樣的結(jié)論。volatile的規(guī)則溢谤,保證了read瞻凤、load、use的順序和連續(xù)行世杀,同理assign阀参、store、write也是順序和連續(xù)的瞻坝。也就是這幾個(gè)動(dòng)作是原子性的蛛壳,但是對(duì)變量的修改,或者對(duì)變量的運(yùn)算所刀,卻不能保證是原子性的衙荐。如果對(duì)變量的修改是分為多個(gè)步驟的,那么多個(gè)線程同時(shí)從主內(nèi)存拿到的值是最新的勉痴,但是經(jīng)過(guò)多步運(yùn)算后回寫(xiě)到主內(nèi)存的值是有可能存在覆蓋情況發(fā)生的赫模。如下代碼的例子:
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++
}
private static final int THREADS_COUNT = 20;
public void static main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT);
for (int = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable(){
@Override
public void run() {
for (int j = 0; j < 10000; j++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount() > 1) {
Thread.yield();
}
System.out.println(race);
}
}
代碼就是對(duì)volatile類(lèi)型的變量啟動(dòng)了20個(gè)線程,每個(gè)線程對(duì)變量執(zhí)行1w次加1操作蒸矛,如果volatile變量并發(fā)操作沒(méi)有問(wèn)題的話(huà)瀑罗,那么結(jié)果應(yīng)該是輸出20w胸嘴,但是結(jié)果運(yùn)行的時(shí)候每次都是小于20w,這就是因?yàn)?code>race++操作不是原子性的斩祭,是分多個(gè)步驟完成的劣像。假設(shè)兩個(gè)線程a、b同時(shí)取到了主內(nèi)存的值摧玫,是0耳奕,這是沒(méi)有問(wèn)題的,在進(jìn)行++
操作的時(shí)候假設(shè)線程a執(zhí)行到一半诬像,線程b執(zhí)行完了屋群,這時(shí)線程b立即同步給了主內(nèi)存,主內(nèi)存的值為1坏挠,而線程a此時(shí)也執(zhí)行完了芍躏,同步給了主內(nèi)存,此時(shí)的值仍然是1降狠,線程b的結(jié)果被覆蓋掉了对竣。
-
volatile變量禁止指令重排序優(yōu)化
普通的變量?jī)H僅會(huì)保證在該方法執(zhí)行的過(guò)程中,所有依賴(lài)賦值結(jié)果的地方都能獲取到正確的結(jié)果榜配,但不能保證變量賦值的操作順序和程序代碼的順序一致否纬。因?yàn)樵谝粋€(gè)線程的方法執(zhí)行過(guò)程中無(wú)法感知到這一點(diǎn),這也就是java內(nèi)存模型中描述的所謂的“線程內(nèi)部表現(xiàn)為串行的語(yǔ)義”蛋褥。
也就是在單線程內(nèi)部临燃,我們看到的或者感知到的結(jié)果和代碼順序是一致的,即使代碼的執(zhí)行順序和代碼順序不一致烙心,但是在需要賦值的時(shí)候結(jié)果也是正確的谬俄,所以看起來(lái)就是串行的。但實(shí)際結(jié)果有可能代碼的執(zhí)行順序和代碼順序是不一致的弃理。這在多線程中就會(huì)出現(xiàn)問(wèn)題。
看下面的偽代碼舉例:
Map configOptions;
char[] configText;
//volatile類(lèi)型bianliang
volatile boolean initialized = false;
//假設(shè)以下代碼在線程A中執(zhí)行
//模擬讀取配置信息屎蜓,讀取完成后認(rèn)為是初始化完成
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
//假設(shè)以下代碼在線程B中執(zhí)行
//等待initialized為true后痘昌,讀取配置信息進(jìn)行操作
while ( !initialized) {
sleep();
}
doSomethingWithConfig();
如果initialiezd是普通變量,沒(méi)有被volatile修飾炬转,那么線程A執(zhí)行的代碼的修改初始化完成的結(jié)果initialized = true
就有可能先于之前的三行代碼執(zhí)行辆苔,而此時(shí)線程B發(fā)現(xiàn)initialized為true了,就執(zhí)行doSomethingWithConfig()
方法扼劈,但是里面的配置信息都是null的驻啤,就會(huì)出現(xiàn)問(wèn)題了。
現(xiàn)在initialized是volatile類(lèi)型變量荐吵,保證禁止代碼重排序優(yōu)化骑冗,那么就可以保證initialized = true
執(zhí)行的時(shí)候赊瞬,前邊的三行代碼一定執(zhí)行完成了,那么線程B讀取的配置文件信息就是正確的贼涩。
跟其他保證并發(fā)安全的工具相比巧涧,volatile的性能確實(shí)會(huì)好一些。在某些情況下遥倦,volatile的同步機(jī)制性能要優(yōu)于鎖(使用synchronized關(guān)鍵字或者java.util.concurrent包中的鎖)谤绳。但是現(xiàn)在由于虛擬機(jī)對(duì)鎖的不斷優(yōu)化和實(shí)行的許多消除動(dòng)作,很難有一個(gè)量化的比較袒哥。
與自己相比缩筛,就可以確定一個(gè)原則:volatile變量的讀操作和普通變量的讀操作幾乎沒(méi)有差異,但是寫(xiě)操作會(huì)性能差一些堡称,慢一些瞎抛,因?yàn)橐诒镜卮a中插入許多內(nèi)存屏障指令來(lái)禁止指令重排序,保證處理器不發(fā)生代碼亂序執(zhí)行行為粮呢。
long和double變量的特殊規(guī)則
Java內(nèi)存模型要求對(duì)主內(nèi)存和工作內(nèi)存交換的八個(gè)動(dòng)作是原子的婿失,正如章節(jié)開(kāi)頭所講,對(duì)long和double有一些特殊規(guī)則啄寡。八個(gè)動(dòng)作中l(wèi)ock豪硅、unlock、read挺物、load懒浮、use、assign识藤、store砚著、write對(duì)待32位的基本數(shù)據(jù)類(lèi)型都是原子操作,對(duì)待long和double這兩個(gè)64位的數(shù)據(jù)痴昧,java虛擬機(jī)規(guī)范對(duì)java內(nèi)存模型的規(guī)定中特別定義了一條相對(duì)寬松的規(guī)則:允許虛擬機(jī)將沒(méi)有被volatile修飾的64位數(shù)據(jù)的讀寫(xiě)操作劃分為兩次32位的操作來(lái)進(jìn)行稽穆,也就是允許虛擬機(jī)不保證對(duì)64位數(shù)據(jù)的read、load赶撰、store和write這4個(gè)動(dòng)作的操作是原子的舌镶。這也就是我們常說(shuō)的long和double的非原子性協(xié)定(Nonautomic Treatment of double and long Variables)。
并發(fā)內(nèi)存模型的實(shí)質(zhì)
Java內(nèi)存模型圍繞著并發(fā)過(guò)程中如何處理原子性豪娜、可見(jiàn)性和順序性這三個(gè)特征來(lái)設(shè)計(jì)的餐胀。
原子性(Automicity)
由Java內(nèi)存模型來(lái)直接保證原子性的變量操作包括read、load瘤载、use否灾、assign、store鸣奔、write這6個(gè)動(dòng)作墨技,雖然存在long和double的特例惩阶,但基本可以忽律不計(jì),目前虛擬機(jī)基本都對(duì)其實(shí)現(xiàn)了原子性健提。如果需要更大范圍的控制琳猫,lock和unlock也可以滿(mǎn)足需求。lock和unlock雖然沒(méi)有被虛擬機(jī)直接開(kāi)給用戶(hù)使用私痹,但是提供了字節(jié)碼層次的指令monitorenter和monitorexit對(duì)應(yīng)這兩個(gè)操作脐嫂,對(duì)應(yīng)到j(luò)ava代碼就是synchronized關(guān)鍵字,因此在synchronized塊之間的代碼都具有原子性紊遵。
可見(jiàn)性
可見(jiàn)性是指一個(gè)線程修改了一個(gè)變量的值后账千,其他線程立即可以感知到這個(gè)值的修改。正如前面所說(shuō)暗膜,volatile類(lèi)型的變量在修改后會(huì)立即同步給主內(nèi)存匀奏,在使用的時(shí)候會(huì)從主內(nèi)存重新讀取,是依賴(lài)主內(nèi)存為中介來(lái)保證多線程下變量對(duì)其他線程的可見(jiàn)性的学搜。
除了volatile娃善,synchronized和final也可以實(shí)現(xiàn)可見(jiàn)性。synchronized關(guān)鍵字是通過(guò)unlock之前必須把變量同步回主內(nèi)存來(lái)實(shí)現(xiàn)的瑞佩,final則是在初始化后就不會(huì)更改聚磺,所以只要在初始化過(guò)程中沒(méi)有把this指針傳遞出去也能保證對(duì)其他線程的可見(jiàn)性。
有序性
有序性從不同的角度來(lái)看是不同的炬丸。單純單線程來(lái)看都是有序的瘫寝,但到了多線程就會(huì)跟我們預(yù)想的不一樣〕砭妫可以這么說(shuō):如果在本線程內(nèi)部觀察焕阿,所有操作都是有序的;如果在一個(gè)線程中觀察另一個(gè)線程首启,所有的操作都是無(wú)序的暮屡。前半句說(shuō)的就是“線程內(nèi)表現(xiàn)為串行的語(yǔ)義”,后半句值得是“指令重排序”現(xiàn)象和主內(nèi)存與工作內(nèi)存之間同步存在延遲的現(xiàn)象毅桃。
保證有序性的關(guān)鍵字有volatile和synchronized栽惶,volatile禁止了指令重排序,而synchronized則由“一個(gè)變量在同一時(shí)刻只能被一個(gè)線程對(duì)其進(jìn)行l(wèi)ock操作”來(lái)保證疾嗅。
總體來(lái)看,synchronized對(duì)三種特性都有支持冕象,雖然簡(jiǎn)單代承,但是如果無(wú)控制的濫用對(duì)性能就會(huì)產(chǎn)生較大影響。
先行發(fā)生原則
如果Java內(nèi)存模型中所有的有序性都要依靠volatile和synchronized來(lái)實(shí)現(xiàn)渐扮,那是不是非常繁瑣论悴。Java語(yǔ)言中有一個(gè)“先行發(fā)生原則”掖棉,是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)、線程是否安全的主要依據(jù)膀估。
什么是先行發(fā)生原則
先行發(fā)生原則是Java內(nèi)存模型中定義的兩個(gè)操作之間的偏序關(guān)系幔亥。比如說(shuō)操作A先行發(fā)生于操作B,那么在B操作發(fā)生之前察纯,A操作產(chǎn)生的“影響”都會(huì)被操作B感知到帕棉。這里的影響是指修改了內(nèi)存中的共享變量、發(fā)送了消息饼记、調(diào)用了方法等香伴。個(gè)人覺(jué)得更直白一些就是有可能對(duì)操作B的結(jié)果有影響的都會(huì)被B感知到,對(duì)B操作的結(jié)果沒(méi)有影響的是否感知到?jīng)]有太大關(guān)系具则。
Java內(nèi)存模型自帶先行發(fā)生原則有哪些
- 程序次序原則
在一個(gè)線程內(nèi)部即纲,按照代碼的順序,書(shū)寫(xiě)在前面的先行發(fā)生與后邊的博肋〉驼或者更準(zhǔn)確的說(shuō)是在控制流順序前面的先行發(fā)生與控制流后面的,而不是代碼順序匪凡,因?yàn)闀?huì)有分支膊畴、跳轉(zhuǎn)、循環(huán)等锹雏。 - 管程鎖定規(guī)則
一個(gè)unlock操作先行發(fā)生于后面對(duì)同一個(gè)鎖的lock操作巴比。這里必須注意的是對(duì)同一個(gè)鎖,后面是指時(shí)間上的后面 - volatile變量規(guī)則
對(duì)一個(gè)volatile變量的寫(xiě)操作先行發(fā)生與后面對(duì)這個(gè)變量的讀操作礁遵,這里的后面是指時(shí)間上的先后順序 - 線程啟動(dòng)規(guī)則
Thread對(duì)象的start()方法先行發(fā)生與該線程的每個(gè)動(dòng)作轻绞。當(dāng)然如果你錯(cuò)誤的使用了線程,創(chuàng)建線程后沒(méi)有執(zhí)行start方法佣耐,而是執(zhí)行run方法政勃,那此句話(huà)是不成立的,但是如果這樣其實(shí)也不是線程了 - 線程終止規(guī)則
線程中的所有操作都先行發(fā)生與對(duì)此線程的終止檢測(cè)兼砖,可以通過(guò)Thread.join()和Thread.isAlive()的返回值等手段檢測(cè)線程是否已經(jīng)終止執(zhí)行 - 線程中斷規(guī)則
對(duì)線程interrupt()方法的調(diào)用先行發(fā)生于被中斷線程的代碼檢測(cè)到中斷事件的發(fā)生奸远,可以通過(guò)Thread.interrupted()方法檢測(cè)到是否有中斷發(fā)生。 - 對(duì)象終結(jié)規(guī)則
一個(gè)對(duì)象的初始化完成先行發(fā)生于他的finalize方法的執(zhí)行讽挟,也就是初始化方法先行發(fā)生于finalize方法 - 傳遞性
如果操作A先行發(fā)生于操作B懒叛,操作B先行發(fā)生于操作C,那么操作A先行發(fā)生于操作C耽梅。
看一個(gè)例子:
private int value = 0;
public void setValue(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
如果有兩個(gè)線程A和B薛窥,A先調(diào)用setValue方法,然后B調(diào)用getValue方法,那么B線程執(zhí)行方法返回的結(jié)果是什么诅迷?
我們?nèi)?duì)照先行發(fā)生原則一個(gè)一個(gè)對(duì)比佩番。首先是程序次序規(guī)則,這里是多線程罢杉,不在一個(gè)線程中趟畏,不適用;然后是管程鎖定規(guī)則滩租,這里沒(méi)有synchronized赋秀,自然不會(huì)發(fā)生lock和unlock,不適用持际;后面對(duì)于線程啟動(dòng)規(guī)則沃琅、線程終止規(guī)則、線程中斷規(guī)則也不適用蜘欲,這里與對(duì)象終結(jié)規(guī)則益眉、傳遞性規(guī)則也沒(méi)有關(guān)系。所以說(shuō)B返回的結(jié)果是不確定的姥份,也就是說(shuō)在多線程環(huán)境下該操作不是線程安全的郭脂。
如何修改呢,一個(gè)是對(duì)get/set方法加入synchronized 關(guān)鍵字澈歉,可以使用管程鎖定規(guī)則展鸡;要么對(duì)value加volatile修飾,可以使用volatile變量規(guī)則埃难。
通過(guò)上面的例子可知莹弊,一個(gè)操作時(shí)間上先發(fā)生并不代表這個(gè)操作先行發(fā)生,那么一個(gè)操作先行發(fā)生是不是代表這個(gè)操作在時(shí)間上先發(fā)生涡尘?也不是忍弛,如下面的例子:
int i = 2;
int j = 1;
在同一個(gè)線程內(nèi),對(duì)i的賦值先行發(fā)生于對(duì)j賦值的操作考抄,但是代碼重排序優(yōu)化细疚,也有可能是j的賦值先發(fā)生,我們無(wú)法感知到這一變化川梅。
所以疯兼,綜上所述,時(shí)間先后順序與先行發(fā)生原則之間基本沒(méi)有太大關(guān)系贫途。我們衡量并發(fā)安全的問(wèn)題的時(shí)候不要受到時(shí)間先后順序的干擾吧彪,一切以先行發(fā)生原則為準(zhǔn)。