1. 程式人生 > 其它 >雙重檢查鎖的漏洞 -- volatile 指令進行重排

雙重檢查鎖的漏洞 -- volatile 指令進行重排

public class SimpleSingleton7 {

    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {
    }

    public static SimpleSingleton7 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton7.class) {
                if (INSTANCE == null
) { INSTANCE = new SimpleSingleton7(); } } } return INSTANCE; } }

如果不新增 volatitle,    java虛擬機器實際上會做一些優化,對一些程式碼指令進行重排。 變成:

public static SimpleSingleton4 getInstance() {
    if (INSTANCE == null) {//1
       if (INSTANCE == null) {//3
           synchronized (SimpleSingleton4.class
) {//2 INSTANCE = new SimpleSingleton4();//4 } } } return INSTANCE;//5 }

 

 

詳細:

........................................

 

單例模式有:餓漢模式懶漢模式兩種。

餓漢模式程式碼如下:

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    
//私有的構造方法 private SimpleSingleton() { } //對外提供獲取例項的靜態方法 public static SimpleSingleton getInstance() { return INSTANCE; } }

使用餓漢模式的好處是:沒有執行緒安全的問題,但帶來的壞處也很明顯。

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一開始就例項化物件了,如果例項化過程非常耗時,並且最後這個物件沒有被使用,不是白白造成資源浪費嗎?

還真是啊。

這個時候你也許會想到,不用提前例項化物件,在真正使用的時候再例項化不就可以了?

這就是我接下來要介紹的:懶漢模式

具體程式碼如下:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

示例中的INSTANCE物件一開始是空的,在呼叫getInstance方法才會真正例項化。

嗯,不錯不錯。但這段程式碼還是有問題。

假如有多個執行緒中都呼叫了getInstance方法,那麼都走到 if (INSTANCE == null) 判斷時,可能同時成立,因為INSTANCE初始化時預設值是null。這樣會導致多個執行緒中同時建立INSTANCE物件,即INSTANCE物件被建立了多次,違背了只建立一個INSTANCE物件的初衷。

為了解決餓漢模式懶漢模式各自的問題,於是出現了:雙重檢查鎖

具體程式碼如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {
    }

    public static SimpleSingleton4 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton4.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton4();
                }
            }
        }
        return INSTANCE;
    }
}

需要在synchronized前後兩次判空。

但我要告訴你的是:這段程式碼有漏洞的。

有什麼問題?

public static SimpleSingleton4 getInstance() {
    if (INSTANCE == null) {//1
        synchronized (SimpleSingleton4.class) {//2
            if (INSTANCE == null) {//3
                INSTANCE = new SimpleSingleton4();//4
            }
        }
    }
    return INSTANCE;//5
}

getInstance方法的這段程式碼,我是按1、2、3、4、5這種順序寫的,希望也按這個順序執行。

但是java虛擬機器實際上會做一些優化,對一些程式碼指令進行重排。重排之後的順序可能就變成了:1、3、2、4、5,這樣在多執行緒的情況下同樣會建立多次例項。重排之後的程式碼可能如下:

public static SimpleSingleton4 getInstance() {
    if (INSTANCE == null) {//1
       if (INSTANCE == null) {//3
           synchronized (SimpleSingleton4.class) {//2
                INSTANCE = new SimpleSingleton4();//4
            }
        }
    }
    return INSTANCE;//5
}

 

原來如此,那有什麼辦法可以解決呢?

答:可以在定義INSTANCE是加上volatile關鍵字。具體程式碼如下:

public class SimpleSingleton7 {

    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {
    }

    public static SimpleSingleton7 getInstance() {
        if (INSTANCE == null) {
            synchronized (SimpleSingleton7.class) {
                if (INSTANCE == null) {
                    INSTANCE = new SimpleSingleton7();
                }
            }
        }
        return INSTANCE;
    }
}

volatile關鍵字可以保證多個執行緒的可見性,但是不能保證原子性。同時它也能禁止指令重排

雙重檢查鎖的機制既保證了執行緒安全,又比直接上鎖提高了執行效率,還節省了記憶體空間。