定義
確保一個類只有一個實例,并且自行實例化并向整個系統(tǒng)提供這個實例。
使用場景
確保某個類有且只有一個的場景,避免消耗過多資源,或者某種類型的對象只應(yīng)該有且只有一個。
如:訪問IO,數(shù)據(jù)庫,打印機等等
關(guān)鍵點
- 構(gòu)造函數(shù)不對外開放,一般為private
- 通過一個靜態(tài)方法或者枚舉返回單例對象
- 確保單例類的對象有且只有一個,尤其在多線程環(huán)境下
- 確保單例類對象在反序列化時不會重新創(chuàng)建對象
類型
1. 懶漢模式(一般不建議使用)
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
- 優(yōu)點:只有在第一次調(diào)用才初始化,在一定程度上節(jié)約了資源。
- 缺點:必須加鎖 synchronized 才能保證單例,但加鎖會影響效率。
2. 餓漢模式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
- 優(yōu)點:沒有加鎖,執(zhí)行效率會提高。
- 缺點:類加載時就初始化,浪費內(nèi)存。
基于 classloader 機制避免了多線程的同步問題,不過,instance 在類裝載時就實例化,雖然導(dǎo)致類裝載的原因有很多種,在單例模式中大多數(shù)都是調(diào)用 getInstance 方法, 但是也不能確定有其他的方式(或者其他的靜態(tài)方法)導(dǎo)致類裝載,這時候初始化 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;
}
}
- 優(yōu)點:資源利用率高
- 缺點:不適合高并發(fā)或者JDK6以下使用
這種方式采用雙鎖機制,安全且在多線程情況下能保持高性能。
注意點:需要在JDK1.5之后才能使用。
若singleton定義時不加volatile,有可能會造成失效。
原因:
假設(shè)線程A執(zhí)行到singleton = new Singleton()語句,這看起來是一句代碼,但實際上著并非是一個原子操作,這句代碼會被翻譯成多條匯編指令,大致做了三件事:
- 給Singleton的實例分配內(nèi)存
- 調(diào)用Singleton()的構(gòu)造函數(shù),初始化成員字段
- 將singleton字段指向分配的內(nèi)存空間(此時singleton就不是null了)
但是由于JAVA編譯器允許處理器亂序執(zhí)行,以及JDK1.5之前JMM中Cache,寄存器到主內(nèi)存回寫順序的規(guī)定,上面的第二和第三的順序是無法保證的,執(zhí)行順序可能是1-2-3,也可能是1-3-2。如果是后者,并且在3執(zhí)行完畢2執(zhí)行之前,被切換到B線程上,此時singleton因為在A線程中已經(jīng)執(zhí)行過第三點,已經(jīng)是非空了,所以B線程會直接取走singleton,此時使用就會出錯,這就是DCL失效問題。
在JDK1.5之后SUN官方已經(jīng)注意到這種問題了,調(diào)整了JVM,具體化了volatile關(guān)鍵字,因此如果JDK1.5之后,只要加上volatile關(guān)鍵字,就可以保證singleton對象每次都是從主內(nèi)存讀取,此時就可以正常使用DCL單例模式。
DCL雖然在一定程度上解決了資源消耗,多余的同步,線程安全等問題,但是,他還是在某些情況下出現(xiàn)失效的問題,在《JAVA并發(fā)編程實踐》一書最后談到了這個問題,建議使用靜態(tài)內(nèi)部類單例模式代替。
4. 靜態(tài)內(nèi)部類單例模式/登記式(推薦)
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的作用,而靜態(tài)內(nèi)部類方式在Singleton類被裝載時并不會立即實例化,而是在需要實例化時,調(diào)用getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的實例化。
類的靜態(tài)屬性只會在第一次加載類的時候初始化,所以在這里,JVM幫助我們保證了線程的安全性,在類進行初始化時,別的線程是無法進入的。
- 優(yōu)點:延遲了單例的實例化。
- 缺點:反序列化不特殊處理會重新生成對象
上述方式中如何杜絕反序列化時重新生成對象:
加入readResolve函數(shù),在readResolve方法中將單例對象返回,而不是重新創(chuàng)建新的對象。
- 可序列化類中的字段類型不是Java內(nèi)置類型,那么該字段也需要實現(xiàn)Serializable接口。
- 如果你調(diào)整了可序列化類的內(nèi)部結(jié)構(gòu),例如新增去除某個字段,但沒有修改serialVersionUID,那么會引發(fā)java.io.IvalidClassException異?;蛘邔?dǎo)致某個屬性為0或者null。此時我們可以直接將serialVersionUID設(shè)置為0L,這樣即使修改了類的內(nèi)部結(jié)構(gòu),我們反序列化也不會拋
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() {
}
}
- 優(yōu)點:寫法簡單,是 Effective Java 作者 Josh Bloch 提倡的方式,它不僅能避免多線程同步問題,而且還自動支持序列化機制,防止反序列化重新創(chuàng)建新的對象,絕對防止多次實例化。防止反射強行調(diào)用構(gòu)造器。
- 缺點: 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.使用容器實現(xiàn)單例(管理多種類型的單例對象)
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) ;
}
}
如何選擇
- 是否是復(fù)雜的并發(fā)環(huán)境
- JDK版本是否過低
- 單例對象的資源消耗,lazy loading
- 等等
一般情況下,不建議使用懶漢方式,建議使用餓漢方式。只有在要明確實現(xiàn) lazy loading 效果時,才會使用登記方式。如果涉及到反序列化創(chuàng)建對象時,可以嘗試使用枚舉方式。如果有其他特殊的需求,可以考慮使用雙檢鎖方式。
小結(jié)
客戶端中一般沒有高并發(fā)的情況,出于效率考慮一般推薦使用雙檢鎖/雙重校驗鎖(DCL)或者靜態(tài)內(nèi)部類單例模式/登記式。
單例模式的缺點:
- 單例模式一般沒有接口,拓展困難,只能修改代碼
- 單例對象如果持有Context,容易引發(fā)內(nèi)存泄漏,此時需要注意傳給單例對象的Context最好是Application Context
Android源碼中的單例模式(拓展)
如何獲取系統(tǒng)服務(wù)(ServiceFetcher)
在6.0之前是直接寫在ContextImpl.java中(可參考Android源碼設(shè)計模式解析與實戰(zhàn)第二章的講解),之后寫在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);
}});
//同樣方式注冊各種服務(wù)
....
}
/**
* 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,將這些服務(wù)以鍵值對的形式存儲在一個HashMap中,用戶只需要根據(jù)Key來獲取對應(yīng)的ServiceFetcher,然后通過ServiceFetcher對象的getService(ContextImpl ctx)方法來獲取具體的服務(wù)對象。在第一次獲取時,會調(diào)用ServiceFetcher的createService(ContextImpl ctx)函數(shù)創(chuàng)建服務(wù)對象,然后緩存到一個cache數(shù)組中,下次再取時直接從cache中獲取,避免重復(fù)創(chuàng)建對象,達到單例的效果。這種方式就是通過容器的單例模式實現(xiàn)方式,系統(tǒng)服務(wù)以單例的形式存在,減少資源消耗。