pdf下載:
https://pan.baidu.com/s/1SM__fev_esbYhVOWo90RKw
深入理解Java內(nèi)存模型(四)——volatile
java內(nèi)存模型
解決內(nèi)存可見行
問題。
并發(fā)編程處理兩個問題:
1.線程之間如何通信
2.線程之間如何同步
通信:線程之間以何種機制來交換信息;
線程之間的通信機制有兩種:
1.共享內(nèi)存
2.消息傳遞
共享內(nèi)存:
線程之間共享程序的公共狀態(tài)酸役,線程之間通過寫-讀內(nèi)存中公共狀態(tài)來隱式進行通信
消息傳遞:
線程之間沒有公共狀態(tài),線程之間必須通過明確的發(fā)送消息顯示進行通信
同步:程序用于控制不同線程之間操作執(zhí)行相對順序的機制蛹稍;
在共享內(nèi)存并發(fā)模型里愿伴,同步是顯式進行的趟咆;
程序員必須顯式指定某個方法或某段代碼需要在線程之間互斥執(zhí)行园蝠。
在消息傳遞模型的并發(fā)編程里渺蒿,由于消息的發(fā)送必須在消息的接收之前,因此同步是隱式進行的砰琢;
java的并發(fā)采用的是共享內(nèi)存模型蘸嘶,java線程之間的通信總是隱式進行的良瞧。整個通信過程對程序員完全透明陪汽。
如果編寫多線程的java程序員不理解隱式進行的線程之間通信的工作機制训唱,很可能會遇到各種奇怪的內(nèi)存可見性問題。
關(guān)鍵詞:內(nèi)存可見行
java內(nèi)存模型的抽象
共享變量:實例域挚冤,靜態(tài)域况增,數(shù)組元素
不會在線程共享:
局部變量,方法定義參數(shù)训挡,異常處理器對象:
不會有內(nèi)存可見行性問題澳骤,也不受內(nèi)存模型影響。
jmm決定一個線程對共享變量寫入何時對另一個線程可見澜薄。
共享主內(nèi)存
線程私有內(nèi)存
jmm通過控制主內(nèi)存與每個線程的本地內(nèi)存之間的交互为肮,來為java程序員提供內(nèi)存可見行保證。
重排序
編譯器和處理器對指令重排序肤京。
重排序分為三種:
編譯器重排序
指令級并行的重排序
內(nèi)存系統(tǒng)的重排序
這些重排序都可以導(dǎo)致多線程出現(xiàn)內(nèi)存可見性
問題颊艳。
禁止重排序:jmm有重排序規(guī)則。處理器級別的有內(nèi)存屏障
jmm(java內(nèi)存模型)
是屬于語言級的內(nèi)存模型忘分,它確保在不同的編譯器和不同的處理器平臺之上棋枕,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見行
保證妒峦。
處理器重排序與內(nèi)存屏障指令
從內(nèi)存操作實際發(fā)生的順序來看重斑,直到處理器A執(zhí)行A3來刷新自己的寫緩存區(qū),寫操作A1才算真正執(zhí)行了肯骇。雖然處理器A執(zhí)行內(nèi)存操作的順序為:A1->A2窥浪,但內(nèi)存操作實際發(fā)生的順序卻是:A2->A1。此時笛丙,處理器A的內(nèi)存操作順序被重排序了(處理器B的情況和處理器A一樣寒矿,這里就不贅述了)。
這句話怎么理解若债?
對于處理器A來說符相,
a=1;//A1
x=b;//A2
是可以先執(zhí)行A2再執(zhí)行A1的,因為先執(zhí)行A2再執(zhí)行A1蠢琳,對程序沒有任何影響啊终;a=1;和x=b;之間并沒有邏輯上的先后因果關(guān)系。不存在數(shù)據(jù)依賴
寫緩存僅對自己的處理器可見傲须,它會導(dǎo)致處理器執(zhí)行內(nèi)存操作的順序可能會與內(nèi)存實際的操作執(zhí)行順序不一致蓝牲。
x86僅僅允許對寫-讀操作做重排序。因為使用了寫緩沖區(qū)泰讽。
為了保證內(nèi)存可見性
,java編譯器會在生成指令序列的適當位置插入內(nèi)存屏障
來禁止特定類型的處理器重排序例衍。
happens-before:用來闡述操作之間的內(nèi)存可見行
從jdk5開始昔期,java使用新的內(nèi)存模型(jsr-133內(nèi)存模型)
jsr-133使用hanppens-before的概念來闡述操作之間的內(nèi)存可見行。
jmm中佛玄,如果一個操作執(zhí)行的結(jié)果需要對另一操作可見硼一,那么這兩個操作之間必須要存在happens-before關(guān)系。
這里提到的兩個操作梦抢,可以在一個線程之內(nèi)般贼,也可以在多個線程之間。
happens-before規(guī)則:
程序順序規(guī)則:
一個線程內(nèi)的所有操作奥吩,happens-before于該線程中的任意后續(xù)操作哼蛆。
互斥鎖規(guī)則:
對一個互斥鎖的釋放/解鎖,happens-before于隨后對這個互斥鎖的獲取/加鎖霞赫。
volatile變量規(guī)則:
對一個volatile域的寫腮介,happens-before 于任意后續(xù)對這個volatile域的讀。
傳遞性:
如果A happens-before B,且B happens-before C端衰, 那么 A happens-before C
也就就說可以通過加鎖叠洗,volatile修飾變量保證內(nèi)存可見性
,
注意:兩個操作之間具有happens-before關(guān)系靴迫,并不意味著前一個操作必須要在后一個操作之前執(zhí)行惕味!
happens-before僅僅要求前一個操作(執(zhí)行的結(jié)果)對后一個操作可見
。
被volatile修飾的變量玉锌,在讀之前名挥,要先等待寫并刷新的主內(nèi)存,
互斥鎖主守,在獲取鎖之間禀倔,必須等待鎖的釋放;
一個線程內(nèi)参淫,代碼按順序執(zhí)行救湖。
重排序
如果兩個操作訪問同一個變量,且這兩個操作中有一個寫操作涎才,此時兩個操作之間就存在了數(shù)據(jù)依賴性鞋既。
數(shù)據(jù)依賴性分下列三種類型:
寫后讀
寫后寫
讀后寫
上面三種情況,只要重排序兩個操作的執(zhí)行順序耍铜,程序的執(zhí)行結(jié)果將會被改變邑闺。
前面提到過,編譯器和處理器可能會對操作做重排序棕兼。編譯器和處理器在重排序時陡舅,會遵守數(shù)據(jù)依賴性,編譯器和處理器不會改變存在數(shù)據(jù)依賴關(guān)系的兩個操作的執(zhí)行順序伴挚。
注意靶衍,這里所說的數(shù)據(jù)依賴性僅針對單個處理器中執(zhí)行的指令序列和單個線程中執(zhí)行的操作灾炭,不同處理器之間和不同線程之間的數(shù)據(jù)依賴性不被編譯器和處理器考慮。
as-if-serial語義
不管怎么重排序(編譯器和處理器為了提高并發(fā)度)颅眶,(單線程)程序的執(zhí)行結(jié)果不能被改變蜈出。編譯器,runtime,和處理器都必須遵循as-if-serial語義帚呼。
為了遵循as-if-serial語義掏缎,編譯器和處理器不會對存在數(shù)據(jù)依賴關(guān)系的操作做重排序皱蹦。因為這種重排序會改變執(zhí)行結(jié)果煤杀。
但是,如果操作之間不存在數(shù)據(jù)依賴關(guān)系沪哺,這些操作被編譯器和處理器重排序沈自。
double pi = 3.14;//A
double r = 1.0;//B
double area = pirr;//C
A和C之間存在數(shù)據(jù)依賴關(guān)系,同時B和C之間也存在數(shù)據(jù)依賴關(guān)系辜妓。因此在最終執(zhí)行的指令序列中枯途,C不能被重排序到A和B的前面(C排到A和B的前面,程序的結(jié)果將會被改變)籍滴。
但A和B之間沒有數(shù)據(jù)依賴關(guān)系酪夷,編譯器和處理器可以重排序A和B之間的執(zhí)行順序。
實際上:執(zhí)行順序也可能是
double r = 1.0;//B
double pi = 3.14;//A
double area = pirr;//C
as-if-serial語義把單線程程序保護了起來孽惰,遵守as-if-serial語義的編譯器晚岭,runtime 和處理器共同為編寫單線程程序的程序員創(chuàng)建了一個幻覺:單線程程序是按程序的順序來執(zhí)行的。as-if-serial語義使單線程程序員無需擔心重排序會干擾他們勋功,也無需擔心內(nèi)存可見性問題坦报。
程序順序規(guī)則
在計算機中,軟件技術(shù)和硬件技術(shù)有一個共同的目標:在不改變程序執(zhí)行結(jié)果的前提下狂鞋,盡可能的開發(fā)并行度片择。
編譯器和處理器遵從這一目標,從happens- before的定義我們可以看出骚揍,JMM同樣遵從這一目標字管。
重排序?qū)Χ嗑€程的影響
控制依賴關(guān)系
當代碼中存在控制依賴性
時,會影響指令序列執(zhí)行的并行度信不。
為此嘲叔,編譯器和處理器會采用猜測(Speculation)
執(zhí)行來克服控制相關(guān)性對并行度的影響
。
在單線程程序中浑塞,對存在控制依賴的操作重排序借跪,不會改變執(zhí)行結(jié)果(這也是as-if-serial語義允許對存在控制依賴的操作做重排序的原因);但在多線程程序中酌壕,對存在控制依賴的操作重排序掏愁,可能會改變程序的執(zhí)行結(jié)果歇由。
數(shù)據(jù)競爭與順序一致性保證
當程序未正確同步時,就會存在數(shù)據(jù)競爭果港。java內(nèi)存模型規(guī)范對數(shù)據(jù)競爭的定義如下:
在一個線程中寫一個變量沦泌,
在另一個線程讀同一個變量,
而且寫和讀沒有通過同步來排序辛掠。
jmm對正確同步的多線程程序的內(nèi)存一致性
做了如下保證:
如果程序是正確同步的谢谦,程序的執(zhí)行將具有順序一致性
--即程序的執(zhí)行結(jié)果與該程序在順序一致性內(nèi)存模型
中的執(zhí)行結(jié)果相同,
這里的同步是指廣義上的同步萝衩,包括對常用同步原語(lock,volatile和final)的正確使用回挽。
順序一致性內(nèi)存模型
順序一致性內(nèi)存模型
是一個被計算機科學(xué)家理想化了的理論參考模型,
它為程序員提供了極強的內(nèi)存可見性保證
猩谊。
順序一致性內(nèi)存模型有兩大特性:
一個線程中所有操作必須按照程序的順序來執(zhí)行千劈。
(不管程序是否同步)所有線程都只能看到一個單一的操作執(zhí)行順序。
在順序一致性內(nèi)存模型中牌捷,每個操作都必須原子執(zhí)行且立即對所有線程可見墙牌。
舉例說明:
順序一致性
保證
未同步程序在順序一致性模型中雖然整體執(zhí)行順序是無序的,但所有線程都只能看到一個一致的整體執(zhí)行順序暗甥。以上圖為例喜滨,線程A和B看到的執(zhí)行順序都是:B1->A1->A2->B2->A3->B3。之所以能得到這個保證是因為順序一致性內(nèi)存模型中的每個操作必須立即對任意線程可見撤防。
但是虽风,jmm中就沒有這個保證
。未同步程序在jmm中不但整體的執(zhí)行順序是無序的即碗,而且所有線程看到的操作順序也可能不一致焰情。
比如,在當前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中剥懒,且還沒有刷新到主內(nèi)存之前内舟,這個寫操作僅對當前線程可見;從其他線程的角度來觀察初橘,會認為這個寫操作根本還沒有被當前線程執(zhí)行验游。只有當前線程把本地內(nèi)存中寫過的數(shù)據(jù)刷新到主內(nèi)存之后,這個寫操作才能對其他線程可見保檐。在這種情況下耕蝉,當前線程和其它線程看到的操作執(zhí)行順序?qū)⒉灰恢隆?/p>
同步程序順序一致性效果
在順序一致性模型中,所有操作完全按程序的順序串行執(zhí)行夜只。而在JMM中垒在,臨界區(qū)內(nèi)的代碼可以重排序(但JMM不允許臨界區(qū)內(nèi)的代碼“逸出”到臨界區(qū)之外,那樣會破壞監(jiān)視器的語義)
扔亥。JMM會在退出監(jiān)視器和進入監(jiān)視器這兩個關(guān)鍵時間點做一些特別處理场躯,使得線程在這兩個時間點具有與順序一致性模型相同的內(nèi)存視圖(具體細節(jié)后文會說明)
谈为。雖然線程A在臨界區(qū)內(nèi)做了重排序,但由于監(jiān)視器的互斥執(zhí)行的特性踢关,這里的線程B根本無法“觀察”到線程A在臨界區(qū)內(nèi)的重排序伞鲫。這種重排序既提高了執(zhí)行效率,又沒有改變程序的執(zhí)行結(jié)果签舞。
從這里我們可以看到JMM在具體實現(xiàn)上的基本方針:在不改變(正確同步的)程序執(zhí)行結(jié)果的前提下秕脓,盡可能的為編譯器和處理器的優(yōu)化打開方便之門。
未同步程序的執(zhí)行特性
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致儒搭。因為未同步程序在順序一致性模型中執(zhí)行時吠架,整體上是無序的,其執(zhí)行結(jié)果無法預(yù)知师妙。保證未同步程序在兩個模型中的執(zhí)行結(jié)果一致毫無意義诵肛。
和順序一致性模型一樣屹培,未同步程序在JMM中的執(zhí)行時默穴,整體上也是無序的,其執(zhí)行結(jié)果也無法預(yù)知褪秀。同時蓄诽,未同步程序在這兩個模型中的執(zhí)行特性有下面幾個差異:
順序一致性模型保證單線程內(nèi)的操作會按程序的順序執(zhí)行,而JMM不保證單線程內(nèi)的操作會按程序的順序執(zhí)行(比如上面正確同步的多線程程序在臨界區(qū)內(nèi)的重排序)媒吗。這一點前面已經(jīng)講過了仑氛,這里就不再贅述。
順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序闸英,而JMM不保證所有線程能看到一致的操作執(zhí)行順序锯岖。這一點前面也已經(jīng)講過,這里就不再贅述甫何。
JMM不保證對64位的long型和double型變量的讀/寫操作具有原子性出吹,而順序一致性模型保證對所有的內(nèi)存讀/寫操作都具有原子性。
線程之間的通信由java內(nèi)存模型jmm控制辙喂,jmm決定一個線程對共享變量的寫入何時對另一個線程可見捶牢。從抽象的角度來看,jmm定義了線程和主內(nèi)存之間的抽象關(guān)系:線程之間的共享變量存儲在主內(nèi)存中巍耗,每個線程都有一個私有的本地內(nèi)存(local memory )秋麸,本地內(nèi)存中存儲了該線程以讀/寫共享變量的副本。本地內(nèi)存是jmm中一個抽象概念炬太,并不真實存在。它涵蓋了緩存亲族,寫緩存區(qū)吓歇,寄存器以及其他的硬件和編譯器優(yōu)化票腰,
volatile的特性
理解volatile特性的一個好方法是:把對volatile變量的單個讀/寫,看作是使用同一個monitor對這個單個讀/寫操作做了同步杏慰。
class VolatileFeaturesExample {
volatile long vl = 0L; //使用volatile聲明64位的long型變量
public void set(long l) {
vl = l; //單個volatile變量的寫
}
public void getAndIncrement () {
vl++; //復(fù)合(多個)volatile變量的讀/寫
}
public long get() {
return vl; //單個volatile變量的讀
}
}
假設(shè)有多個線程分別調(diào)用上面程序的三個方法,這個程序在語意上和下面程序等價:
class VolatileFeaturesExample {
long vl = 0L; // 64位的long型普通變量
public synchronized void set(long l) { //對單個的普通 變量的寫用同一個監(jiān)視器同步
vl = l;
}
public void getAndIncrement () { //普通方法調(diào)用
long temp = get(); //調(diào)用已同步的讀方法
temp += 1L; //普通寫操作
set(temp); //調(diào)用已同步的寫方法
}
public synchronized long get() {
//對單個的普通變量的讀用同一個監(jiān)視器同步
return vl;
}
}
監(jiān)視器鎖的happens-before規(guī)則保證釋放監(jiān)視器和獲取監(jiān)視器的兩個線程之間的內(nèi)存可見性轰胁,這意味著對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入赃阀。
監(jiān)視器鎖的語義決定了臨界區(qū)代碼的執(zhí)行具有原子性。這意味著即使是64位的long型和double型變量榛斯,只要它是volatile變量,對該變量的讀寫就將具有原子性搂捧。如果是多個volatile操作或類似于volatile++這種復(fù)合操作驮俗,這些操作整體上不具有原子性。
簡而言之允跑,volatile變量自身具有下列特性:
可見行王凑。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最后的寫入聋丝。
原子性:對任意單個volatile變量的讀/寫具有原子性索烹。但類似于volatile++這種復(fù)合操作不具有原子性。
鎖的釋放--獲取建立的happens-before關(guān)系
鎖是java并發(fā)編程中最重要的同步機制弱睦。
鎖除了讓臨界區(qū)互斥執(zhí)行外百姓,還可以讓釋放鎖的線程向獲取同一個鎖的線程發(fā)送消息。
鎖內(nèi)存語義的具體實現(xiàn)機制
final
對于final域每篷,編譯器和處理器要遵守兩個重排序規(guī)則:
1.在構(gòu)造函數(shù)內(nèi)對一個final域的寫入瓣戚,與隨后把這個 被構(gòu)造對象的引用(這個對象 指的是這個構(gòu)造方法所在的類) 賦值給一個引用變量,這兩個操作之間不能重排序焦读。
2.初次讀一個包含final域的對象的引用子库,與隨后初次讀這個final域,這兩個操作之間不能重排序矗晃。
寫final域的重排序規(guī)則
寫final域的重排序規(guī)則禁止把final域的寫重排序到構(gòu)造函數(shù)之外仑嗅。這個規(guī)則的實現(xiàn)包含下面2個方面:
JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外。
編譯器會在final域的寫之后,構(gòu)造函數(shù)return之前仓技,插入一個StoreStore屏障鸵贬。這個屏障禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外。
寫final域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前脖捻,對象的final域已經(jīng)被正確初始化過了阔逼,而普通域不具有這個保障。
這里final域為一個引用類型地沮,它引用一個int型的數(shù)組對象嗜浮。對于引用類型,寫final域的重排序規(guī)則對編譯器和處理器增加了如下約束:
在構(gòu)造函數(shù)內(nèi)對一個final引用的對象的成員域的寫入摩疑,與隨后在構(gòu)造函數(shù)外把這個被構(gòu)造對象的引用賦值給一個引用變量危融,這兩個操作之間不能重排序。
處理器內(nèi)存模型
順序一致性內(nèi)存模型
是一個理論參考模型
雷袋,jmm
和處理器內(nèi)存模型
在設(shè)計時通常會把順序一致性內(nèi)存模型
作為參照吉殃。
jmm和處理器內(nèi)存模型在設(shè)計時通常會把順序一致性內(nèi)存模型作為參照,
jmm和處理器內(nèi)存模型在設(shè)計時會對順序一致性模型做一些放松楷怒,因為如果完全按照順序一致性模型來處理處理器和jmm,那么很多的處理器和編譯器優(yōu)化都要被禁止迫卢,這對執(zhí)行性能將會有很多的影響。
根據(jù)對不同類型讀/寫操作組合的執(zhí)行順序的放松每界,可以把常見處理器內(nèi)存模型劃分為下面幾種類型:
注意眨层,這里處理器對讀/寫操作的放松馒闷,是以兩個操作之間不存在數(shù)據(jù)依賴性為前提的
(因為處理器要遵守as-if-serial語義
,處理器不會對存在數(shù)據(jù)依賴性的兩個內(nèi)存操作做重排序
)捺疼。
jmm卧秘,處理器內(nèi)存模型與順序一致性內(nèi)存模型之間的關(guān)系
jmm是個語言級的內(nèi)存模型羞福,
處理器內(nèi)存模型是個硬件級的內(nèi)存模型坯临,
順序一致內(nèi)存模型是個理論參考模型看靠。
因此挟炬,JMM把happens- before要求禁止的重排序分為了下面兩類:
會改變程序執(zhí)行結(jié)果的重排序谤祖。
不會改變程序執(zhí)行結(jié)果的重排序老速。
JMM對這兩種不同性質(zhì)的重排序,采取了不同的策略:
對于會改變程序執(zhí)行結(jié)果的重排序额湘,JMM要求編譯器和處理器必須禁止這種重排序锋华。
對于不會改變程序執(zhí)行結(jié)果的重排序箭窜,JMM對編譯器和處理器不作要求(JMM允許這種重排序)磺樱。
jmm的內(nèi)存可見性
保證
1.單線程程序竹捉。單線程程序不會出現(xiàn)內(nèi)存可見行
問題。
編譯器物遇,runtime和處理器會共同確保單線程程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果相同。
2.正確同步的多線程程序乃沙。
正確同步的多線程程序的執(zhí)行將具有順序一致性警儒。
(程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果相同)
這是jmm關(guān)注的重點眶根,jmm通過限制編譯器和處理器的重排序來為程序員提供內(nèi)存可見性
的保證属百。
3.未同步/未正確同步的多線程程序
jmm為它們提供了最小安全性保障:
線程執(zhí)行時讀取到的值族扰,要么是之前某個線程寫入的值,要么是默認值(0,null,false)怒竿。
jsr-133對舊內(nèi)存模型的修補
增強volatile的內(nèi)存語義耕驰。
增強final的內(nèi)存語義朦肘。