設計模式-----單例模式

單例模式

定義

保證一個類僅有一個實例,並提供一個訪問它的全局訪問點

六種寫法

1.餓漢式

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton(){    
    }
    public static Singleton getInatance(){
        return instance;
    }
}

餓漢式是典型的空間換時間,在類裝載時進行了對象實例化,不管是否使用都先創建出來,類裝載較慢,但提取對象的速度快,餓漢式基於JVM類裝載的機制避免了多線程同步問題,但是沒有達到懶加載的效果, 如果從始至終從未使用過這個實例,則會造成內存的浪費

2.懶漢式(線程不安全)

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

懶漢式實例化時機是在第一次調用時,實現Lazy Loading,第一次調用反應稍慢,而且多線程時不安全

懶漢式(線程安全)

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

這種寫法實現了線程安全,但是每次提取實例對象的時候,都需要進行同步,造成不必要的開銷,而且大部分時候我們用不到同步,所以不建議使用這種寫法

3.雙重檢測鎖(DCL)

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

雙重檢測鎖(Double Checked Locking),有兩次對instance的判空,一次在同步塊外,次在同步塊內,因爲有可能多個線程一起進入同步塊外的if,如果在同步塊內不進行二次檢驗的話就會生成多個實例了,兩次判空也減少了不必要的同步

注意: 這段代碼看起來很完美,很可惜,它是有問題。主要在於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 就可以了

4.靜態內部類(static nested class)

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

這種寫法仍然使用JVM本身機制保證了線程安全問題;由於 SingletonHolder 是私有的,除了 getInstance() 之外沒有辦法訪問它,而第一次加載Singleton類時並不會初始化INSTANCE,只有第一次調用getInstance方法時虛擬機加載SingletonHolder 並初始化INSTANCE,因此它是懶漢式的,這樣不僅能確保線程安全也能保證Singleton類的唯一性, 同時讀取實例的時候不會進行同步,沒有性能缺陷;也不依賴 JDK 版本。 所以推薦使用靜態內部類單例模式

5.枚舉(Enum)

public enum Singleton{
	INSTANCE;
}

可以通過EasySingleton.INSTANCE來訪問實例,這比調用getInstance()方法簡單多了。創建枚舉默認就是線程安全的,而且還能防止反序列化導致重新創建新的對象。但是還是很少看到有人這樣寫,不熟悉還有可讀性並不是很高

6.容器

public class SingletonManager {
    private static Map<String,Object> map=new HashMap<String, Object>();
    private SingletonManager(){}
    
    public static void registerService(String key,Object instance){
        if (!map.containsKey(key)){
            map.put(key,instance);
        }
    }
    
    public static Object getService(String key){
        return map.get(key);
    } 
}

用SingletonManager 將多種的單例類統一管理,在使用時根據key獲取對象對應類型的對象。這種方式使得我們可以管理多種類型的單例,並且在使用時可以通過統一的接口進行獲取操作,降低了用戶的使用成本,也對用戶隱藏了具體實現,降低了耦合度

總結

一般來說,單例模式有六種寫法:餓漢式 、 懶漢式 、 雙重檢測鎖 、 靜態內部 、 枚舉 、 容器。

在開發中,一般情況下直接使用餓漢式就好了,如果明確要求要懶加載(lazy loading)會傾向於使用靜態內部類,如果涉及到反序列化創建對象時會試着使用枚舉的方式來實現單例

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章