單例模式
定義
保證一個類僅有一個實例,並提供一個訪問它的全局訪問點
六種寫法
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 件事情
- 給 instance 分配內存
- 調用 Singleton 的構造函數來初始化成員變量
- 將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)會傾向於使用靜態內部類,如果涉及到反序列化創建對象時會試着使用枚舉的方式來實現單例