[設計模式] Singleton Pattern (單例模式) Part2 (解決多執行緒問題) - Update Volatile關鍵字

[2011-1-1補充] volatile關鍵字


感謝馮大哥留言,在運用Double-Checked locking的作法時,在Java 5.0之後必須要加上volatile的修飾子在物件的變數宣告上,也就是變成:
  •  private volatile static Singleton instance;
這樣子的寫法。原因在於,如果不加上volatile這個關鍵字時,在Java中由"順序"的問題,還是有可能會發生問題。最顯而易見的錯誤在於,當A執行緒進入getInstance()方法後,並且得到if(instance==null)為true的結果,進入到同步區,要執行Singleton()建構子內的敘述,但還沒有執行完時,執行緒B就進入getInstance()方法中,並且得到instance不是null的結果(因為在記憶體空間中已經配置的位置給instance了),如此一來就會造成執行緒B的物件內容並沒有成功的被指派,A和B之間就會有不同步的情形了。

詳細的過程可以參考這篇文章:Double-checked Locking (DCL) and how to fix it

我覺得寫的還蠻清楚的,另外我也加了相關閱讀在最下方,歡迎大家參考,這幾篇對於Double Check Locking的機制都寫得蠻清楚的,值得一看囉。
==

在Singleton Pattern (單例模式) Part1中的【Lazy Instantization】方法可以說是最基礎的方式,但是在多執行緒的程式當中,這樣的寫法還是有可能會產生不同的實體,也就是這樣的寫法並不是一個thread-safe的方式,我們可以寫一個小程式來檢查看看:


public class SingletonTest extends Thread {

    String threadName = "";

    public SingletonTest(String name) {
        threadName = name;
    }

    public void run() {
        for(int i=0 ; i<10 ; i++) {
            try {
                Thread.sleep((int)500);
                Singleton instance1 = Singleton.getInstance();
                System.out.println(threadName + " ==> " + instance1.hashCode());

            } catch (InterruptedException e) {
                e.printStackTrace();
        }
    }
}

    public static void main(String [] a) {
        Thread t1 = new SingletonTest("thread-1");
        Thread t2 = new SingletonTest("thread-2");

        t1.start();
        t2.start();
    }
}


兩個threads可能會產生相同的實體(相同的hashCode)或是不同的實體(但不一定每次都會發生),原因在於兩個threads可能同時呼叫Singleton.getInstance()時,在進行if(instance==null)判斷時,結果都可能會是true,以至於都產生一個新的實體,這樣就會違背Singleton Pattern的宗旨了,所以我們必須進一步的修改這個模式。

修改的想法是,當A、B兩個執行緒同時呼叫Singleton.getInstance()這個方法來取得物件的實體時,必須要先等對方結束之後才呼叫,才不會造成問題。換句話說,getInstance()這個方法在Java中只要加上Synchronized這個修飾子就可以解決了,如此一來,整個getInstance()方法就變成一個同步等待區,可以避免多執行緒造成重複實體的問題。

public class Singleton {

    private static Singleton instance;

    private Singleton () { }

    public static Synchronized Singleton getInstance() {
        if(instance==null)
            instance = new Singleton();
        return instance;
    }
}


上面提到,某個執行緒必須等到另外一個執行緒離開之後才能存取getInstance()這個method,當執行緒一多的時候,可以想像的會造成效率的低落。所以又有進一步的改良寫法,那就是採用雙重上鎖(Double-Checked locking)的方式。

雙重上鎖的概念是,一開始先檢查instance這個變數是不是null,如果不是null代表之前已經實體化了,直接回傳就好,如果沒有實體化時,才會進入同步等待區域。實際撰寫上只要將上面的getInstance()方法改寫成以下的形式就可以了:


public class Singleton {

    private volatile static Singleton instance;

    private Singleton () { }

    public static Singleton getInstance() {
        if(instance==null) {
            Synchronized(Singleton.class) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}


如此一來就可以解決多執行緒效率的問題囉!歡迎大家一起留言討論:)

【相關閱讀】
Double-checked locking and the Singleton pattern
Double-checked Locking (DCL) and how to fix it
The "Double-Checked Locking is Broken" Declaration

Share this post!

Bookmark and Share

2 意見:

馮彥文 提到...

若在 java 用要加上 volatile, 不然還是有 multi-thread 沒有 synchronized 的問題, 請見: http://en.wikipedia.org/wiki/Double-checked_locking

kevingo 提到...

感謝馮大哥的留言,我會在內文加註上去,謝謝:)