Java的并發(fā)采用的是共享內(nèi)存模型(而非消息傳遞模型)瘟滨,線程之間共享程序的公共狀態(tài),線程之間通過(guò)寫(xiě)-讀內(nèi)存中的公共狀態(tài)來(lái)隱式進(jìn)行通信铡溪。多個(gè)線程之間是不能直接傳遞數(shù)據(jù)交互的瓷胧,它們之間的交互只能通過(guò)共享變量來(lái)實(shí)現(xiàn)
同步是顯式進(jìn)行的趟庄。程序員必須顯式指定某個(gè)方法或某段代碼需要在線程之間互斥執(zhí)行。
Java線程之間的通信由Java內(nèi)存模型(JMM)控制岔激,JMM決定一個(gè)線程對(duì)共享變量的寫(xiě)入何時(shí)對(duì)另一個(gè)線程可見(jiàn)。
從抽象的角度來(lái)看是掰,JMM定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲(chǔ)在主內(nèi)存(main memory)中虑鼎,每個(gè)線程都有一個(gè)私有的本地內(nèi)存(local memory),本地內(nèi)存中存儲(chǔ)了該線程以讀/寫(xiě)共享變量的副本键痛。本地內(nèi)存是JMM的一個(gè)抽象概念炫彩,并不真實(shí)存在,它涵蓋了緩存絮短,寫(xiě)緩沖區(qū)江兢,寄存器以及其他的硬件和編譯器優(yōu)化。Java內(nèi)存模型的抽象示意圖如下:
線程間通信的步驟:
首先丁频,線程A把本地內(nèi)存A中更新過(guò)的共享變量刷新到主內(nèi)存中去杉允。
然后,線程B到主內(nèi)存中去讀取線程A之前已更新過(guò)的共享變量席里。
本地內(nèi)存A和B有主內(nèi)存中共享變量x的副本叔磷。
假設(shè)初始時(shí),這三個(gè)內(nèi)存中的x值都為0奖磁。線程A在執(zhí)行時(shí)改基,把更新后的x值(假設(shè)值為1)臨時(shí)存放在自己的本地內(nèi)存A中。
當(dāng)線程A和線程B需要通信時(shí)(如何激發(fā)咖为?--隱式)秕狰,線程A首先會(huì)把自己本地內(nèi)存中修改后的x值刷新到主內(nèi)存中,此時(shí)主內(nèi)存中的x值變?yōu)榱?躁染。
隨后鸣哀,線程B到主內(nèi)存中去讀取線程A更新后的x值,此時(shí)線程B的本地內(nèi)存的x值也變?yōu)榱?吞彤。
從整體來(lái)看我衬,這兩個(gè)步驟實(shí)質(zhì)上是線程A在向線程B發(fā)送消息,而且這個(gè)通信過(guò)程必須要經(jīng)過(guò)主內(nèi)存备畦。JMM通過(guò)控制主內(nèi)存與每個(gè)線程的本地內(nèi)存之間的交互低飒,來(lái)為java程序員提供內(nèi)存可見(jiàn)性保證。
例如在多個(gè)線程之間共享了Count類的一個(gè)對(duì)象褥赊,這個(gè)對(duì)象是被創(chuàng)建在主內(nèi)存(堆內(nèi)存)中,每個(gè)線程都有自己的本地內(nèi)存(線程棧)莉恼,工作內(nèi)存存儲(chǔ)了主內(nèi)存Count對(duì)象的一個(gè)副本拌喉,當(dāng)線程操作Count對(duì)象時(shí)速那,首先從主內(nèi)存復(fù)制Count對(duì)象到工作內(nèi)存中,然后執(zhí)行代碼count.count()尿背,改變了num值端仰,最后用工作內(nèi)存Count刷新主內(nèi)存Count。
當(dāng)一個(gè)對(duì)象在多個(gè)內(nèi)存中都存在副本時(shí)田藐,如果一個(gè)內(nèi)存修改了共享變量荔烧,其它線程也應(yīng)該能夠看到被修改后的值,此為可見(jiàn)性汽久。
一個(gè)運(yùn)算賦值操作并不是一個(gè)原子性操作鹤竭,多個(gè)線程執(zhí)行時(shí),CPU對(duì)線程的調(diào)度是隨機(jī)的景醇,我們不知道當(dāng)前程序被執(zhí)行到哪步就切換到了下一個(gè)線程臀稚,一個(gè)最經(jīng)典的例子就是銀行匯款問(wèn)題,一個(gè)銀行賬戶存款100三痰,這時(shí)一個(gè)人從該賬戶取10元吧寺,同時(shí)另一個(gè)人向該賬戶匯10元,那么余額應(yīng)該還是100散劫。那么此時(shí)可能發(fā)生這種情況稚机,A線程負(fù)責(zé)取款,B線程負(fù)責(zé)匯款舷丹,A從主內(nèi)存讀到100抒钱,B從主內(nèi)存讀到100,A執(zhí)行減10操作颜凯,并將數(shù)據(jù)刷新到主內(nèi)存,這時(shí)主內(nèi)存數(shù)據(jù)100-10=90仗扬,而B(niǎo)內(nèi)存執(zhí)行加10操作症概,并將數(shù)據(jù)刷新到主內(nèi)存,最后主內(nèi)存數(shù)據(jù)100+10=110早芭,顯然這是一個(gè)嚴(yán)重的問(wèn)題彼城,我們要保證A線程和B線程有序執(zhí)行,先取款后匯款或者先匯款后取款退个,此為有序性募壕。
一個(gè)線程執(zhí)行互斥代碼過(guò)程如下:
獲得同步鎖;
清空工作內(nèi)存语盈;
從主內(nèi)存拷貝對(duì)象副本到工作內(nèi)存舱馅;
執(zhí)行代碼(計(jì)算或者輸出等);
刷新主內(nèi)存數(shù)據(jù)刀荒;
釋放同步鎖代嗤。
所以棘钞,synchronized既保證了多線程的并發(fā)有序性,又保證了多線程的內(nèi)存可見(jiàn)性干毅。
volatile是第二種Java多線程同步的手段宜猜,根據(jù)JLS的說(shuō)法,一個(gè)變量可以被volatile修飾硝逢,在這種情況下內(nèi)存模型確保所有線程可以看到一致的變量值
class Test {? ?
static volatile int i = 0, j = 0;? ?
static void one() {? ?
? ? ? ? i++;? ?
? ? ? ? j++;? ?
? ? }? ?
static void two() {? ?
System.out.println("i=" + i + " j=" + j);? ?
? ? }? ?
}? ?
加上volatile可以將共享變量i和j的改變直接響應(yīng)到主內(nèi)存中姨拥,這樣保證了i和j的值可以保持一致,然而我們不能保證執(zhí)行two方法的線程是在i和j執(zhí)行到什么程度獲取到的渠鸽,所以volatile可以保證內(nèi)存可見(jiàn)性垫毙,不能保證并發(fā)有序性。
如果沒(méi)有volatile拱绑,則代碼執(zhí)行過(guò)程如下:
將變量i從主內(nèi)存拷貝到工作內(nèi)存综芥;
刷新主內(nèi)存數(shù)據(jù);
改變i的值猎拨;
將變量j從主內(nèi)存拷貝到工作內(nèi)存膀藐;
刷新主內(nèi)存數(shù)據(jù);
改變j的值红省;
JMM屬于語(yǔ)言級(jí)的內(nèi)存模型,它確保在不同的編譯器和不同的處理器平臺(tái)之上吧恃,通過(guò)禁止特定類型的編譯器重排序和處理器重排序虾啦,為程序員提供一致的內(nèi)存可見(jiàn)性保證。
對(duì)于編譯器沖排序痕寓,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序(不是所有的編譯器重排序都要禁止)傲醉。
對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求java編譯器在生成指令序列時(shí)呻率,插入特定類型的內(nèi)存屏障(memory barriers硬毕,intel稱之為memory fence)指令,通過(guò)內(nèi)存屏障指令來(lái)禁止特定類型的處理器重排序(不是所有的處理器重排序都要禁止)礼仗。
引申:
在執(zhí)行程序時(shí)為了提高性能吐咳,編譯器和處理器常常會(huì)對(duì)指令做重排序。重排序分三種類型:
編譯器優(yōu)化的重排序元践。編譯器在不改變單線程程序語(yǔ)義的前提下韭脊,可以重新安排語(yǔ)句的執(zhí)行順序。
指令級(jí)并行的重排序〉ヅ裕現(xiàn)代處理器采用了指令級(jí)并行技術(shù)(Instruction-Level Parallelism沪羔, ILP)來(lái)將多條指令重疊執(zhí)行。如果不存在數(shù)據(jù)依賴性慎恒,處理器可以改變語(yǔ)句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序任内。
內(nèi)存系統(tǒng)的重排序撵渡。由于處理器使用緩存和讀/寫(xiě)緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行死嗦。
上述的1屬于編譯器重排序趋距,2和3屬于處理器重排序。這些重排序都可能會(huì)導(dǎo)致多線程程序出現(xiàn)內(nèi)存可見(jiàn)性問(wèn)題越除。
如果兩個(gè)操作訪問(wèn)同一個(gè)變量节腐,且這兩個(gè)操作中有一個(gè)為寫(xiě)操作,此時(shí)這兩個(gè)操作之間就存在數(shù)據(jù)依賴性摘盆。數(shù)據(jù)依賴分下列三種類型:
名稱代碼示例說(shuō)明
寫(xiě)后讀a = 1;b = a;寫(xiě)一個(gè)變量之后翼雀,再讀這個(gè)位置。
寫(xiě)后寫(xiě)a = 1;a = 2;寫(xiě)一個(gè)變量之后孩擂,再寫(xiě)這個(gè)變量狼渊。
讀后寫(xiě)a = b;b = 1;讀一個(gè)變量之后,再寫(xiě)這個(gè)變量类垦。
上面三種情況狈邑,只要重排序兩個(gè)操作的執(zhí)行順序,程序的執(zhí)行結(jié)果將會(huì)被改變蚤认。
前面提到過(guò)米苹,編譯器和處理器可能會(huì)對(duì)操作做重排序。編譯器和處理器在重排序時(shí)砰琢,會(huì)遵守?cái)?shù)據(jù)依賴性蘸嘶,編譯器和處理器不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序。
注意陪汽,這里所說(shuō)的數(shù)據(jù)依賴性僅針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作训唱,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
as-if-serial語(yǔ)義的意思指:不管怎么重排序(編譯器和處理器為了提高并行度)掩缓,(單線程)程序的執(zhí)行結(jié)果不能被改變雪情。編譯器,runtime 和處理器都必須遵守as-if-serial語(yǔ)義你辣。
【例】
double pi? = 3.14;? ? //A?
double r? = 1.0;? ? //B?
double area = pi * r * r; //C?
上面三個(gè)操作的數(shù)據(jù)依賴關(guān)系如下圖所示:
如上圖所示,A和C之間存在數(shù)據(jù)依賴關(guān)系尘执,同時(shí)B和C之間也存在數(shù)據(jù)依賴關(guān)系舍哄。因此在最終執(zhí)行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面誊锭,程序的結(jié)果將會(huì)被改變)表悬。但A和B之間沒(méi)有數(shù)據(jù)依賴關(guān)系,編譯器和處理器可以重排序A和B之間的執(zhí)行順序丧靡。下圖是該程序的兩種執(zhí)行順序:
as-if-serial語(yǔ)義把單線程程序保護(hù)了起來(lái)蟆沫,遵守as-if-serial語(yǔ)義的編譯器籽暇,runtime 和處理器共同為編寫(xiě)單線程程序的程序員創(chuàng)建了一個(gè)幻覺(jué):?jiǎn)尉€程程序是按程序的順序來(lái)執(zhí)行的。as-if-serial語(yǔ)義使單線程程序員無(wú)需擔(dān)心重排序會(huì)干擾他們饭庞,也無(wú)需擔(dān)心內(nèi)存可見(jiàn)性問(wèn)題戒悠。
從JDK5開(kāi)始,java使用新的JSR -133內(nèi)存模型舟山。JSR-133提出了happens-before的概念绸狐,通過(guò)這個(gè)概念來(lái)闡述操作之間的內(nèi)存可見(jiàn)性。如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見(jiàn)累盗,那么這兩個(gè)操作之間必須存在happens-before關(guān)系寒矿。這里提到的兩個(gè)操作既可以是在一個(gè)線程之內(nèi),也可以是在不同線程之間若债。 與程序員密切相關(guān)的happens-before規(guī)則如下:
程序順序規(guī)則:一個(gè)線程中的每個(gè)操作符相,happens- before 于該線程中的任意后續(xù)操作。
監(jiān)視器鎖規(guī)則:對(duì)一個(gè)監(jiān)視器鎖的解鎖蠢琳,happens- before 于隨后對(duì)這個(gè)監(jiān)視器鎖的加鎖啊终。
volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫(xiě),happens- before 于任意后續(xù)對(duì)這個(gè)volatile域的讀挪凑。
傳遞性:如果A happens- before B孕索,且B happens- before C,那么A happens- before C躏碳。
注意搞旭,兩個(gè)操作之間具有happens-before關(guān)系,并不意味著前一個(gè)操作必須要在后一個(gè)操作之前執(zhí)行菇绵!happens-before僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見(jiàn)肄渗,且前一個(gè)操作按順序排在第二個(gè)操作之前(the first is visible to and ordered before the second)。happens- before的定義很微妙咬最,后文會(huì)具體說(shuō)明happens-before為什么要這么定義翎嫡。
【例】根據(jù)happens- before的程序順序規(guī)則,上面計(jì)算圓的面積的示例代碼存在三個(gè)happens- before關(guān)系:
A happens- before B永乌;
B happens- before C惑申;
A happens- before C;
這里的第3個(gè)happens- before關(guān)系翅雏,是根據(jù)happens- before的傳遞性推導(dǎo)出來(lái)的圈驼。
這里A happens- before B,但實(shí)際執(zhí)行時(shí)B卻可以排在A之前執(zhí)行(看上面的重排序后的執(zhí)行順序)望几。A happens- before B绩脆,JMM并不要求A一定要在B之前執(zhí)行。JMM僅僅要求前一個(gè)操作(執(zhí)行的結(jié)果)對(duì)后一個(gè)操作可見(jiàn),且前一個(gè)操作按順序排在第二個(gè)操作之前靴迫。這里操作A的執(zhí)行結(jié)果不需要對(duì)操作B可見(jiàn)惕味;而且重排序操作A和操作B后的執(zhí)行結(jié)果,與操作A和操作B按happens- before順序執(zhí)行的結(jié)果一致玉锌。在這種情況下名挥,JMM會(huì)認(rèn)為這種重排序并不非法(not illegal),JMM允許這種重排序芬沉。
在計(jì)算機(jī)中躺同,軟件技術(shù)和硬件技術(shù)有一個(gè)共同的目標(biāo):在不改變程序執(zhí)行結(jié)果的前提下,盡可能的開(kāi)發(fā)并行度丸逸。編譯器和處理器遵從這一目標(biāo)蹋艺,從happens- before的定義我們可以看出,JMM同樣遵從這一目標(biāo)黄刚。
現(xiàn)在讓我們來(lái)看看捎谨,重排序是否會(huì)改變多線程程序的執(zhí)行結(jié)果°疚【例】:
class ReorderExample {?
int a = 0;?
boolean flag = false;?
public void writer() {?
a =1;? ? ? ? ? ? ? ? ? //1?
flag =true;? ? ? ? ? ? //2?
? ? }?
Publicvoid reader() {?
if (flag) {? ? ? ? ? ? ? ? //3?
int i =? a * a;? ? ? ? //4?
? ? ? ? ? ? ……?
? ? ? ? }?
? ? }?
}?
flag變量是個(gè)標(biāo)記涛救,用來(lái)標(biāo)識(shí)變量a是否已被寫(xiě)入。這里假設(shè)有兩個(gè)線程A和B业扒,A首先執(zhí)行writer()方法检吆,隨后B線程接著執(zhí)行reader()方法。線程B在執(zhí)行操作4時(shí)程储,能否看到線程A在操作1對(duì)共享變量a的寫(xiě)入蹭沛?
答案是:不一定能看到。
由于操作1和操作2沒(méi)有數(shù)據(jù)依賴關(guān)系章鲤,編譯器和處理器可以對(duì)這兩個(gè)操作重排序摊灭;同樣,操作3和操作4沒(méi)有數(shù)據(jù)依賴關(guān)系(败徊?)帚呼,編譯器和處理器也可以對(duì)這兩個(gè)操作重排序。讓我們先來(lái)看看皱蹦,當(dāng)操作1和操作2重排序時(shí)煤杀,可能會(huì)產(chǎn)生什么效果?請(qǐng)看下面的程序執(zhí)行時(shí)序圖:
如上圖所示沪哺,操作1和操作2做了重排序怜珍。程序執(zhí)行時(shí),線程A首先寫(xiě)標(biāo)記變量flag凤粗,隨后線程B讀這個(gè)變量。由于條件判斷為真,線程B將讀取變量a嫌拣。此時(shí)柔袁,變量a還根本沒(méi)有被線程A寫(xiě)入,在這里多線程程序的語(yǔ)義被重排序破壞了异逐!
下面再讓我們看看捶索,當(dāng)操作3和操作4重排序時(shí)會(huì)產(chǎn)生什么效果(借助這個(gè)重排序,可以順便說(shuō)明控制依賴性)灰瞻。下面是操作3和操作4重排序后腥例,程序的執(zhí)行時(shí)序圖:
在程序中,操作3和操作4存在控制依賴關(guān)系酝润。當(dāng)代碼中存在控制依賴性時(shí)燎竖,會(huì)影響指令序列執(zhí)行的并行度。為此要销,編譯器和處理器會(huì)采用猜測(cè)(Speculation)執(zhí)行來(lái)克服控制相關(guān)性對(duì)并行度的影響构回。以處理器的猜測(cè)執(zhí)行為例,執(zhí)行線程B的處理器可以提前讀取并計(jì)算a*a疏咐,然后把計(jì)算結(jié)果臨時(shí)保存到一個(gè)名為重排序緩沖(reorder buffer ROB)的硬件緩存中纤掸。當(dāng)接下來(lái)操作3的條件判斷為真時(shí),就把該計(jì)算結(jié)果寫(xiě)入變量i中浑塞。
從圖中我們可以看出借跪,猜測(cè)執(zhí)行實(shí)質(zhì)上對(duì)操作3和4做了重排序。重排序在這里破壞了多線程程序的語(yǔ)義酌壕!
在單線程程序中掏愁,對(duì)存在控制依賴的操作重排序,不會(huì)改變執(zhí)行結(jié)果(這也是as-if-serial語(yǔ)義允許對(duì)存在控制依賴的操作做重排序的原因)仅孩;但在多線程程序中托猩,對(duì)存在控制依賴的操作重排序,可能會(huì)改變程序的執(zhí)行結(jié)果辽慕。
3京腥、順序一致性
3.1 數(shù)據(jù)競(jìng)爭(zhēng)
當(dāng)程序未正確同步時(shí),就會(huì)存在數(shù)據(jù)競(jìng)爭(zhēng)溅蛉。java內(nèi)存模型規(guī)范對(duì)數(shù)據(jù)競(jìng)爭(zhēng)的定義如下:
在一個(gè)線程中寫(xiě)一個(gè)變量公浪,
在另一個(gè)線程讀同一個(gè)變量,
而且寫(xiě)和讀沒(méi)有通過(guò)同步來(lái)排序船侧。
當(dāng)代碼中包含數(shù)據(jù)競(jìng)爭(zhēng)時(shí)欠气,程序的執(zhí)行往往產(chǎn)生違反直覺(jué)的結(jié)果(前一章的示例正是如此)。如果一個(gè)多線程程序能正確同步镜撩,這個(gè)程序?qū)⑹且粋€(gè)沒(méi)有數(shù)據(jù)競(jìng)爭(zhēng)的程序预柒。
JMM對(duì)正確同步的多線程程序的內(nèi)存一致性做了如下保證:
如果程序是正確同步的,程序的執(zhí)行將具有順序一致性(sequentially consistent)——即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型中的執(zhí)行結(jié)果相同。這里的同步是指廣義上的同步宜鸯,包括對(duì)常用同步原語(yǔ)(lock憔古,volatile和final)的正確使用。
順序一致性內(nèi)存模型有兩大特性:
一個(gè)線程中的所有操作必須按照程序的順序來(lái)執(zhí)行淋袖。
(不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序鸿市。在順序一致性內(nèi)存模型中,每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見(jiàn)即碗。
順序一致性內(nèi)存模型為程序員提供的視圖如下焰情。在概念上,順序一致性模型有一個(gè)單一的全局內(nèi)存剥懒,這個(gè)內(nèi)存通過(guò)一個(gè)左右擺動(dòng)的開(kāi)關(guān)可以連接到任意一個(gè)線程内舟。同時(shí),每一個(gè)線程必須按程序的順序來(lái)執(zhí)行內(nèi)存讀/寫(xiě)操作蕊肥。在任意時(shí)間點(diǎn)最多只能有一個(gè)線程可以連接到內(nèi)存谒获。當(dāng)多個(gè)線程并發(fā)執(zhí)行時(shí),圖中的開(kāi)關(guān)裝置能把所有線程的所有內(nèi)存讀/寫(xiě)操作串行化壁却。
為了更好的理解批狱,下面我們通過(guò)兩個(gè)示意圖來(lái)對(duì)順序一致性模型的特性做進(jìn)一步的說(shuō)明。
假設(shè)有兩個(gè)線程A和B并發(fā)執(zhí)行展东。其中A線程有三個(gè)操作赔硫,它們?cè)诔绦蛑械捻樞蚴牵篈1->A2->A3。B線程也有三個(gè)操作盐肃,它們?cè)诔绦蛑械捻樞蚴牵築1->B2->B3爪膊。
假設(shè)這兩個(gè)線程使用監(jiān)視器來(lái)正確同步:A線程的三個(gè)操作執(zhí)行后釋放監(jiān)視器,隨后B線程獲取同一個(gè)監(jiān)視器砸王。那么程序在順序一致性模型中的執(zhí)行效果將如下圖所示:
假設(shè)這兩個(gè)線程沒(méi)有做同步推盛,下面是這個(gè)未同步程序在順序一致性模型中的執(zhí)行示意圖:
未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無(wú)序的,但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序谦铃。以上圖為例耘成,線程A和B看到的執(zhí)行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個(gè)保證是因?yàn)轫樞蛞恢滦詢?nèi)存模型中的每個(gè)操作必須立即對(duì)任意線程可見(jiàn)驹闰。
但是瘪菌,在JMM中就沒(méi)有這個(gè)保證。未同步程序在JMM中不但整體的執(zhí)行順序是無(wú)序的嘹朗,而且所有線程看到的操作執(zhí)行順序也可能不一致师妙。比如,在當(dāng)前線程把寫(xiě)過(guò)的數(shù)據(jù)緩存在本地內(nèi)存中屹培,且還沒(méi)有刷新到主內(nèi)存之前默穴,這個(gè)寫(xiě)操作僅對(duì)當(dāng)前線程可見(jiàn)怔檩;從其他線程的角度來(lái)觀察,會(huì)認(rèn)為這個(gè)寫(xiě)操作根本還沒(méi)有被當(dāng)前線程執(zhí)行壁顶。只有當(dāng)前線程把本地內(nèi)存中寫(xiě)過(guò)的數(shù)據(jù)刷新到主內(nèi)存之后珠洗,這個(gè)寫(xiě)操作才能對(duì)其他線程可見(jiàn)。在這種情況下若专,當(dāng)前線程和其它線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/p>
【例】
class SynchronizedExample {?
int a = 0;?
boolean flag = false;?
public synchronized void writer() {?
a =1;?
flag =true;?
? }?
public synchronized void reader() {?
if (flag) {?
int i = a;?
? ? ? ? ……?
? ? }?
? }?
}?
在順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行蝴猪。而在JMM中调衰,臨界區(qū)內(nèi)的代碼可以重排序。
對(duì)于未同步或未正確同步的多線程程序自阱,JMM只提供最小安全性:線程執(zhí)行時(shí)讀取到的值嚎莉,要么是之前某個(gè)線程寫(xiě)入的值,要么是默認(rèn)值(0沛豌,null趋箩,false),JMM保證線程讀操作讀取到的值不會(huì)無(wú)中生有(out of thin air)的冒出來(lái)加派。
為了實(shí)現(xiàn)最小安全性叫确,JVM在堆上分配對(duì)象時(shí),首先會(huì)清零內(nèi)存空間芍锦,然后才會(huì)在上面分配對(duì)象(JVM內(nèi)部會(huì)同步這兩個(gè)操作)竹勉。因此,在以清零的內(nèi)存空間(pre-zeroed memory)分配對(duì)象時(shí)娄琉,域的默認(rèn)初始化已經(jīng)完成了次乓。
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致。因?yàn)槲赐匠绦蛟陧樞蛞恢滦阅P椭袌?zhí)行時(shí)孽水,整體上是無(wú)序的票腰,其執(zhí)行結(jié)果無(wú)法預(yù)知。保證未同步程序在兩個(gè)模型中的執(zhí)行結(jié)果一致毫無(wú)意義女气。
和順序一致性模型一樣杏慰,未同步程序在JMM中的執(zhí)行時(shí),整體上也是無(wú)序的主卫,其執(zhí)行結(jié)果也無(wú)法預(yù)知逃默。同時(shí),未同步程序在這兩個(gè)模型中的執(zhí)行特性有下面幾個(gè)差異:
順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行簇搅,而JMM不保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)完域。——前文已述
順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序瘩将,而JMM不保證所有線程能看到一致的操作執(zhí)行順序吟税“及遥——前文已述
JMM不保證對(duì)64位的long型和double型變量的讀/寫(xiě)操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫(xiě)操作都具有原子性肠仪。
關(guān)于第三點(diǎn):
第三點(diǎn)差異與處理器總線的工作機(jī)制密切相關(guān)肖抱。在計(jì)算機(jī)中,數(shù)據(jù)通過(guò)總線在處理器和內(nèi)存之間傳遞异旧。每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過(guò)一系列步驟來(lái)完成的意述,這一系列步驟稱之為總線事務(wù)(bus transaction)∷庇迹總線事務(wù)包括讀事務(wù)(read transaction)和寫(xiě)事務(wù)(write transaction)荤崇。讀事務(wù)從內(nèi)存?zhèn)魉蛿?shù)據(jù)到處理器,寫(xiě)事務(wù)從處理器傳送數(shù)據(jù)到內(nèi)存潮针,每個(gè)事務(wù)會(huì)讀/寫(xiě)內(nèi)存中一個(gè)或多個(gè)物理上連續(xù)的字术荤。這里的關(guān)鍵是,總線會(huì)同步試圖并發(fā)使用總線的事務(wù)每篷。在一個(gè)處理器執(zhí)行總線事務(wù)期間瓣戚,總線會(huì)禁止其它所有的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫(xiě)。
在一些32位的處理器上焦读,如果要求對(duì)64位數(shù)據(jù)的讀/寫(xiě)操作具有原子性子库,會(huì)有比較大的開(kāi)銷。為了照顧這種處理器吨灭,java語(yǔ)言規(guī)范鼓勵(lì)但不強(qiáng)求JVM對(duì)64位的long型變量和double型變量的讀/寫(xiě)具有原子性刚照。當(dāng)JVM在這種處理器上運(yùn)行時(shí)刁品,會(huì)把一個(gè)64位long/ double型變量的讀/寫(xiě)操作拆分為兩個(gè)32位的讀/寫(xiě)操作來(lái)執(zhí)行活鹰。這兩個(gè)32位的讀/寫(xiě)操作可能會(huì)被分配到不同的總線事務(wù)中執(zhí)行,此時(shí)對(duì)這個(gè)64位變量的讀/寫(xiě)將不具有原子性参袱。
當(dāng)單個(gè)內(nèi)存操作不具有原子性吠冤,將可能會(huì)產(chǎn)生意想不到后果浑彰。請(qǐng)看下面示意圖:
如上圖所示,假設(shè)處理器A寫(xiě)一個(gè)long型變量拯辙,同時(shí)處理器B要讀這個(gè)long型變量郭变。處理器A中64位的寫(xiě)操作被拆分為兩個(gè)32位的寫(xiě)操作,且這兩個(gè)32位的寫(xiě)操作被分配到不同的寫(xiě)事務(wù)中執(zhí)行涯保。同時(shí)處理器B中64位的讀操作被拆分為兩個(gè)32位的讀操作诉濒,且這兩個(gè)32位的讀操作被分配到同一個(gè)的讀事務(wù)中執(zhí)行。當(dāng)處理器A和B按上圖的時(shí)序來(lái)執(zhí)行時(shí)夕春,處理器B將看到僅僅被處理器A“寫(xiě)了一半“的無(wú)效值未荒。
4、volatile
把對(duì)volatile變量的單個(gè)讀/寫(xiě)及志,看成是使用同一個(gè)監(jiān)視器鎖對(duì)這些單個(gè)讀/寫(xiě)操作做了同步片排。對(duì)一個(gè)volatile變量的讀寨腔,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫(xiě)入。
這意味著即使是64位的long型和double型變量率寡,只要它是volatile變量迫卢,對(duì)該變量的讀寫(xiě)就將具有原子性。如果是多個(gè)volatile操作或類似于volatile++這種復(fù)合操作冶共,這些操作整體上不具有原子性乾蛤。
簡(jiǎn)而言之,volatile變量自身具有下列特性:
可見(jiàn)性比默。對(duì)一個(gè)volatile變量的讀幻捏,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫(xiě)入。
原子性:對(duì)任意單個(gè)volatile變量的讀/寫(xiě)具有原子性命咐,但類似于volatile++這種復(fù)合操作不具有原子性。
4.1 volatile寫(xiě)-讀建立的happens before關(guān)系
從JSR-133開(kāi)始谐岁,volatile變量的寫(xiě)-讀可以實(shí)現(xiàn)線程之間的通信醋奠。
從內(nèi)存語(yǔ)義的角度來(lái)說(shuō),volatile與監(jiān)視器鎖有相同的效果:volatile寫(xiě)和監(jiān)視器的釋放有相同的內(nèi)存語(yǔ)義伊佃;volatile讀與監(jiān)視器的獲取有相同的內(nèi)存語(yǔ)義窜司。
class VolatileExample {?
int a = 0;?
volatile boolean flag = false;?
public void writer() {?
a =1;? ? ? ? ? ? ? ? ? //1?
flag =true;? ? ? ? ? ? ? //2?
? ? }?
public void reader() {?
if (flag) {? ? ? ? ? ? ? ? //3?
int i =? a;? ? ? ? ? //4?
? ? ? ? ? ? ……?
? ? ? ? }?
? ? }?
}?
假設(shè)線程A執(zhí)行writer()方法之后,線程B執(zhí)行reader()方法航揉。根據(jù)happens before規(guī)則塞祈,這個(gè)過(guò)程建立的happens before 關(guān)系可以分為兩類:
根據(jù)程序次序規(guī)則,1 happens before 2; 3 happens before 4帅涂。
根據(jù)volatile規(guī)則议薪,2 happens before 3。
根據(jù)happens before 的傳遞性規(guī)則媳友,1 happens before 4斯议。
上圖中,每一個(gè)箭頭鏈接的兩個(gè)節(jié)點(diǎn)醇锚,代表了一個(gè)happens before 關(guān)系哼御。黑色箭頭表示程序順序規(guī)則;橙色箭頭表示volatile規(guī)則焊唬;藍(lán)色箭頭表示組合這些規(guī)則后提供的happens before保證恋昼。
這里A線程寫(xiě)一個(gè)volatile變量后,B線程讀同一個(gè)volatile變量赶促。A線程在寫(xiě)volatile變量之前所有可見(jiàn)的共享變量液肌,在B線程讀同一個(gè)volatile變量后,將立即變得對(duì)B線程可見(jiàn)芳杏。
4.2 volatile寫(xiě)-讀的內(nèi)存語(yǔ)義
volatile寫(xiě)的內(nèi)存語(yǔ)義如下:
當(dāng)寫(xiě)一個(gè)volatile變量時(shí)矩屁,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存辟宗。
以上面示例程序VolatileExample為例,假設(shè)線程A首先執(zhí)行writer()方法吝秕,隨后線程B執(zhí)行reader()方法泊脐,初始時(shí)兩個(gè)線程的本地內(nèi)存中的flag和a都是初始狀態(tài)。
下圖是線程A執(zhí)行volatile寫(xiě)后烁峭,共享變量的狀態(tài)示意圖容客。線程A在寫(xiě)flag變量后,本地內(nèi)存A中被線程A更新過(guò)的兩個(gè)共享變量的值被刷新到主內(nèi)存中约郁。此時(shí)缩挑,本地內(nèi)存A和主內(nèi)存中的共享變量的值是一致的。
volatile讀的內(nèi)存語(yǔ)義如下:
當(dāng)讀一個(gè)volatile變量時(shí)鬓梅,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無(wú)效供置。線程接下來(lái)將從主內(nèi)存中讀取共享變量。
下面是線程B讀同一個(gè)volatile變量后绽快,共享變量的狀態(tài)示意圖芥丧。在讀flag變量后,本地內(nèi)存B已經(jīng)被置為無(wú)效坊罢。此時(shí)续担,線程B必須從主內(nèi)存中讀取共享變量。線程B的讀取操作將導(dǎo)致本地內(nèi)存B與主內(nèi)存中的共享變量的值也變成一致的了活孩。
把volatile寫(xiě)和volatile讀這兩個(gè)步驟綜合起來(lái)看的話物遇,在讀線程B讀一個(gè)volatile變量后,寫(xiě)線程A在寫(xiě)這個(gè)volatile變量之前所有可見(jiàn)的共享變量的值都將立即變得對(duì)讀線程B可見(jiàn)憾儒。
下面對(duì)volatile寫(xiě)和volatile讀的內(nèi)存語(yǔ)義做個(gè)總結(jié):
線程A寫(xiě)一個(gè)volatile變量询兴,實(shí)質(zhì)上是線程A向接下來(lái)將要讀這個(gè)volatile變量的某個(gè)線程發(fā)出了(其對(duì)共享變量所在修改的)消息。
線程B讀一個(gè)volatile變量航夺,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫(xiě)這個(gè)volatile變量之前對(duì)共享變量所做修改的)消息蕉朵。
線程A寫(xiě)一個(gè)volatile變量,隨后線程B讀這個(gè)volatile變量阳掐,這個(gè)過(guò)程實(shí)質(zhì)上是線程A通過(guò)主內(nèi)存向線程B發(fā)送消息始衅。
4.3 volatile內(nèi)存語(yǔ)義的實(shí)現(xiàn)
為了實(shí)現(xiàn)volatile內(nèi)存語(yǔ)義,JMM會(huì)分別限制編譯器重排序和處理器重排序缭保。下面是JMM針對(duì)編譯器制定的volatile重排序規(guī)則表:
是否能重排序第二個(gè)操作
第一個(gè)操作普通讀/寫(xiě)volatile讀volatile寫(xiě)
普通讀/寫(xiě)? NO
volatile讀NONONO
volatile寫(xiě) NONO
舉例來(lái)說(shuō)汛闸,第三行最后一個(gè)單元格的意思是:在程序順序中,當(dāng)?shù)谝粋€(gè)操作為普通變量的讀或?qū)憰r(shí)艺骂,如果第二個(gè)操作為volatile寫(xiě)诸老,則編譯器不能重排序這兩個(gè)操作。
從上表我們可以看出:
當(dāng)?shù)诙€(gè)操作是volatile寫(xiě)時(shí)钳恕,不管第一個(gè)操作是什么别伏,都不能重排序蹄衷。這個(gè)規(guī)則確保volatile寫(xiě)之前的操作不會(huì)被編譯器重排序到volatile寫(xiě)之后。
當(dāng)?shù)谝粋€(gè)操作是volatile讀時(shí)厘肮,不管第二個(gè)操作是什么愧口,都不能重排序。這個(gè)規(guī)則確保volatile讀之后的操作不會(huì)被編譯器重排序到volatile讀之前类茂。
當(dāng)?shù)谝粋€(gè)操作是volatile寫(xiě)耍属,第二個(gè)操作是volatile讀時(shí),不能重排序巩检。