1. 餓漢模式
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){ } public static Singleton getInstance() { return instance; } }
這種方式在類載入時就完成了初始化,所以類載入較慢,但獲取物件的速度快。 這種方式基於類載入機制避免了多執行緒的同步問題,但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance顯然沒有達到懶載入的效果。
2. 懶漢模式(執行緒不安全)
public class Singleton { private static Singleton instance; private Singleton (){ } public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懶漢模式申明瞭一個靜態物件,在使用者第一次呼叫時初始化,雖然節約了資源,但第一次載入時需要例項化,反映稍慢一些,而且在多執行緒不能正常工作。
3. 懶漢模式(執行緒安全)
public class Singleton { private static Singleton instance; private Singleton (){ } public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
這種寫法能夠在多執行緒中很好的工作,但是每次呼叫getInstance方法時都需要進行同步,造成不必要的同步開銷,而且大部分時候我們是用不到同步的,所以不建議用這種模式。
4. 雙重檢查模式 (DCL)
public class Singleton { private volatile static Singleton singleton; private Singleton (){ } public static Singleton getInstance() { if (instance== null) { synchronized (Singleton.class) { if (instance== null) { instance= new Singleton(); } } } return singleton; } }
這種寫法在getSingleton方法中對singleton進行了兩次判空,第一次是為了不必要的同步,第二次是在singleton等於null的情況下才建立例項。在這裡用到了volatile關鍵字,不瞭解volatile關鍵字的可以檢視Java多執行緒(三)volatile域這篇文章,在這篇文章我也提到了雙重檢查模式是正確使用volatile關鍵字的場景之一。
在這裡使用volatile會或多或少的影響效能,但考慮到程式的正確性,犧牲這點效能還是值得的。 DCL優點是資源利用率高,第一次執行getInstance時單例物件才被例項化,效率高。缺點是第一次載入時反應稍慢一些,在高併發環境下也有一定的缺陷,雖然發生的概率很小。DCL雖然在一定程度解決了資源的消耗和多餘的同步,執行緒安全等問題,但是他還是在某些情況會出現失效的問題,也就是DCL失效,在《java併發程式設計實踐》一書建議用靜態內部類單例模式來替代DCL。
5. 靜態內部類單例模式
public class Singleton { private Singleton(){ } public static Singleton getInstance(){ return SingletonHolder.sInstance; } private static class SingletonHolder { private static final Singleton sInstance = new Singleton(); } }
第一次載入Singleton類時並不會初始化sInstance,只有第一次呼叫getInstance方法時虛擬機器載入SingletonHolder 並初始化sInstance ,這樣不僅能確保執行緒安全也能保證Singleton類的唯一性,所以推薦使用靜態內部類單例模式。
6. 列舉單例
public enum Singleton { INSTANCE; public void doSomeThing() { } }
預設列舉例項的建立是執行緒安全的,並且在任何情況下都是單例,上述講的幾種單例模式實現中,有一種情況下他們會重新建立物件,那就是反序列化,將一個單例例項物件寫到磁碟再讀回來,從而獲得了一個例項。反序列化操作提供了readResolve方法,這個方法可以讓開發人員控制物件的反序列化。在上述的幾個方法示例中如果要杜絕單例物件被反序列化是重新生成物件,就必須加入如下方法:
private Object readResolve() throws ObjectStreamException{ return singleton; }
列舉單例的優點就是簡單,但是大部分應用開發很少用列舉,可讀性並不是很高,不建議用。
7. 使用容器實現單例模式
public class SingletonManager { private static Map<String, Object> objMap = new HashMap<String,Object>(); private Singleton() { } public static void registerService(String key, Objectinstance) { if (!objMap.containsKey(key) ) { objMap.put(key, instance) ; } } public static ObjectgetService(String key) { return objMap.get(key) ; } }
用SingletonManager 將多種的單例類統一管理,在使用時根據key獲取物件對應型別的物件。這種方式使得我們可以管理多種型別的單例,並且在使用時可以通過統一的介面進行獲取操作,降低了使用者的使用成本,也對使用者隱藏了具體實現,降低了耦合度。
總結
到這裡七中寫法都介紹完了,至於選擇用哪種形式的單例模式,取決於你的專案本身,是否是有複雜的併發環境,還是需要控制單例物件的資源消耗。
8.注意事項
1.使用反射能夠破壞單例模式,所以應該慎用反射
1 Constructor con = Singleton.class.getDeclaredConstructor(); 2 con.setAccessible(true); 3 // 通過反射獲取例項 4 Singleton singeton1 = (Singleton) con.newInstance(); 5 Singleton singeton2 = (Singleton) con.newInstance(); 6 System.out.println(singeton1==singeton2);//結果為false,singeton1和singeton2將是兩個不同的例項
- 可以通過當第二次呼叫建構函式時丟擲異常來防止反射破壞單例,以懶漢式為例:
1 public class Singleton { 2 private static boolean flag = true; 3 private static Singleton single = null; 4 5 private Singleton() { 6 if (flag) { 7 flag = !flag; 8 } else { 9 throw new RuntimeException("單例模式被破壞!"); 10 } 11 } 12 13 public static Singleton getInstance() { 14 if (single == null) { 15 single = new Singleton(); 16 } 17 return single; 18 } 19 }
2.反序列化時也會破壞單例模式,可以通過重寫readResolve方法避免,以餓漢式為例
1 public class Singleton implements Serializable { 2 private Singleton() { 3 } 4 5 private static final Singleton single = new Singleton(); 6 7 public static Singleton getInstance() { 8 return single; 9 } 10 11 private Object readResolve() throws ObjectStreamException {//重寫readResolve() 12 return single;//直接返回單例物件 13 } 14 }