【設計模式】單例模式

面試結束~抽空整理下設計模式相關的知識吧?太菜了啊
在這裏插入圖片描述
第一個當然是單例模式啦。面試讓你手寫一個單例模式你怎麼寫?
看我一頓操作:

public class Singleton {
    private static Singleton singleton = null;
    private Singleton() {}
    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}

感覺很easy啊,先是構造器私有化,防止外部創建實例,然後提供一個靜態的方法,當實例不存在時,創建一個實例返回。但是我仔細一看,有問題。如果兩個線程同時運行到if (singleton == null)那不就創建兩個實例返回了嗎?這可咋整?
我靈機一動,想到了之前看到過的餓漢模式:

public class Singleton {
    private static Singleton singleton = new Singleton();
    private Singleton() {}
    public static Singleton getInstance() {
        return singleton;
    }
}

是不是完美解決了線程不安全的問題?完美~

那麼回到問題本身,如果預先創建對象的方式比較消耗系統資源,會拖延程序啓動的速度。我如何在不預先創建對象的情況下設計一個單例模式呢?
我們直接想到的方法是加一個synchronized關鍵字:

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

這可以說是很粗暴了,直接在靜態方法上加synchronized,所有調用到這個方法的程序都需要排隊一個一個執行,效率可以說是很低的,那麼怎麼提高效率呢?我們可以這麼做:

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

這是雙重加鎖機制,先判斷對象是否存在,如果不存在,加鎖後再次判斷,如果還不存在,則創建新的對象。這樣我們是不是就完全解決了線程安全問題?其實不是的,還有一個地方更需要注意。這涉及到JVM編譯時的指令重排機制,什麼是指令重排?singleton = new Singleton(),會被編譯器編譯成如下JVM指令:

memory = allocate();    //1:分配對象的內存空間 
ctorInstance(memory);  //2:初始化對象 
instance = memory;     //3:設置instance指向剛分配的內存地址

在實際運行的時候有可能順序是這樣的:

memory = allocate();    //1:分配對象的內存空間 
instance = memory;     //3:設置instance指向剛分配的內存地址 
ctorInstance(memory);  //2:初始化對象 

當線程A執行完1,3,時,instance對象還未完成初始化,但已經不再指向null。此時如果線程B搶佔到CPU資源,執行 if(singleton == null)的結果會是false,從而返回一個沒有初始化完成的singleton對象,也就是說獲得了一個爲null的對象。
怎麼避免這一情況呢?
我們可以使用volatile關鍵字,禁止指令重排,線程安全的單例模式如下:

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

除了餓漢和懶漢模式之外還有兩種創建單例的方法,第一種是靜態內部類的方式,這也是一種延遲加載的方式去創建單例對象,不會造成系統資源的浪費,而且這種方式創建非常簡單,線程安全,所以我比較推薦:

public class Singleton {  
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

第二種是枚舉類,但是這種方式在平常開發的時候用的少,可讀性不好,一般很少使用:

public enum Singleton  {
    INSTANCE      
    //可以省略此方法,通過Singleton.INSTANCE進行操作
    public static Singleton get Instance() {
        return Singleton.INSTANCE;
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章