單例模式總結
定義
確保一個類只有一個例項,並且自行例項化並向整個系統提供這個例項。
使用場景
確保某個類有且只有一個的場景,避免消耗過多資源,或者某種型別的物件只應該有且只有一個。
關鍵點
- 建構函式不對外開放,一般為private
- 通過一個靜態方法或者列舉返回單例物件
- 確保單例類的物件有且只有一個,尤其在多執行緒環境下
- 確保單例類物件在反序列化時不會重新建立物件
型別
1. 懶漢模式(一般不建議使用)
public class Singleton { private static Singleton instance; private Singleton (){} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
- 優點:只有在第一次呼叫才初始化,在一定程度上節約了資源。
- 缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
2. 餓漢模式
public class Singleton { private static Singleton instance = new Singleton(); private Singleton (){} public static Singleton getInstance() { return instance; } }
- 優點:沒有加鎖,執行效率會提高。
- 缺點:類載入時就初始化,浪費記憶體。
基於 classloader 機制避免了多執行緒的同步問題,不過,instance 在類裝載時就例項化,雖然導致類裝載的原因有很多種,在單例模式中大多數都是呼叫 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化 instance 顯然沒有達到 lazy loading 的效果。
3. 雙檢鎖/雙重校驗鎖(DCL,即 double-checked locking)
public class Singleton { private volatile static Singleton singleton; private Singleton (){} public static Singleton getSingleton() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
- 優點:資源利用率高
- 缺點:不適合高併發或者JDK6以下使用
這種方式採用雙鎖機制,安全且在多執行緒情況下能保持高效能。
注意點:需要在JDK1.5之後才能使用。
若singleton定義時不加volatile,有可能會造成失效。
原因:
假設執行緒A執行到singleton = new Singleton()語句,這看起來是一句程式碼,但實際上著並非是一個原子操作,這句程式碼會被翻譯成多條彙編指令,大致做了三件事:
- 給Singleton的例項分配記憶體
- 呼叫Singleton()的建構函式,初始化成員欄位
- 將singleton欄位指向分配的記憶體空間(此時singleton就不是null了)
但是由於JAVA編譯器允許處理器亂序執行,以及JDK1.5之前JMM中Cache,暫存器到主記憶體回寫順序的規定,上面的第二和第三的順序是無法保證的,執行順序可能是1-2-3,也可能是1-3-2。如果是後者,並且在3執行完畢2執行之前,被切換到B執行緒上,此時singleton因為在A執行緒中已經執行過第三點,已經是非空了,所以B執行緒會直接取走singleton,此時使用就會出錯,這就是DCL失效問題。
在JDK1.5之後SUN官方已經注意到這種問題了,調整了JVM,具體化了volatile關鍵字,因此如果JDK1.5之後,只要加上volatile關鍵字,就可以保證singleton物件每次都是從主記憶體讀取,此時就可以正常使用DCL單例模式。
DCL雖然在一定程度上解決了資源消耗,多餘的同步,執行緒安全等問題,但是,他還是在某些情況下出現失效的問題,在《JAVA併發程式設計實踐》一書最後談到了這個問題,建議使用靜態內部類單例模式代替。
4. 靜態內部類單例模式/登記式(推薦)
public class Singleton { private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } }
這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化例項時只有一個執行緒。不同的地方在餓漢式方式是隻要Singleton類被裝載就會例項化,沒有Lazy-Loading的作用,而靜態內部類方式在Singleton類被裝載時並不會立即例項化,而是在需要例項化時,呼叫getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的例項化。
類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了執行緒的安全性,在類進行初始化時,別的執行緒是無法進入的。
- 優點:延遲了單例的例項化。
- 缺點:反序列化不特殊處理會重新生成物件
上述方式中如何杜絕反序列化時重新生成物件:
加入readResolve函式,在readResolve方法中將單例物件返回,而不是重新建立新的物件。
- 可序列化類中的欄位型別不是Java內建型別,那麼該欄位也需要實現Serializable介面。
-
如果你調整了可序列化類的內部結構,例如新增去除某個欄位,但沒有修改serialVersionUID,那麼會引發java.io.IvalidClassException異常或者導致某個屬性為0或者null。此時我們可以直接將serialVersionUID設定為0L,這樣即使修改了類的內部結構,我們反序列化也不會拋
java.io.IvalidClassException,只是那些新修改的欄位會為0或者null.
public class Singleton implements Serializable { private static final long serialVersionUID = 0L; private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } private Singleton (){} public static final Singleton getInstance() { return SingletonHolder.INSTANCE; } private Object readResolve() throws ObjectStreamException { return SingletonHolder.INSTANCE; } }
5.列舉單例(推薦)
public enum Singleton { INSTANCE; public void doSomething() { } }
- 優點:寫法簡單,是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多執行緒同步問題,而且還自動支援序列化機制,防止反序列化重新建立新的物件,絕對防止多次例項化。防止反射強行呼叫構造器。
- 缺點: JDK1.5 之後才加入 enum 特性。在Android中卻不是特別推薦:Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.
6.使用容器實現單例(管理多種型別的單例物件)
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) ; } }
如何選擇
- 是否是複雜的併發環境
- JDK版本是否過低
- 單例物件的資源消耗,lazy loading
- 等等
一般情況下,不建議使用懶漢方式,建議使用餓漢方式。只有在要明確實現 lazy loading 效果時,才會使用登記方式。如果涉及到反序列化建立物件時,可以嘗試使用列舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖方式。
小結
客戶端中一般沒有高併發的情況,出於效率考慮一般推薦使用雙檢鎖/雙重校驗鎖(DCL)或者靜態內部類單例模式/登記式。
單例模式的缺點:
- 單例模式一般沒有介面,拓展困難,只能修改程式碼
- 單例物件如果持有Context,容易引發記憶體洩漏,此時需要注意傳給單例物件的Context最好是Application Context
Android原始碼中的單例模式(拓展)
如何獲取系統服務(ServiceFetcher)
在6.0之前是直接寫在ContextImpl.java中(可參考Android原始碼設計模式解析與實戰第二章的講解),之後寫在SystemServiceRegistry.java中,這裡採用最新的Android8.0程式碼
final class SystemServiceRegistry { // Service registry information. // This information is never changed once static initialization has completed. private static final HashMap<Class<?>, String> SYSTEM_SERVICE_NAMES = new HashMap<Class<?>, String>(); private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS = new HashMap<String, ServiceFetcher<?>>(); private static int sServiceCacheSize; static { registerService(Context.ACCESSIBILITY_SERVICE, AccessibilityManager.class, new CachedServiceFetcher<AccessibilityManager>() { @Override public AccessibilityManager createService(ContextImpl ctx) { return AccessibilityManager.getInstance(ctx); }}); //同樣方式註冊各種服務 .... } /** * Gets a system service from a given context. */ public static Object getSystemService(ContextImpl ctx, String name) { ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name); return fetcher != null ? fetcher.getService(ctx) : null; } /** * Statically registers a system service with the context. * This method must be called during static initialization only. */ private static <T> void registerService(String serviceName, Class<T> serviceClass, ServiceFetcher<T> serviceFetcher) { SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName); SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher); } /** * Base interface for classes that fetch services. * These objects must only be created during static initialization. */ static abstract interface ServiceFetcher<T> { T getService(ContextImpl ctx); } /** * Override this class when the system service constructor needs a * ContextImpl and should be cached and retained by that context. */ static abstract class CachedServiceFetcher<T> implements ServiceFetcher<T> { private final int mCacheIndex; public CachedServiceFetcher() { mCacheIndex = sServiceCacheSize++; } @Override @SuppressWarnings("unchecked") public final T getService(ContextImpl ctx) { final Object[] cache = ctx.mServiceCache; synchronized (cache) { // Fetch or create the service. Object service = cache[mCacheIndex]; if (service == null) { try { service = createService(ctx); cache[mCacheIndex] = service; } catch (ServiceNotFoundException e) { onServiceNotFound(e); } } return (T)service; } } public abstract T createService(ContextImpl ctx) throws ServiceNotFoundException; } /** * Like StaticServiceFetcher, creates only one instance of the service per application, but when * creating the service for the first time, passes it the application context of the creating * application. * * TODO: Delete this once its only user (ConnectivityManager) is known to work well in the * case where multiple application components each have their own ConnectivityManager object. */ static abstract class StaticApplicationContextServiceFetcher<T> implements ServiceFetcher<T> { private T mCachedInstance; @Override public final T getService(ContextImpl ctx) { synchronized (StaticApplicationContextServiceFetcher.this) { if (mCachedInstance == null) { Context appContext = ctx.getApplicationContext(); // If the application context is null, we're either in the system process or // it's the application context very early in app initialization. In both these // cases, the passed-in ContextImpl will not be freed, so it's safe to pass it // to the service. http://b/27532714 . try { mCachedInstance = createService(appContext != null ? appContext : ctx); } catch (ServiceNotFoundException e) { onServiceNotFound(e); } } return mCachedInstance; } } public abstract T createService(Context applicationContext) throws ServiceNotFoundException; } }
在虛擬機器第一次載入該類的時候會註冊各種ServiceFetcher,將這些服務以鍵值對的形式儲存在一個HashMap中,使用者只需要根據Key來獲取對應的ServiceFetcher,然後通過ServiceFetcher物件的getService(ContextImpl ctx)方法來獲取具體的服務物件。在第一次獲取時,會呼叫ServiceFetcher的createService(ContextImpl ctx)函式建立服務物件,然後快取到一個cache陣列中,下次再取時直接從cache中獲取,避免重複建立物件,達到單例的效果。這種方式就是通過容器的單例模式實現方式,系統服務以單例的形式存在,減少資源消耗。