JAVA併發編程的藝術讀書筆記

雙重檢查鎖定與延遲初始化

在Java多線程程序中,有時候須要採用延遲初始化來下降初始化類和建立對象的開銷。雙重檢查鎖定是常見的延遲初始化技術,但它是一個錯誤的用法編程

非線程安全的延遲初始化對象

package 雙重檢查鎖定與延遲初始化;
//非線程安全的延遲初始化對象
public class UnsafeLazyInitialization {
    private static UnsafeLazyInitialization instance;

    public static UnsafeLazyInitialization getInstance(){
        if(instance == null)//代碼1
            instance = new UnsafeLazyInitialization();//代碼2
        return instance;
    }
}

在上面的類中,假設線程A執行代碼1的同時,B線程執行代碼2。此時,線程A可能會看到instance引用的對象尚未完成初始化。安全

線程安全的延遲初始化

package 雙重檢查鎖定與延遲初始化;
//線程安全的延遲初始化,因爲對getInstance方法作了同步處理,sychronized將致使性能開銷
public class SafeLazyInitialization {
    private static SafeLazyInitialization instance;

    public synchronized static SafeLazyInitialization getInstance(){
        if(instance == null)
            instance = new SafeLazyInitialization();
        return instance;
    }
}

因爲對getInstance方法作了同步處理,synchronized將致使性能開銷。getInstance方法若是被多個線程頻繁調用,將致使程序執行性能的降低。多線程

雙重檢查鎖定

public class DoubleCheckedLocking {
    private static DoubleCheckedLocking instance;

    public static DoubleCheckedLocking getInstance(){
        if(instance == null){//1處
            synchronized(DoubleCheckedLocking.class){
                if(instance == null)
                    instance = new DoubleCheckedLocking();//2處
            }
        }
        return instance;
    }
}

在A線程位於同步代碼塊2處,初始化對象(但還未初始化完成的時候),有可能有另外的線程B運行到1處。此時instance檢查不爲null,但instance返回的卻不是一個完整的對象。併發

爲何instance沒有被A真正初始化的時候,其指向不爲null呢?緣由在於編譯器的指令重排序app

建立一個對象的過程可由以下三步僞代碼表示:性能

1.memory = allocate() //分配對象的內存空間spa

2.ctorInstance(memory) //初始化對象線程

3.instance = memory //設置instance指向分配的內存地址3d

上面的建立對象的過程是在一個單線程中完成的, JMM保證了重排序不會改變單線程內的程序執行結果。換句話說,JAVA容許那些在單線程內,不會改變單線程程序執行結果對的指令重排序。上述三個步驟在一些JIT編譯器上被單線程執行的時候,爲了提升程序的執行性能,有可能會被重排序且不影響單線程下的執行結果:code

1.memory = allocate() //分配對象的內存空間

2.instance = memory //設置instance指向分配的內存地址

3.ctorInstance(memory) //初始化對象

這樣,instance 在單線程內會先被指向內存分配地址(此時檢查instance不爲null),然後這個地址內才真正存放初始化完成的對象數據。這樣線程A執行到步驟2,將instance指向內存地址以致於對instance做檢查不爲null的時候(但實際上該地址內沒有初始化完成的對象數據),線程B運行到了程序的1處,對instance做了不爲null的檢查後,直接返回了這個「虛有其表」的instance。後續程序若是用到了這個對象,必然致使程序的錯誤。

解決方法

在知曉了問題的根源後,咱們能夠採用兩個辦法來實現線程安全的延遲初始化:

1. 不容許2和3重排序

2. 容許2和3重排序,但不容許其餘線程「看到」這個重排序

基於volatile的解決方案

基於前面的雙重檢查鎖定來實現延遲初始化的方案,只須要作一點小的修改(將instance聲明爲volatile類型),就能夠實現線程安全的延遲初始化

package 雙重檢查鎖定與延遲初始化;
public class SafeDoubleCheckedLocking {
    private volatile static SafeDoubleCheckedLocking instance;

    public static SafeDoubleCheckedLocking getInstance(){
        if(instance==null){
            synchronized (SafeDoubleCheckedLocking.class){
                if(instance == null)
                    instance = new SafeDoubleCheckedLocking();//instance爲volatile,禁止了volatile變量的指令重排序,如今沒有問題了。
            }
        }
        return instance;
    }
}

當聲明對象引用爲volatile後,前面僞代碼的2和3之間的重排序在多線程環境中將被禁止,從而解決了這個問題。

基於類初始化的解決方案

JVM在類的初始化階段(即在class被加載後,且被線程使用以前),會執行類的初始化。在執行類的初始化期間,JVM會取獲取一個鎖。這個鎖能夠同步多個線程對同一個類的初始化。。這個方案的實質是,容許僞代碼中2和3的重排序,但不容許非構造線程(指線程B)「看到」這個重排序。

package 雙重檢查鎖定與延遲初始化;
/*JVM在類的初始化階段(即在class被加載後,且被線程使用以前),會執行類的初始化。
* 在執行類的初始化期間,JVM會取獲取一個鎖。這個鎖能夠同步多個線程對同一個類的初始化。*/
public class InstanceFactory {
    private static class InstanceHolder{
        public static InstanceFactory instance = new InstanceFactory();
    }

    public static InstanceFactory getInstance(){
        return InstanceHolder.instance;
    }
}

JAVA語言規範規定,對於每個類或者接口C,都有惟一一個的初始化鎖LC與之對應。JVM在初始化期間會獲取這個初始化鎖,而且每一個線程至少獲取了一次鎖來確保這個類已經被初始化過了。

爲了更好的說明類初始化過程當中的同步處理機制,本書做者人爲的把類初始化的處理過程分紅了5個階段:

第1階段:經過在Class對象上同步(即獲取Class對象的初始化鎖),來控制類或接口的初始化。這個獲取鎖的線程會一直等待,直到當前線程可以獲取到這個初始化鎖。

假設Class對象當前尚未被初始化(初始化狀態state被標記爲noInitialization),且有兩個線程A和B試圖同時初始化這個Class對象。

2段:A的初始化,同時線B在初始化鎖對應condition上等待。

 

3段:Astate=initialized,而後醒在condition中等待的全部程。

4段:B的初始化理。

A在第2段的A1的初始化,並在第3段的A4放初始化B在第4段的B1取同一個初始化,並在第4段的B4以後纔開始訪問這。根據Java內存模型範的鎖規則裏將存在以下的happens-before關係。happens-before關係將保A的初始化的寫入操做(的靜初始化和初始化中聲明的靜字段),B必定能看到 

5段:C的初始化的理。

 

在第3段以後,完成了初始化。所以C在第5段的初始化程相對簡一些(前面的AB初始化程都經歷了兩次鎖獲-鎖釋放,而C始化理只須要經歷一次鎖獲-鎖釋放)。A在第2段的A1的初始化,並在第3段的A4C在第5段的C1取同一個,並在在第5段的C4以後纔開始訪問這。根據Java內存模型範的鎖規,將存在以下的happens-before關係。happens-before關係將保A的初始化的寫入操做,C必定能看到 。

 這個講起來有點複雜,見《JAVA併發編程的藝術》p72-p78

相關文章
相關標籤/搜索