Java內(nèi)存模型總結(jié)(摘選)

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í)行。

1伪很、多線程通信

1.1 內(nèi)存模型

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)性保證。

1.2 可見(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í)行,先取款后匯款或者先匯款后取款退个,此為有序性募壕。

1.3 synchronized與volatile

一個(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的值红省;


2额各、重排序

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)題越除。

2.1 數(shù)據(jù)依賴性

如果兩個(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ù)依賴性不被編譯器和處理器考慮

2.2 as-if-serial語(yǔ)義

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)題戒悠。

2.3 happens-before

從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)黄刚。

2.4 重排序?qū)Χ嗑€程的影響

現(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)的正確使用。

3.2 順序一致性內(nèi)存模型

順序一致性內(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>

3.3 同步程序的執(zhí)行特性

【例】

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)的代碼可以重排序。

3.4 未同步程序的執(zhí)行特性

對(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í),不能重排序巩检。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末厚骗,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子兢哭,更是在濱河造成了極大的恐慌领舰,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,470評(píng)論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件迟螺,死亡現(xiàn)場(chǎng)離奇詭異提揍,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)煮仇,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谎仲,“玉大人浙垫,你說(shuō)我怎么就攤上這事≈E担” “怎么了夹姥?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,577評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)辙诞。 經(jīng)常有香客問(wèn)我辙售,道長(zhǎng),這世上最難降的妖魔是什么飞涂? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,176評(píng)論 1 292
  • 正文 為了忘掉前任旦部,我火速辦了婚禮,結(jié)果婚禮上较店,老公的妹妹穿的比我還像新娘士八。我一直安慰自己,他們只是感情好梁呈,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,189評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布婚度。 她就那樣靜靜地躺著,像睡著了一般官卡。 火紅的嫁衣襯著肌膚如雪蝗茁。 梳的紋絲不亂的頭發(fā)上醋虏,一...
    開(kāi)封第一講書(shū)人閱讀 51,155評(píng)論 1 299
  • 那天,我揣著相機(jī)與錄音哮翘,去河邊找鬼颈嚼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛忍坷,可吹牛的內(nèi)容都是我干的粘舟。 我是一名探鬼主播,決...
    沈念sama閱讀 40,041評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼佩研,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼柑肴!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起旬薯,我...
    開(kāi)封第一講書(shū)人閱讀 38,903評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤晰骑,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后绊序,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體硕舆,經(jīng)...
    沈念sama閱讀 45,319評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,539評(píng)論 2 332
  • 正文 我和宋清朗相戀三年骤公,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了抚官。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,703評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡阶捆,死狀恐怖凌节,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情洒试,我是刑警寧澤倍奢,帶...
    沈念sama閱讀 35,417評(píng)論 5 343
  • 正文 年R本政府宣布,位于F島的核電站垒棋,受9級(jí)特大地震影響卒煞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜叼架,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,013評(píng)論 3 325
  • 文/蒙蒙 一畔裕、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧碉碉,春花似錦柴钻、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,664評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春毫蚓,著一層夾襖步出監(jiān)牢的瞬間占键,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,818評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工元潘, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留畔乙,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,711評(píng)論 2 368
  • 正文 我出身青樓翩概,卻偏偏與公主長(zhǎng)得像牲距,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子钥庇,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,601評(píng)論 2 353

推薦閱讀更多精彩內(nèi)容