Java語言中有一個「先行發生」(happen—before)的規則,它是Java內存模型中定義的兩項操做之間的偏序關係,若是操做A先行發生於操做B,其意思就是說,在發生操做B以前,操做A產生的影響都能被操做B觀察到,「影響」包括修改了內存中共享變量的值、發送了消息、調用了方法等,它與時間上的前後發生基本沒有太大關係。這個原則特別重要,它是判斷數據是否存在競爭、線程是否安全的主要依據。
java
舉例來講,假設存在以下三個線程,分別執行對應的操做:web
線程A中執行以下操做:i=1
線程B中執行以下操做:j=i
線程C中執行以下操做:i=2
假設線程A中的操做」i=1「 happen—before線程B中的操做「j=i」,那麼就能夠保證在線程B的操做執行後,變量j的值必定爲1,即線程B觀察到了線程A中操做「i=1」所產生的影響;如今,咱們依然保持線程A和線程B之間的happen—before關係,同時線程C出如今了線程A和線程B的操做之間,可是C與B並無happen—before關係,那麼j的值就不肯定了,線程C對變量i的影響可能會被線程B觀察到,也可能不會,這時線程B就存在讀取到不是最新數據的風險,不具有線程安全性。數組
下面是Java內存模型中的八條可保證happen—before的規則,它們無需任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們進行隨機地重排序。
緩存
一、程序次序規則:在一個單獨的線程中,按照程序代碼的執行流順序,(時間上)先執行的操做happen—before(時間上)後執行的操做。安全
二、管理鎖定規則:一個unlock操做happen—before後面(時間上的前後順序,下同)對同一個鎖的lock操做。微信
三、volatile變量規則:對一個volatile變量的寫操做happen—before後面對該變量的讀操做。app
四、線程啓動規則:Thread對象的start()方法happen—before此線程的每個動做。dom
五、線程終止規則:線程的全部操做都happen—before對此線程的終止檢測,能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。ide
六、線程中斷規則:對線程interrupt()方法的調用happen—before發生於被中斷線程的代碼檢測到中斷時事件的發生。函數
七、對象終結規則:一個對象的初始化完成(構造函數執行結束)happen—before它的finalize()方法的開始。
八、傳遞性:若是操做A happen—before操做B,操做B happen—before操做C,那麼能夠得出A happen—before操做C。
」時間上執行的前後順序「與」happen—before「之間有何不一樣呢?
一、首先來看操做A在時間上先與操做B發生,是否意味着操做A happen—before操做B?
一個經常使用來分析的例子以下:
private int value = 0;
public int get(){
return value;
}
public void set(int value){
this.value = value;
}
假設存在線程A和線程B,線程A先(時間上的先)調用了setValue(3)操做,而後(時間上的後)線程B調用了同一對象的getValue()方法,那麼線程B獲得的返回值必定是3嗎?
對照以上八條happen—before規則,發現沒有一條規則適合於這裏的value變量,從而咱們能夠斷定線程A中的setValue(3)操做與線程B中的getValue()操做不存在happen—before關係。所以,儘管線程A的setValue(3)在操做時間上先於操做B的getvalue(),但沒法保證線程B的getValue()操做必定觀察到了線程A的setValue(3)操做所產生的結果,也便是getValue()的返回值不必定爲3(有多是以前setValue所設置的值)。這裏的操做不是線程安全的。
所以,」一個操做時間上先發生於另外一個操做「並不表明」一個操做happen—before另外一個操做「。
解決方法:能夠將setValue(int)方法和getValue()方法均定義爲synchronized方法,也能夠把value定義爲volatile變量(value的修改並不依賴value的原值,符合volatile的使用場景),分別對應happen—before規則的第2和第3條。注意,只將setValue(int)方法和getvalue()方法中的一個定義爲synchronized方法是不行的,必須對同一個變量的全部讀寫同步,才能保證不讀取到陳舊的數據,僅僅同步讀或寫是不夠的 。
二、其次來看,操做A happen—before操做B,是否意味着操做A在時間上先與操做B發生?
看有以下代碼:
x = 1;
y = 2;
假設同一個線程執行上面兩個操做:操做A:x=1和操做B:y=2。根據happen—before規則的第1條,操做A happen—before 操做B,可是因爲編譯器的指令重排序(Java語言規範規定了JVM線程內部維持順序化語義,也就是說只要程序的最終結果等同於它在嚴格的順序化環境下的結果,那麼指令的執行順序就可能與代碼的順序不一致。這個過程經過叫作指令的重排序。指令重排序存在的意義在於:JVM可以根據處理器的特性(CPU的多級緩存系統、多核處理器等)適當的從新排序機器指令,使機器指令更符合CPU的執行特色,最大限度的發揮機器的性能。在沒有同步的狀況下,編譯器、處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整)等緣由,操做A在時間上有可能後於操做B被處理器執行,但這並不影響happen—before原則的正確性。
所以,」一個操做happen—before另外一個操做「並不表明」一個操做時間上先發生於另外一個操做「。
最後,一個操做和另外一個操做一定存在某個順序,要麼一個操做或者是先於或者是後於另外一個操做,或者與兩個操做同時發生。同時發生是徹底可能存在的,特別是在多CPU的狀況下。而兩個操做之間卻可能沒有happen-before關係,也就是說有可能發生這樣的狀況,操做A不happen-before操做B,操做B也不happen-before操做A,用數學上的術語happen-before關係是個偏序關係。兩個存在happen-before關係的操做不可能同時發生,一個操做A happen-before操做B,它們一定在時間上是徹底錯開的,這實際上也是同步的語義之一(獨佔訪問)。
DCL即雙重檢查加鎖。下面是一個典型的在單例模式中使用DCL的例子:
public class LazySingleton {
private int someField;
private static LazySingleton instance;
private LazySingleton() {
this.someField = new Random().nextInt(200)+1; // (1)
}
public static LazySingleton getInstance() {
if (instance == null) { // (2)
synchronized(LazySingleton.class) { // (3)
if (instance == null) { // (4)
instance = new LazySingleton(); // (5)
}
}
}
return instance; // (6)
}
public int getSomeField() {
return this.someField; // (7)
}
}
這裏獲得單一的instance實例是沒有問題的,問題的關鍵在於儘管獲得了Singleton的正確引用,可是卻有可能訪問到其成員變量的不正確值。具體來講Singleton.getInstance().getSomeField()有可能返回someField的默認值0。若是程序行爲正確的話,這應當是不可能發生的事,由於在構造函數裏設置的someField的值不可能爲0。爲也說明這種狀況理論上有可能發生,咱們只須要說明語句(1)和語句(7)並不存在happen-before關係。
假設線程Ⅰ是初次調用getInstance()方法,緊接着線程Ⅱ也調用了getInstance()方法和getSomeField()方法,咱們要說明的是線程Ⅰ的語句(1)並不happen-before線程Ⅱ的語句(7)。線程Ⅱ在執行getInstance()方法的語句(2)時,因爲對instance的訪問並無處於同步塊中,所以線程Ⅱ可能觀察到也可能觀察不到線程Ⅰ在語句(5)時對instance的寫入,也就是說instance的值可能爲空也可能爲非空。咱們先假設instance的值非空,也就觀察到了線程Ⅰ對instance的寫入,這時線程Ⅱ就會執行語句(6)直接返回這個instance的值,而後對這個instance調用getSomeField()方法,該方法也是在沒有任何同步狀況被調用,所以整個線程Ⅱ的操做都是在沒有同步的狀況下調用 ,這時咱們便沒法利用上述8條happen-before規則獲得線程Ⅰ的操做和線程Ⅱ的操做之間的任何有效的happen-before關係(主要考慮規則的第2條,但因爲線程Ⅱ沒有在進入synchronized塊,所以不存在lock與unlock鎖的問題),這說明線程Ⅰ的語句(1)和線程Ⅱ的語句(7)之間並不存在happen-before關係,這就意味着線程Ⅱ在執行語句(7)徹底有可能觀測不到線程Ⅰ在語句(1)處對someFiled寫入的值,這就是DCL的問題所在。很荒謬,是吧?DCL本來是爲了逃避同步,它達到了這個目的,也正是由於如此,它最終受到懲罰,這樣的程序存在嚴重的bug,雖然這種bug被發現的機率絕對比中×××的機率還要低得多,並且是轉瞬即逝,更可怕的是,即便發生了你也不會想到是DCL所引發的。
前面咱們說了,線程Ⅱ在執行語句(2)時也有可能觀察空值,若是是種狀況,那麼它須要進入同步塊,並執行語句(4)。在語句(4)處線程Ⅱ還可以讀到instance的空值嗎?不可能。這裏由於這時對instance的寫和讀都是發生在同一個鎖肯定的同步塊中,這時讀到的數據是最新的數據。爲也加深印象,我再用happen-before規則分析一遍。線程Ⅱ在語句(3)處會執行一個lock操做,而線程Ⅰ在語句(5)後會執行一個unlock操做,這兩個操做都是針對同一個鎖--Singleton.class,所以根據第2條happen-before規則,線程Ⅰ的unlock操做happen-before線程Ⅱ的lock操做,再利用單線程規則,線程Ⅰ的語句(5) -> 線程Ⅰ的unlock操做,線程Ⅱ的lock操做 -> 線程Ⅱ的語句(4),再根據傳遞規則,就有線程Ⅰ的語句(5) -> 線程Ⅱ的語句(4),也就是說線程Ⅱ在執行語句(4)時可以觀測到線程Ⅰ在語句(5)時對Singleton的寫入值。接着對返回的instance調用getSomeField()方法時,咱們也能獲得線程Ⅰ的語句(1) -> 線程Ⅱ的語句(7)(因爲線程Ⅱ有進入synchronized塊,根據規則2可得),這代表這時getSomeField可以獲得正確的值。可是僅僅是這種狀況的正確性並不妨礙DCL的不正確性,一個程序的正確性必須在全部的狀況下的行爲都是正確的,而不能有時正確,有時不正確。
對DCL的分析也告訴咱們一條經驗原則:對引用(包括對象引用和數組引用)的非同步訪問,即便獲得該引用的最新值,卻並不能保證也能獲得其成員變量(對數組而言就是每一個數組元素)的最新值。
解決方案:
一、最簡單並且安全的解決方法是使用static內部類的思想,它利用的思想是:一個類直到被使用時才被初始化,而類初始化的過程是非並行的,這些都有JLS保證。
以下述代碼:
public class Singleton {
private Singleton() {}
// Lazy initialization holder class idiom for static fields
private static class InstanceHolder {
private static final Singleton instance = new Singleton();
}
public static Singleton getSingleton() {
return InstanceHolder.instance;
}
}
二、另外,能夠將instance聲明爲volatile,即
private volatile static LazySingleton instance;
這樣咱們即可以獲得,線程Ⅰ的語句(5) -> 語線程Ⅱ的句(2),根據單線程規則,線程Ⅰ的語句(1) -> 線程Ⅰ的語句(5)和語線程Ⅱ的句(2) -> 語線程Ⅱ的句(7),再根據傳遞規則就有線程Ⅰ的語句(1) -> 語線程Ⅱ的句(7),這表示線程Ⅱ可以觀察到線程Ⅰ在語句(1)時對someFiled的寫入值,程序可以獲得正確的行爲。
注:
一、volatile屏蔽指令重排序的語義在JDK1.5中才被徹底修復,此前的JDK中及時將變量聲明爲volatile,也仍然不能徹底避免重排序所致使的問題(主要是volatile變量先後的代碼仍然存在重排序問題),這點也是在JDK1.5以前的Java中沒法安全使用DCL來實現單例模式的緣由。
二、把volatile寫和volatile讀這兩個操做綜合起來看,在讀線程B讀一個volatile變量後,寫線程A在寫這個volatile變量以前,全部可見的共享變量的值都將當即變得對讀線程B可見。
三、 在java5以前對final字段的同步語義和其它變量沒有什麼區別,在java5中,final變量一旦在構造函數中設置完成(前提是在構造函數中沒有泄露this引用),其它線程一定會看到在構造函數中設置的值。而DCL的問題正好在於看到對象的成員變量的默認值,所以咱們能夠將LazySingleton的someField變量設置成final,這樣在java5中就可以正確運行了。
來源:https://blog.csdn.net/ns_code/article/details/17348313