1. 程式人生 > >單例模式(Singleton mode)實戰講解

單例模式(Singleton mode)實戰講解

單例模式是實際專案中使用最多的一種設計模式,有著非常廣泛的使用場景。下面我們就結合一個實際專案中的例子,來說說單例模式的使用方式。

1.經典單例模式之懶漢模式

import java.util.HashMap;
import java.util.Map;

public class Singleton {

    private static Map<String, String> testMap = new HashMap<String, String>();

    private static Singleton instance;

    private
Singleton() { initTestMap(); } public static void initTestMap() { for(int i=0; i<10; i++) { String value = "a" + String.valueOf(i); testMap.put(String.valueOf(i), value); } } public static Singleton getInstance() { if(instance == null
) { instance = new Singleton(); } return instance; } }

這個單例物件裡面有一個testMap,我們希望Singleton物件初始化的時候testMap就已經初始化。
注意的幾個點是:
1.instance物件與testMap物件均為static物件,這樣可以直接用類名呼叫。
2.在構造方法中,將testMap初始化。

2.餓漢模式

import java.util.HashMap;
import java.util.Map;

public class Singleton {

    private
static Map<String, String> testMap = new HashMap<String, String>(); private static Singleton instance = new Singleton(); private Singleton() { initTestMap(); } public static void initTestMap() { for(int i=0; i<10; i++) { String value = "a" + String.valueOf(i); testMap.put(String.valueOf(i), value); } } public static synchronized Singleton getInstance() { return instance; } }

餓漢模式與懶漢模式相比較起來,一上來就直接將例項初始化,不存在延遲載入的問題。

上面的兩種寫法,沒有考慮多執行緒的情況。如果是在多執行緒的場景下使用,請參考後面的文章。

3.雙重鎖校驗(double checked locking pattern)

雙重檢驗鎖模式(double checked locking pattern),是一種使用同步塊加鎖的方法。程式設計師稱其為雙重檢查鎖,因為會有兩次檢查 instance == null,一次是在同步塊外,一次是在同步塊內。為什麼在同步塊內還要再檢驗一次?因為可能會有多個執行緒一起進入同步塊外的 if,如果在同步塊內不進行二次檢驗的話就會生成多個例項了。

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

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

這段程式碼看起來很完美,很可惜,它是有問題。主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。

1.給 instance 分配記憶體
2.呼叫 Singleton 的建構函式來初始化成員變數
3.將instance物件指向分配的記憶體空間(執行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被執行緒二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以執行緒二會直接返回 instance,然後使用,然後順理成章地報錯。

我們只需要將 instance 變數宣告成 volatile 就可以了。

public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {}

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

volatile用在多執行緒,同步變數。 執行緒為了提高效率,將某成員變數(如A)拷貝了一份(如B),執行緒中對A的訪問其實訪問的是B。只在某些動作時才進行A和B的同步。因此存在A和B不一致的情況。volatile就是用來避免這種情況的。volatile告訴jvm, 它所修飾的變數不保留拷貝,直接訪問主記憶體中的(也就是上面說的A)