1.Java內(nèi)存模型的基礎(chǔ)
①并發(fā)編程模型的兩個(gè)關(guān)鍵問題
線程之間如何通信粹懒、線程之間如何同步
通信是指線程之間以何種機(jī)制來交換信息重付。在命令式編程中,線程之間的通信機(jī)制有兩種:共享內(nèi)存和消息傳遞凫乖。
共享內(nèi)存的并發(fā)模型通過寫-讀內(nèi)存中的公共狀態(tài)進(jìn)行隱式通信确垫。消息傳遞的并發(fā)模型必須通過發(fā)送消息來顯式進(jìn)行通信弓颈。
同步是指程序中用于控制不同線程間操作發(fā)生相對(duì)順序的機(jī)制。
共享內(nèi)存的并發(fā)模型同步是顯式進(jìn)行的删掀。消息傳遞的并發(fā)模型同步是隱式進(jìn)行的翔冀。
Java的并發(fā)采用的是共享內(nèi)存模型。
②Java內(nèi)存模型的抽象結(jié)構(gòu)
在Java中披泪,實(shí)例域纤子、靜態(tài)域、數(shù)組元素都存儲(chǔ)在堆內(nèi)存中款票,堆內(nèi)存在線程之間共享(用共享變量代指)控硼。局部變量、方法定義參數(shù)艾少、異常處理器參數(shù)不會(huì)在線程之間共享卡乾,它們不會(huì)有內(nèi)存可見性問題,也不受內(nèi)存模型的影響缚够。
Java線程之間的通信由Java內(nèi)存模型(JMM)控制幔妨。
③從源代碼到指令序列的重排序
重排序分3種類型:
1)編譯器優(yōu)化的重排序。編譯器在不改變單線程程序語義的前提下谍椅,可以重新安排語句執(zhí)行順序误堡。
2)指令級(jí)并行的重排序。現(xiàn)代處理器采用了指令級(jí)并行技術(shù)來將多條指令重疊執(zhí)行毯辅。如果不存在數(shù)據(jù)依賴性埂伦,處理器可以改變語句對(duì)應(yīng)機(jī)器指令的執(zhí)行順序煞额。
3)內(nèi)存系統(tǒng)的重排序思恐。由于處理器使用緩存和讀/寫緩沖區(qū),這使得加載和存儲(chǔ)操作看上去可能是在亂序執(zhí)行膊毁。
1屬于編譯器重排序胀莹,2和3屬于處理器重排序。對(duì)于編譯器婚温,JMM的編譯器重排序規(guī)則會(huì)禁止特定類型的編譯器重排序描焰。對(duì)于處理器重排序,JMM的處理器重排序規(guī)則會(huì)要求Java編譯器在生成指令序列時(shí)栅螟,插入特定類型的內(nèi)存屏障指令荆秦,通過內(nèi)存屏障指令來禁止特定類型的處理器重排序。
JMM屬于語言級(jí)的內(nèi)存模型力图,它確保在不同的編譯器和處理器平臺(tái)上步绸,通過禁止特定類型的編譯器重排序和處理器重排序,為程序員提供一致的內(nèi)存可見性保證吃媒。
④并發(fā)編程模型的分類
由于每個(gè)處理器上寫緩沖區(qū)瓤介,僅僅對(duì)它所在的處理器可見吕喘,處理器對(duì)內(nèi)存的讀/寫操作的執(zhí)行順序,不一定與內(nèi)存實(shí)際發(fā)生的讀/寫操作順序一致刑桑!
下表是常見處理器允許的重排序類型的列表:N表示處理器不允許兩個(gè)操作重排序氯质,Y表示允許重排序。
Load-讀祠斧,Store-寫
為了保證內(nèi)存可見性闻察,Java編譯器在生成指令序列的適當(dāng)位置會(huì)插入內(nèi)存屏障指令來禁止特定類型的處理器重排序。JMM把內(nèi)存屏障指令分為4類琢锋,如下表
? Store Load Barriers 是一個(gè)“全能型”的屏障蜓陌,同時(shí)具有其他3個(gè)屏障的效果。現(xiàn)代的多處理器大多支持該屏障吩蔑。執(zhí)行該屏障開銷會(huì)很昂貴钮热,因?yàn)楫?dāng)前處理器通常要把寫緩沖區(qū)中的數(shù)據(jù)全部刷新到內(nèi)存中(Buffer Fully Flush)
⑤happens-before簡介
從JDK5開始,Java使用新的JSR-133內(nèi)存模型烛芬。JSR-133使用happens-before的概念來闡述操作之間的內(nèi)存可見性隧期。JMM中,如果一個(gè)操作執(zhí)行的結(jié)果需要對(duì)另一個(gè)操作可見赘娄,那么兩個(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è)鎖的解鎖揍堰,happens-before于隨后對(duì)這個(gè)鎖的加鎖鹏浅。
- volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,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è)操作可見幽纷,且前一個(gè)操作按順序排在第二個(gè)操作自謙式塌。
2.重排序
重排序是指編譯器和處理器為了優(yōu)化程序性能而對(duì)指令序列進(jìn)行重新排序的一種手段。
①數(shù)據(jù)依賴性
如果兩個(gè)操作訪問同一個(gè)變量友浸,且這兩個(gè)操作中有一個(gè)為寫操作峰尝,這兩個(gè)操作之間就存在數(shù)據(jù)依賴性。編譯器和處理器在重排序時(shí)尾菇,不會(huì)改變存在數(shù)據(jù)依賴關(guān)系的兩個(gè)操作的執(zhí)行順序(只針對(duì)單個(gè)處理器中執(zhí)行的指令序列和單個(gè)線程中執(zhí)行的操作境析,多個(gè)的時(shí)候不考慮數(shù)據(jù)依賴性)囚枪。
②as-if-serial語義
as-if-serial語義意思是:不管怎么重排序,單線程程序的執(zhí)行結(jié)果不能被改變劳淆。
as-if-serial語義使單線程程序員無需擔(dān)心重排序會(huì)干擾他們链沼,也無需擔(dān)心內(nèi)存可見性問題。
③程序順序規(guī)則
根據(jù)happens-before的程序順序規(guī)則沛鸵,上面計(jì)算圓的面積的示例代碼存在3個(gè)happens-before關(guān)系括勺。
1)A happens-before B。
2)B happens-before C曲掰。
3)A happens-before C疾捍。
重排序操作A和操作B后的執(zhí)行結(jié)果與操作A和操作B按happens-before順序執(zhí)行的結(jié)果一致。這種情況下栏妖,JMM會(huì)認(rèn)為這種重排序并不非法乱豆,允許這種重排序。
④重排序?qū)Χ嗑€程的影響
class ReorderExample {
int a = 0;
boolean flag= false;
public void writer() {
a ==1; //1
flag == true; //2
}
public void reader() {
if(flag) { //3
int i = a * a; //4
......
}
}
}
3和4存在控制依賴關(guān)系吊趾。當(dāng)代碼中存在控制依賴性時(shí)宛裕,會(huì)影響指令序列執(zhí)行的并行度。為此編譯器和處理器會(huì)采用猜測執(zhí)行來克服控制相關(guān)性對(duì)并行度的影響论泛。以處理器的猜測執(zhí)行為例揩尸,執(zhí)行線程B的處理器可以提前讀取并計(jì)算a*a,然后把計(jì)算記過臨時(shí)保存到一個(gè)名為重排序緩沖的硬件緩存中屁奏。當(dāng)3的條件判斷為真時(shí)岩榆,就把該計(jì)算結(jié)果寫入變量i中。單線程程序?qū)Υ嬖诳刂埔蕾嚨牟僮髦嘏判虿粫?huì)改變執(zhí)行結(jié)果坟瓢,但在多線程程序中勇边,對(duì)存在控制依賴的操作重排序,可能會(huì)改變程序的執(zhí)行結(jié)果载绿。
3.順序一致性
①數(shù)據(jù)競爭與順序一致性
Java內(nèi)存模型規(guī)范對(duì)數(shù)據(jù)競爭的定義如下:
在一個(gè)線程中寫一個(gè)變量粥诫,在另一個(gè)線程讀同一個(gè)變量油航,而且寫和讀沒有通過同步來排序崭庸。
②順序一致性內(nèi)存模型
順序一致性內(nèi)存模型:是一個(gè)被計(jì)算機(jī)科學(xué)家理想化了的理論參考模型,它為程序員提供了極強(qiáng)的內(nèi)存可見性保證谊囚。
順序一致性內(nèi)存模型兩大特性:
1)一個(gè)線程中的所有操作必須按照程序的順序來執(zhí)行
2)(不管程序是否同步)所有線程都只能看到一個(gè)單一的操作執(zhí)行順序怕享。每個(gè)操作都必須原子執(zhí)行且立刻對(duì)所有線程可見。
兩個(gè)線程使用監(jiān)視器鎖正確同步:
兩個(gè)線程沒有做同步:整體執(zhí)行順序是無序的镰踏,但所有線程都只能看到一個(gè)一致的整體執(zhí)行順序函筋。順序一致性內(nèi)存模型中的每個(gè)操作必須立即對(duì)任意線程可見。
但是JMM中沒有這個(gè)保證奠伪。未同步程序在JMM中不但整體的執(zhí)行順序是無序的跌帐,而且所有線程看到的操作執(zhí)行順序也可能不一致首懈。(比如當(dāng)前線程把寫過的數(shù)據(jù)緩存在本地內(nèi)存中,在沒有刷新到主內(nèi)存之前谨敛,寫操作僅對(duì)當(dāng)前線程可見究履,其他線程會(huì)認(rèn)為這個(gè)寫操作根本沒有被當(dāng)前線程執(zhí)行,這種情況下脸狸,當(dāng)前線程和其他線程看到的操作執(zhí)行順序?qū)⒉灰恢拢?/p>
③同步程序的順序一致性效果
對(duì)前面ReorderExample用鎖來同步最仑,看看正確同步的程序如何具有順序一致性。
class SynchronizedExample {
int a = 0;
boolean flag= false;
public synchronized void writer() { //獲取鎖
a ==1; //1
flag == true; //2
} //釋放鎖
public synchronized void reader() { //獲取鎖
if(flag) { //3
int i = a * a; //4
......
}
} //釋放鎖
}
④未同步程序的執(zhí)行特性
對(duì)于未同步或未正確同步的多線程程序炊甲,JMM只提供最小安全性:線程執(zhí)行時(shí)讀取到的值泥彤,要么是之前某個(gè)線程寫入的值,要么是默認(rèn)值(0卿啡,null吟吝,false)。
JMM不保證未同步程序的執(zhí)行結(jié)果與該程序在順序一致性模型中的執(zhí)行結(jié)果一致颈娜。
未同步程序在JMM和順序一致性模型中的執(zhí)行特性有如下幾個(gè)差異:
1)順序一致性模型保證單線程內(nèi)的操作會(huì)按程序的順序執(zhí)行爸黄,而JMM不保證。
2)順序一致性模型保證所有線程只能看到一致的操作執(zhí)行順序揭鳞,而JMM不保證炕贵。
3)JMM不保證對(duì)64位的long型和double型變量的寫操作具有原子性,而順序一致性模型保證對(duì)所有的內(nèi)存讀/寫操作都具有原子性野崇。
第3個(gè)差異與處理器總線的工作機(jī)制密切相關(guān)称开。
總線事務(wù):每次處理器和內(nèi)存之間的數(shù)據(jù)傳遞都是通過一系列步驟來完成的,這一系列步驟稱之為總線事務(wù)乓梨。
總線事務(wù)包括讀事務(wù)和寫事務(wù)鳖轰。
在一個(gè)處理器執(zhí)行總線事務(wù)期間,總線會(huì)禁止其他的處理器和I/O設(shè)備執(zhí)行內(nèi)存的讀/寫扶镀。
總線的這些工作機(jī)制可以把所有處理器對(duì)內(nèi)存的訪問以串行化的方式來執(zhí)行蕴侣。在任意時(shí)間點(diǎn),最多只能有一個(gè)處理器可以訪問內(nèi)存這個(gè)特性確保了單個(gè)總線事務(wù)之中的內(nèi)存讀/寫操作具有原子性臭觉。
在一些32位的處理器上昆雀,如果要求對(duì)64位數(shù)據(jù)的寫操作具有原子性,會(huì)有比較大的開銷蝠筑。JMM對(duì)64位變量的寫操作不具有原子性狞膘。
JSR-133之前的舊內(nèi)存模型中,一個(gè)64位long/double型變量的讀/寫操作可以被拆分為兩個(gè)32位的讀/寫操作來執(zhí)行什乙。從JSR-133開始(JDK 1.5)挽封,僅僅只允許把64位long/double型變量的寫操作拆分為兩個(gè)32位的寫操作來執(zhí)行。任意讀操作在JSR-133中都必須具有原子性(即任意讀操作必須要在單個(gè)讀事務(wù)中執(zhí)行)臣镣。
4.volatile的內(nèi)存語義
①volatile的特性
理解volatile特性的一個(gè)好方法是把對(duì)volatile變量的單個(gè)讀/寫辅愿,看成是使用同一個(gè)鎖對(duì)這些單個(gè)讀/寫操作做了同步智亮。
class VolatileFeaturesExample {
volatile long vl = 0L; //使用volatile聲明64位的long型變量
public void set(long l) {
vl = l; //單個(gè)volatile變量的寫
}
public void getAndIncrement() {
vl++; //復(fù)合(多個(gè))volatile變量的讀/寫
}
public long get() {
return vl; //單個(gè)volatile變量的讀
}
}
假設(shè)有多個(gè)線程分別調(diào)用上面程序的3個(gè)方法,這個(gè)程序在語義上和下面等價(jià)点待。
class VolatileFeaturesExample {
long vl = 0L; //64位的long型普通變量
public synchronized void set(long l) { //對(duì)單個(gè)的普通變量的寫用同一個(gè)鎖同步
vl = l;
}
public void getAndIncrement() { //普通方法調(diào)用
long temp = get(); //調(diào)用已同步的讀方法
temp += 1L; //普通寫操作
set(temp); //調(diào)用已同步的寫方法
}
public synchronized long get() { //對(duì)單個(gè)的普通變量的讀用同一個(gè)鎖同步
return vl;
}
}
volatile變量自身具有下列特性:
- 可見性鸽素。對(duì)一個(gè)volatile變量的讀,總是能看到(任意線程)對(duì)這個(gè)volatile變量最后的寫入亦鳞。
- 原子性馍忽。對(duì)任意單個(gè)volatile變量的讀/寫具有原子性,但類似于volatile++這種復(fù)合操作不具有原子性燕差。
②volatile寫-讀建立的happens-before關(guān)系
從內(nèi)存語義的角度來說
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
......
}
}
}
1)根據(jù)程序次序規(guī)則:1 happens-before 2遭笋;3 happens-before 4。
2)根據(jù)volatile規(guī)則:2 happens-before 3徒探。
3)根據(jù)happens-before的傳遞性規(guī)則瓦呼,1 happens-before 4。
黑色箭頭:程序順序規(guī)則测暗。
橙色箭頭:volatile規(guī)則央串。
藍(lán)色箭頭:組合這些規(guī)則后提供的happens-before保證。
③volatile寫-讀的內(nèi)存語義
volatile寫的內(nèi)存語義:當(dāng)寫一個(gè)volatile變量時(shí)碗啄,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量值刷新到主內(nèi)存质和。
volatile讀的內(nèi)存語義:當(dāng)讀一個(gè)volatile變量時(shí),JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效稚字。線程接下來將從主內(nèi)存中讀取共享變量饲宿。
volatile寫和volatile讀的內(nèi)存語義總結(jié):
- 線程A寫一個(gè)volatile變量,實(shí)質(zhì)上是線程A向接下來將要讀這個(gè)volatile變量的某個(gè)線程發(fā)出了(其對(duì)共享變量所做修改的)消息胆描。
- 線程B讀一個(gè)volatile變量瘫想,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在寫這個(gè)volatile變量之前對(duì)共享變量所做的修改的)消息。
- 線程A寫一個(gè)volatile變量昌讲,隨后線程B讀這個(gè)volatile變量国夜,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息。
④volatile內(nèi)存語義的實(shí)現(xiàn)
JMM針對(duì)編譯器指定的volatile重排序規(guī)則表短绸。
為了實(shí)現(xiàn)volatile的內(nèi)存語義车吹,編譯器在生成字節(jié)碼時(shí),會(huì)在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序鸠按。
- 在每個(gè)volatile寫操作的前面插入一個(gè)StoreStore屏障礼搁。
- 在每個(gè)volatile寫操作的后面插入一個(gè)StoreLoad屏障。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadLoad屏障目尖。
- 在每個(gè)volatile讀操作的后面插入一個(gè)LoadStore屏障。
上述volatile寫和volatile讀的內(nèi)存屏障插入策略非常保守扎运。在實(shí)際執(zhí)行時(shí)瑟曲,只要不改變volatile寫-讀的內(nèi)存語義饮戳,編譯器可以根據(jù)具體情況省略不必要的屏障。
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2= 2;
void readAndWrite() {
int i = v1; //第一個(gè)volatile讀
int j = v2; //第二個(gè)volatile讀
a = i + j; //普通寫
v1 = i + 1; //第一個(gè)volatile寫
v2 = j + 2; //第二個(gè)volatile寫
}
...... //其他方法
}
最后的StoreLoad屏障不能省略洞拨。編譯器可能無法精確斷定第二個(gè)寫之后是否會(huì)有volatile讀或?qū)憽?/p>
由于X86處理器僅會(huì)對(duì)寫-讀操作重排序扯罐,所以在X86中,JMM僅需在volatile寫后面插入一個(gè)StoreLoad屏障即可正確實(shí)現(xiàn)volatile寫-讀的內(nèi)存語義烦衣。
⑤JSR-133為什么要增強(qiáng)volatile的內(nèi)存語義
在JSR-133之前的舊Java內(nèi)存模型中歹河,雖然不允許volatile變量之間重排序,但舊的Java內(nèi)存模型允許volatile變量與普通變量重排序花吟。在舊的內(nèi)存模型中秸歧,VolatileExample示例程序可能被重排序成下列時(shí)序來執(zhí)行:
為了提供一種比鎖更輕量級(jí)的線程之間通信的機(jī)制,JSR-133專家組決定增強(qiáng)volatile的內(nèi)存語義衅澈。如果想在程序中用volatile代替鎖键菱,請(qǐng)一定謹(jǐn)慎,具體詳情參閱Brian Goetz的文章《Java理論與實(shí)踐:正確使用Volatile變量》今布。
5.鎖的內(nèi)存語義
①鎖的釋放-獲取 建立的happens-before關(guān)系
class MonitorExample {
int a = 0;
public synchronized void writer() { //1
a++; //2
} //3
public synchronized void reader() { //4
int i = a; //5
......
} //6
}
線程A執(zhí)行writer()方法经备,線程B執(zhí)行reader()方法,根據(jù)happens-before規(guī)則部默,這個(gè)過程包含的happens-before關(guān)系:
1)根據(jù)程序次序規(guī)則:1 happens-before 2侵蒙,2 happens-before 3,4 happens-before 5傅蹂,5 happens-before 6蘑志。
2)根據(jù)監(jiān)視器鎖規(guī)則:3 happens-before 4。
3)根據(jù)happens-before的傳遞性:2 happens-before 5贬派。
黑色箭頭:程序順序規(guī)則急但。
橙色箭頭:監(jiān)視器鎖規(guī)則。
藍(lán)色箭頭:組合這些規(guī)則后提供的happens-before保證搞乏。
②鎖的釋放和獲取的內(nèi)存語義
當(dāng)線程釋放鎖時(shí)波桩,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存中的共享變量刷新到主內(nèi)存中。
當(dāng)線程獲取鎖時(shí)请敦,JMM會(huì)把該線程對(duì)應(yīng)的本地內(nèi)存置為無效镐躲。從而使得被監(jiān)視器保護(hù)的臨界區(qū)代碼必須從主內(nèi)存中讀取共享變量。
以MonitorExample為例:
A線程釋放鎖后侍筛,共享數(shù)據(jù)的狀態(tài)示意圖:
鎖獲取的狀態(tài)示意圖:
鎖釋放(對(duì)應(yīng)volatile寫)和鎖獲扔┰怼(對(duì)應(yīng)volatile讀)的內(nèi)存語義總結(jié):
- 線程A釋放一個(gè)鎖,實(shí)質(zhì)上是線程A向接下來將要將要獲取這個(gè)鎖的某個(gè)線程發(fā)出了(其對(duì)共享變量所做修改的)消息匣椰。
- 線程B獲取一個(gè)鎖裆熙,實(shí)質(zhì)上是線程B接收了之前某個(gè)線程發(fā)出的(在釋放這個(gè)鎖之前對(duì)共享變量所做的修改的)消息。
- 線程A釋放鎖,隨后線程B獲取這個(gè)鎖入录,這個(gè)過程實(shí)質(zhì)上是線程A通過主內(nèi)存向線程B發(fā)送消息蛤奥。
③鎖內(nèi)存語義的實(shí)現(xiàn)
class ReentrantLockExample {
int a = 0;
ReentrantLock lock = new ReentrantLock();
public void writer() {
lock.lock(); //獲取鎖
try {
a++;
} finally {
lock.unlock(); //釋放鎖
}
}
public void reader() {
lock.lock(); //獲取鎖
try {
int i = a;
......
} finally {
lock.unlock(); //釋放鎖
}
}
}
ReentrantLock的實(shí)現(xiàn)依賴于Java同步器框架AbstractQueuedSynchronizer(本文簡稱為AQS)。AQS使用一個(gè)整型的volatile變量state來維護(hù)同步狀態(tài)僚稿。
ReentrantLock分為公平鎖和非公平鎖凡桥。
公平鎖,lock()調(diào)用軌跡:
1)ReentrantLock:lock()蚀同。
2)FairSync:lock()缅刽。
3)AbstractQueuedSynchronizer:acquire(int arg)。
4)ReentrantLock.FairSync:tryAcquire(int acquires)蠢络。
第4步真正開始加鎖衰猛,下面是該方法源代碼
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); //獲取鎖的開始焦读,首先讀volatile變量state
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平鎖庇勃,unlock()調(diào)用軌跡:
1)ReentrantLock:unlock()晰骑。
2)AbstractQueuedSynchronizer:release(int arg)瞭空。
3)ReentrantReadWriteLock.Sync:tryRelease(int releases)症革。
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);//釋放鎖的最后区岗,寫volatile變量state
return free;
}
從上面源碼中谨娜,可以看出加鎖方法先讀volatile變量state洼哎。在鎖釋放的最后寫volatile變量state酸茴。根據(jù)volatile的happens-before規(guī)則分预,釋放鎖的線程在寫volatile變量之前可見的共享變量,在獲取鎖的線程讀取同一個(gè)volatile變量后將立即變得對(duì)獲取鎖的線程可見薪捍。
非公平鎖笼痹,lock()調(diào)用軌跡:
1)ReentrantLock:lock()。
2)ReentrantLock.NonfairSync:lock()酪穿。
3)AbstractQueuedSynchronizer:compareAndSetState(int expect, int update)凳干。
第3步真正開始加鎖,下面是該方法源代碼:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
非公平鎖被济,unlock()調(diào)用軌跡:同公平鎖
公平鎖和非公平鎖的內(nèi)存語義總結(jié):
- 公平鎖和非公平鎖釋放時(shí)救赐,最后都要寫一個(gè)volatile變量state。
- 公平鎖獲取時(shí)只磷,首先會(huì)讀volatile變量经磅。
- 非公平鎖獲取時(shí),首先會(huì)用CAS更新volatile變量钮追,這個(gè)操作同時(shí)具有volatile讀和volatile寫的內(nèi)存語義预厌。
鎖釋放-獲取的內(nèi)存語義的實(shí)現(xiàn)至少有兩種方式:
1)利用volatile變量的寫-讀所具有的內(nèi)存語義。
2)利用CAS所附帶的volatile讀和volatile寫的內(nèi)存語義元媚。
④concurrent包的實(shí)現(xiàn)
Java線程之間的通信有4種方式:
1)A線程寫volatile變量轧叽,隨后B線程讀這個(gè)volatile變量苗沧。
2)A線程寫volatile變量,隨后B線程用CAS更新這個(gè)volatile變量犹芹。
3)A線程用CAS更新一個(gè)volatile變量崎页,隨后B線程用CAS更新這個(gè)volatile變量鞠绰。
4)A線程用CAS更新一個(gè)volatile變量腰埂,隨后B線程讀這個(gè)volatile變量。
分析concurrent包的源代碼實(shí)現(xiàn)蜈膨,會(huì)發(fā)現(xiàn)一個(gè)通用化的實(shí)現(xiàn)模式:
- 聲明共享變量為volatile
- 使用CAS的原子條件更新來實(shí)現(xiàn)線程之間的同步屿笼。
- 配合以volatile的讀/寫和CAS所具有的volatile讀和寫的內(nèi)存語義來實(shí)現(xiàn)線程之間的通信。
6.final域的內(nèi)存語義
①final域的重排序規(guī)則
對(duì)于final域翁巍,編譯器和處理器要遵守兩個(gè)重排序規(guī)則:
- 在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final域的寫入驴一,與隨后把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量,這兩個(gè)操作之間不能重排序灶壶。
- 初次讀一個(gè)包含final域的對(duì)象的引用肝断,與隨后初次讀這個(gè)final域,這兩個(gè)操作之間不能重排序驰凛。
public class FinalExample {
int i; //普通變量
final int j; //final變量
static FinalExample obj;
public FinalExample () { //構(gòu)造函數(shù)
i = 1; //寫普通域
j = 2; //寫final域
}
public static void writer () { //寫線程A執(zhí)行
obj = new FinalExample ();
}
public static void reader () { //讀線程B執(zhí)行
FinalExample object = obj; //讀對(duì)象引用
int a = object.i; //讀普通域
int b = object.j; //讀final域
}
}
②寫final域的重排序規(guī)則
寫final域的重排序規(guī)則禁止把final域的寫重排序到構(gòu)造函數(shù)之外胸懈。規(guī)則實(shí)現(xiàn)包括:
- JMM禁止編譯器把final域的寫重排序到構(gòu)造函數(shù)之外。
- 編譯器會(huì)在final域的寫之后恰响,構(gòu)造函數(shù)return之前趣钱,插入一個(gè)StoreStore屏障。這個(gè)屏障禁止處理器把final域的寫重排序到構(gòu)造函數(shù)之外胚宦。
寫final域的重排序規(guī)則可以確保:在對(duì)象引用為任意線程可見之前首有,對(duì)象的final域已經(jīng)被正確的初始化過了,而普通域不具有這個(gè)保障枢劝。
③讀final域的重排序規(guī)則
讀final域的重排序規(guī)則:在一個(gè)線程中井联,初次讀對(duì)象引用與初次讀該對(duì)象包含的final域,JMM禁止處理器重排序這兩個(gè)操作您旁。編譯器會(huì)在讀final域操作的前面插入一個(gè)LoadLoad屏障烙常。
讀final域的重排序規(guī)則可以確保:在讀一個(gè)對(duì)象的final域之前,一定會(huì)先讀包含這個(gè)final域的對(duì)象的引用被冒,而普通域不具有這個(gè)保障军掂。
④final域?yàn)橐妙愋?/h4>
引用類型示例代碼:
public class FinalReferenceExample {
final int[] intArray; //final是引用類型
static FinalReferenceExample obj;
public FinalReferenceExample () { //構(gòu)造函數(shù)
intArray = new int[1]; //1
intArray[0] = 1; //2
}
public static void writerfOne () { //寫線程A執(zhí)行
obj = new FinalReferenceExample ();//3
}
public static void writerTwo () { //寫線程B執(zhí)行
obj.intArray[0] = 2; //4
}
public static void reader () { //讀線程C執(zhí)行
if (obj != null) { //5
int temp1 = obj.intArray[0]; //6
}
}
}
對(duì)于引用類型,寫final域的重排序規(guī)則對(duì)編譯器和處理器增加了如下約束:
在構(gòu)造函數(shù)內(nèi)對(duì)一個(gè)final引用的對(duì)象的成員域的寫入昨悼,與隨后在構(gòu)造函數(shù)外把這個(gè)被構(gòu)造對(duì)象的引用賦值給一個(gè)引用變量蝗锥,這兩個(gè)操作之間不能重排序。(2和3不能重排序)
線程C不一定能看到線程B的操作率触。如果要確保能看到终议,寫線程B和讀線程C之間需要使用同步原語(lock或volatile)來確保內(nèi)存可見性。
⑤為什么final引用不能從構(gòu)造函數(shù)內(nèi)“溢出”
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
public FinalReferenceEscapeExample () {
i = 1; //1寫final域
obj = this; //2 this引用在此“逸出”
}
public static void writer () {
new FinalReferenceEscapeExample ();
}
public static void reader () {
if (obj != null) { //3
int temp = obj.i; //4
}
}
}
從圖可以看出:在構(gòu)造函數(shù)返回前,被構(gòu)造對(duì)象的引用不能為其他線程所見穴张,因?yàn)榇藭r(shí)的final域可能還沒有被初始化细燎。在構(gòu)造函數(shù)返回后,任意線程都將保證能看到final域正確初始化之后的值皂甘。
我的理解:不能在構(gòu)造器中讓obj=this玻驻,這樣會(huì)造成this還沒有構(gòu)造完,引用就溢出了
⑥final語義在處理器中的實(shí)現(xiàn)
X86處理器中偿枕,final域的讀/寫不會(huì)插入任何內(nèi)存屏障璧瞬!
⑦JSR-133為什么要增強(qiáng)final的語義
在舊的Java內(nèi)存模型中,一個(gè)最嚴(yán)重的缺陷是線程可能看到final域的值會(huì)改變(先看到未初始化的默認(rèn)值渐夸,再看到初始化之后的值)嗤锉。最常見的例子是在舊的Java內(nèi)存模型中,String的值可能會(huì)改變墓塌。
JSR-133專家組增強(qiáng)了final的語義瘟忱,通過為final域增加寫和讀重排序規(guī)則,可以為Java程序員提供初始化安全保證:只要對(duì)象是正確構(gòu)造的(被構(gòu)造對(duì)象的引用在構(gòu)造函數(shù)中沒有"逸出")苫幢,那么不需要使用同步(lock和volatile的使用)就可以保證任意線程都能看到這個(gè)final域在構(gòu)造函數(shù)中被初始化之后的值访诱。
7.happens-before
①JMM的設(shè)計(jì)
double pi = 3.14; //A
double r = 1.0; //B
double area = pi * r * r; //C
A happens-before B, B happens-before C态坦, A happens-before C盐数。(1不必要,2和3是必需的)
JMM把happens-before要求禁止的重排序分為兩類:
- 會(huì)改變程序執(zhí)行結(jié)果的重排序伞梯。JMM要求編譯器和處理器必須禁止這種重排序玫氢。
- 不會(huì)改變程序執(zhí)行結(jié)果的重排序。JMM允許這種重排序谜诫。
JMM遵循一個(gè)基本原則:只要不改變程序的執(zhí)行結(jié)果(指的是單線程和正確同步的多線程程序)漾峡,編譯器和處理器怎么優(yōu)化都行。例如如果編譯器經(jīng)過細(xì)致的分析后喻旷,認(rèn)定一個(gè)鎖/volatile只會(huì)被單個(gè)線程訪問生逸,那么這個(gè)鎖/volatile可消除。
②happens-before的定義
happens-before關(guān)系的定義如下:(單線程和正確同步的多線程程序)
- 如果一個(gè)操作happens-before另一個(gè)操作且预,那么第一個(gè)操作的執(zhí)行結(jié)果對(duì)第二個(gè)操作可見槽袄。這是JMM對(duì)程序員的承諾。
- 兩個(gè)操作之間存在happens-before關(guān)系锋谐,如果重排序之后的執(zhí)行結(jié)果與原來結(jié)果一致遍尺,那么這種重排序并不非法。這是JMM對(duì)編譯器和處理器重排序的約束原則涮拗。
happens-before與as-if-serial類似:
- 執(zhí)行結(jié)果不被改變:as-if-serial保證單線程內(nèi)程序的乾戏,happens-before關(guān)系保證正確同步的多線程程序的迂苛。
- 給程序員創(chuàng)造一個(gè)幻境:as-if-serial語義-單線程程序按程序的順序來執(zhí)行,happens-before關(guān)系-正確同步的多線程是按happens-before指定的順序來執(zhí)行的鼓择。
③happens-before規(guī)則
happens-before規(guī)則:
- 程序順序規(guī)則:UI個(gè)線程中的每個(gè)操作三幻,happens-before于該線程中的任意后續(xù)操作。
- 監(jiān)視器鎖規(guī)則:對(duì)一個(gè)鎖的解鎖呐能,happens-before于隨后對(duì)這個(gè)鎖的加鎖念搬。
- volatile變量規(guī)則:對(duì)一個(gè)volatile域的寫,happens-before于任意后續(xù)對(duì)這個(gè)volatile域的讀催跪。
- 傳遞性:如果A happens-before B锁蠕, 且B happens-before C夷野,那么A happens-before C懊蒸。
- start()規(guī)則:如果線程A執(zhí)行操作ThreadB.start()(啟動(dòng)線程B),那么A線程的ThreadB.start()操作happens-before于線程B中的任意操作悯搔。
- join()規(guī)則:如果線程A執(zhí)行操作ThreadB.join()并成功返回骑丸,那么線程B中的任意操作happens-before于線程A從ThreadB.join()操作成功返回。
8.雙重檢查鎖定與延遲初始化
①雙重檢查鎖定的由來
在Java多線程程序中妒貌,有時(shí)候需要采用延遲初始化來降低初始化類和創(chuàng)建對(duì)象的開銷通危。雙重檢查鎖定是常見的延遲初始化技術(shù),但它是一個(gè)錯(cuò)誤的用法灌曙。
public class DoubleCheckedLocking { //1
private static Instance instance; //2
public static Instance getInstance() { //3
if (instance == null) { //4:第一次檢查
synchronized (DoubleCheckedLocking.class) { //5:加鎖
if (instance == null) { //6:第二次檢查
instance = new Instance(); //7:問題的根源出在這
} //8
} //9
return instance; //10
} //11
}
}
②問題的根源
第7行代碼可以分解為如下偽代碼:
memory = allocate()菊碟;//1:分配對(duì)象的內(nèi)存空間
ctorInstance(memory);//2:初始化對(duì)象
instance = memory在刺;//3:設(shè)置instance指向剛分配的內(nèi)存地址
上面?zhèn)未a中的2和3之間逆害,可能會(huì)被重排序:
memory = allocate();//1:分配對(duì)象的內(nèi)存空間
instance = memory蚣驼;//3:設(shè)置instance指向剛分配的內(nèi)存地址
//注意魄幕,此時(shí)對(duì)象還沒有被初始化!
ctorInstance(memory)颖杏;//2:初始化對(duì)象
解決方案:
- 不允許2和3重排序纯陨。
- 允許2和3重排序,但不允許其他線程“看到”這個(gè)重排序留储。
③基于volatile的解決方案
需要JDK5或更高版本翼抠。
public class DoubleCheckedLocking { //1
private volatile static Instance instance; //2
public static Instance getInstance() { //3
if (instance == null) { //4:第一次檢查
synchronized (DoubleCheckedLocking.class) { //5:加鎖
if (instance == null) { //6:第二次檢查
instance = new Instance(); //7:instance為volatile,現(xiàn)在沒有問題了
} //8
} //9
return instance; //10
} //11
}
}
④基于類初始化的解決方案
public class InstanceFactory {
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance;//這里將導(dǎo)致InstanceHolder類被初始化
}
}
初始化類的情況:
- T是一個(gè)類获讳,且一個(gè)T類型的實(shí)例被創(chuàng)建阴颖。
- T是一個(gè)類,且T中聲明的一個(gè)靜態(tài)方法被調(diào)用赔嚎。
- T中聲明的一個(gè)靜態(tài)字段被賦值膘盖。
- T中聲明的一個(gè)靜態(tài)字段被使用胧弛,而且這個(gè)字段不是一個(gè)常量字段。(InstanceFactory符合該情況)
- T是一個(gè)頂級(jí)父類侠畔,而且一個(gè)斷言語句嵌套在T內(nèi)部被執(zhí)行结缚。
Java語言規(guī)范規(guī)定,對(duì)于每一個(gè)類或接口软棺,都有一個(gè)唯一的初始化鎖與之對(duì)應(yīng)红竭。
類初始化的處理過程:虛構(gòu)出condition和state標(biāo)記,方便理解喘落。
- 第1階段:通過在Class對(duì)象上同步(即獲取Class對(duì)象的初始化鎖)茵宪,來控制類或接口的初始化。
- 第2階段:線程A執(zhí)行類的初始化瘦棋,同事線程B在初始化鎖對(duì)應(yīng)的condition上等待稀火。
- 第3階段:線程A設(shè)置state=initialized,然后喚醒在condition中等待的所有線程赌朋。
- 第4階段:線程B結(jié)束類的初始化處理凰狞。
- 第5階段:線程C執(zhí)行類的初始化處理。
線程B在第4階段的B1獲取同一個(gè)初始化鎖沛慢,根據(jù)happens-before關(guān)系赡若,保證:線程A執(zhí)行類的初始化時(shí)的寫入操作(執(zhí)行類的靜態(tài)初始化和初始化類中聲明的靜態(tài)字段),線程B一定能看到团甲。
9.Java內(nèi)存模型綜述
①處理器的內(nèi)存模型
內(nèi)存模型類型:
- 放松程序中寫-讀操作的順序逾冬,由此產(chǎn)生了Total Store Ordering內(nèi)存模型。簡稱TSO躺苦。
- 在TSO基礎(chǔ)上身腻,繼續(xù)放松程序中寫-寫操作的順序,由此產(chǎn)生了Partial Store Order內(nèi)存模型圾另。簡稱PSO霸株。
- 在TSO和PSO基礎(chǔ)上,繼續(xù)放松程序中讀-寫和讀-讀操作的順序集乔,由此產(chǎn)生了Relaxed Memory Order內(nèi)存模型(檢查RMO)和PowerPC內(nèi)存模型去件。
②各種內(nèi)存模型之間的關(guān)系
③JMM的內(nèi)存可見性保證
④JSR-133對(duì)舊內(nèi)存模型的修補(bǔ)
主要修補(bǔ)有兩個(gè):
- 增強(qiáng)volatile的內(nèi)存語義。舊內(nèi)存模型允許volatile變量與普通變量重排序扰路。
- 增強(qiáng)final的內(nèi)存語義尤溜。舊內(nèi)存模型多次讀取同一個(gè)final變量的值可能會(huì)不相同。