轉(zhuǎn)載:http://www.cnblogs.com/leesf456/p/5291484.html
1痒留、volatile
1.1 介紹
關鍵字volatile是Java虛擬機提供的最輕量級的同步機制。
當一個變量定義為volatile時蠢沿,它將具備兩種特性:(1)可見性伸头;(2)禁止指令重排序。
- 可見性
當一條線程修改了這個變量的值舷蟀,新值對于其他線程來說是可以立即獲得的恤磷,但是基于volatile變量的操作并不是安全的(如自增操作),不能保證原子性野宜。- 禁止指令重排序
不允許對volatile操作指令進行重排序扫步。
1.2 volatile 的 happens - before 關系
在volatile變量與happens - before 之間是什么關系呢,我們通過一個示例說明:
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
}
}
}
說明:假定線程A先執(zhí)行writer方法匈子,線程B后執(zhí)行reader方法河胎,那么根據(jù)happens - before關系,我們可以知道:
- 根據(jù)程序順序規(guī)則虎敦,1 happens before 2; 3 happens before 4游岳。
- 根據(jù) volatile變量規(guī)則政敢,2 happens before 3。
- 根據(jù) happens before 的傳遞性胚迫,1 happens before 4喷户。
具體的happens - before圖形化如下:
1.3 volatile 讀寫內(nèi)存語義
讀內(nèi)存語義。當讀一個 volatile 變量時晌区,JMM 會把該線程對應的本地內(nèi)存置為無效。線程之后將從主內(nèi)存中讀取共享變量通贞。
寫內(nèi)存語義朗若。當寫一個 volatile 變量時,JMM 會把該線程對應的本地內(nèi)存中的共享變量值刷新到主內(nèi)存昌罩。這樣就保證了volatile的內(nèi)存可見性哭懈。
volatile讀寫內(nèi)存語義總結為如下三條:
線程 A 寫一個 volatile 變量,實質(zhì)上是線程 A 向接下來將要讀這個 volatile 變量的某個線程發(fā)出了(其對共享變量所在修改的)消息茎用。
線程 B 讀一個 volatile 變量遣总,實質(zhì)上是線程 B 接收了之前某個線程發(fā)出的(在寫這個 volatile 變量之前對共享變量所做修改的)消息。
線程 A 寫一個 volatile 變量轨功,隨后線程 B 讀這個 volatile 變量旭斥,這個過程實質(zhì)上是線程 A 通過主內(nèi)存向線程 B 發(fā)送消息。
1.4 volatile 內(nèi)存語義的實現(xiàn)
前面講到古涧,volatile變量會禁止編譯器垂券、處理器重排序。下面是volatile具體的排序規(guī)則表:
為了實現(xiàn) volatile 的內(nèi)存語義羡滑,編譯器在生成字節(jié)碼時菇爪,會在指令序列中插入內(nèi)存屏障來禁止特定類型的處理器重排序。對于編譯器來說柒昏,發(fā)現(xiàn)一個最優(yōu)布置來最小化插入屏障的總數(shù)幾乎不可能凳宙,為此,JMM 采取保守策略职祷。下面是基于保守策略的 JMM 內(nèi)存屏障插入策略:
在每個 volatile 寫操作的前面插入一個 StoreStore 屏障氏涩。
在每個 volatile 寫操作的后面插入一個 StoreLoad 屏障(對volatile寫、普通讀寫實現(xiàn)為不允許重排序有梆,可能會影響性能)削葱。
在每個 volatile 讀操作的后面插入一個 LoadLoad 屏障。
在每個 volatile 讀操作的后面插入一個 LoadStore 屏障(普通讀寫淳梦、volatile讀實現(xiàn)為不允許重排序析砸,可能會影響性能)。
下面通過一個示例展示volatile的內(nèi)存語義:
class VolatileBarrierExample {
int a;
volatile int v1 = 1;
volatile int v2 = 2;
void readAndWrite() {
int i = v1; // 第一個 volatile 讀
int j = v2; // 第二個 volatile 讀
a = i + j; // 普通寫
v1 = i + 1; // 第一個 volatile 寫
v2 = j * 2; // 第二個 volatile 寫
}
}
根據(jù)程序和插入屏障的規(guī)則爆袍,最后的指令序列如下圖所示:
說明:編譯器首繁、處理器會根據(jù)上下文進行優(yōu)化作郭,并不是完全按照保守策略進行插入相應的屏障指令。
2弦疮、鎖
2.1 介紹
鎖是Java并發(fā)編程中最重要的同步機制夹攒。
鎖除了讓臨界區(qū)互斥執(zhí)行外,還可以讓釋放鎖的線程向獲取同一個鎖的線程發(fā)送消息胁塞。
2.2 鎖的 happens - before 關系
下面一個示例展示了鎖的使用:
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()方法江掩。該程序的happens - before關系如下:
根據(jù)程序順序規(guī)則放仗,1 happens before 2, 2 happens before 3; 4 happens before 5, 5 happens before 6郭蕉。
根據(jù)監(jiān)視器鎖規(guī)則酒觅,3 happens before 4危队。
根據(jù)傳遞性谊惭,2 happens before 5空骚。
圖形化表示如下:
2.3 鎖的內(nèi)存語義
1. 當線程釋放鎖時叠国,JMM會把該線程對應的工作內(nèi)存中的共享變量刷新到主內(nèi)存中衩匣,以確保之后的線程可以獲取到最新的值蕾总。
2. 當線程獲取鎖時,JMM會把該線程對應的本地內(nèi)存置為無效琅捏。從而使得被監(jiān)視器保護的臨界區(qū)代碼必須要從主內(nèi)存中去讀取共享變量生百。
鎖的釋放與獲取總結為如下三條:
線程 A 釋放一個鎖,實質(zhì)上是線程 A 向接下來將要獲取這個鎖的某個線程發(fā)出了(線程 A 對共享變量所做修改的)消息柄延。
線程 B 獲取一個鎖置侍,實質(zhì)上是線程 B 接收了之前某個線程發(fā)出的(在釋放這個鎖之前對共享變量所做修改的)消息。
線程 A 釋放鎖拦焚,隨后線程 B 獲取這個鎖蜡坊,這個過程實質(zhì)上是線程 A 通過主內(nèi)存向線程 B 發(fā)送消息。
2.4 鎖內(nèi)存語義的實現(xiàn)
鎖的內(nèi)存語義的具體實現(xiàn)借助了volatile變量的內(nèi)存語義的實現(xiàn)赎败。
3.final
3.1 介紹
對于 final 域秕衙,編譯器和處理器要遵守兩個重排序規(guī)則:
final 寫:“構造函數(shù)內(nèi)對一個final域的寫入”,與“隨后把這個被構造對象的引用賦值給一個引用變量”僵刮,這兩個操作之間不能重排序据忘。
final 讀:“初次讀一個包含final域的對象的引用”,與“隨后初次讀對象的final域”搞糕,這兩個操作之間不能重排序勇吊。
如下面示例展示了final兩種重排序規(guī)則:
public final class FinalExample {
final int i;
public FinalExample() {
i = 3; // 1
}
public static void main(String[] args) {
FinalExample fe = new FinalExample(); // 2
int ele = fe.i; // 3
}
}
說明: 操作1與操作2符合重排序規(guī)則1,不能重排窍仰,操作2與操作3符合重排序規(guī)則2汉规,不能重排。
由下面的示例我們來具體理解final域的重排序規(guī)則。
public class FinalExample {
int i; // 普通變量
final int j; // final變量
static FinalExample obj; // 靜態(tài)變量
public void FinalExample () { // 構造函數(shù)
i = 1; // 寫普通域
j = 2; // 寫final域
}
public static void writer () { // 寫線程A執(zhí)行
obj = new FinalExample();
}
public static void reader () { // 讀線程B執(zhí)行
FinalExample object = obj; // 讀對象引用
int a = object.i; // 讀普通域
int b = object.j; // 讀final域
}
}
說明:假設線程A先執(zhí)行writer()方法针史,隨后另一個線程B執(zhí)行reader()方法晶伦。下面我們通過這兩個線程的交互來說明這兩個規(guī)則。
3.2 寫 final 域 重排序規(guī)則
寫 final 域的重排序規(guī)則禁止把 final 域的寫重排序到構造函數(shù)之外啄枕。這個規(guī)則的實現(xiàn)包含下面兩個方面:
JMM 禁止編譯器把 final 域的寫重排序到構造函數(shù)之外婚陪。
編譯器會在 final 域的寫之后,構造函數(shù) return 之前频祝,插入一個 StoreStore 屏障泌参。這個屏障禁止處理器把 final 域的寫重排序到構造函數(shù)之外。
writer方法的obj = new FinalExample();其實包括兩步常空,首先是在堆上分配一塊內(nèi)存空間創(chuàng)建FinalExample對象沽一,然后將這個對象的地址賦值給obj引用。假設線程 B 讀對象引用與讀對象的成員域之間沒有重排序窟绷,則可能的時序圖如下:
說明:寫普通域的操作被編譯器重排序到了構造函數(shù)之外锯玛,讀線程 B 錯誤的讀取了普通變量 i 初始化之前的值咐柜。而寫 final 域的操作兼蜈,被寫 final 域的重排序規(guī)則 “限定”在了構造函數(shù)之內(nèi),讀線程 B 正確的讀取了 final 變量初始化之后的值拙友。寫 final 域的重排序規(guī)則可以確保:在對象引用為任意線程可見之前为狸,對象的 final 域已經(jīng)被正確初始化過了,而普通域不具有這個保障遗契。以上圖為例辐棒,在讀線程 B “看到”對象引用 obj 時,很可能 obj 對象還沒有構造完成(對普通域 i 的寫操作被重排序到構造函數(shù)外牍蜂,此時初始值 2 還沒有寫入普通域 i)漾根。
3.3 讀 final 域 重排序規(guī)則
讀 final 域的重排序規(guī)則如下:
在一個線程中,"初次讀對象引用"與"初次讀該對象包含的 final 域"鲫竞,JMM 禁止處理器重排序這兩個操作(注意辐怕,這個規(guī)則僅僅針對處理器)。
編譯器會在讀 final 域操作的前面插入一個 LoadLoad 屏障从绘。
初次讀對象引用與初次讀該對象包含的 final 域寄疏,這兩個操作之間存在間接依賴關系。由于編譯器遵守間接依賴關系僵井,因此編譯器不會重排序這兩個操作陕截。大多數(shù)處理器也會遵守間接依賴,大多數(shù)處理器也不會重排序這兩個操作批什。但有少數(shù)處理器允許對存在間接依賴關系的操作做重排序(比如 alpha 處理器)农曲,這個規(guī)則就是專門用來針對這種處理器。
reader方法包含三個操作:① 初次讀引用變量 obj驻债。② 初次讀引用變量 obj 指向?qū)ο蟮钠胀ㄓ?i朋蔫。③ 初次讀引用變量 obj 指向?qū)ο蟮?final 域 j罚渐。
假設寫線程 A 沒有發(fā)生任何重排序,同時程序在不遵守間接依賴的處理器上執(zhí)行驯妄,下面是一種可能的執(zhí)行時序:
3.4 如果 final域 是引用類型
上面我們的例子中荷并,final域是基本數(shù)據(jù)類型,如果final與為引用類型的話情況會稍微不同青扔。對于引用類型源织,寫 final 域的重排序規(guī)則對編譯器和處理器增加了如下約束:
在構造函數(shù)內(nèi)對一個 final 引用的對象的成員域的寫入,與隨后在構造函數(shù)外把這個被構造對象的引用賦值給一個引用變量微猖,這兩個操作之間不能重排序谈息。
3.5 final 逸出
寫 final 域 的重排序規(guī)則可以確保:
在引用變量為任意線程可見之前,該引用變量指向的對象的 final 域已經(jīng)在構造函數(shù)中被正確初始化過了凛剥。
其實要得到這個效果侠仇,還需要一個保證:
在構造函數(shù)內(nèi)部,不能讓這個被構造對象的引用為其他線程可見犁珠,也就是對象引用不能在構造函數(shù)中“逸出”逻炊。