單例模式總結(jié)

定義

確保一個類只有一個實例,并且自行實例化并向整個系統(tǒng)提供這個實例。

使用場景

確保某個類有且只有一個的場景,避免消耗過多資源,或者某種類型的對象只應(yīng)該有且只有一個。

如:訪問IO,數(shù)據(jù)庫,打印機等等

關(guān)鍵點

  1. 構(gòu)造函數(shù)不對外開放,一般為private
  2. 通過一個靜態(tài)方法或者枚舉返回單例對象
  3. 確保單例類的對象有且只有一個,尤其在多線程環(huán)境下
  4. 確保單例類對象在反序列化時不會重新創(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()語句,這看起來是一句代碼,但實際上著并非是一個原子操作,這句代碼會被翻譯成多條匯編指令,大致做了三件事:

  1. 給Singleton的實例分配內(nèi)存
  2. 調(diào)用Singleton()的構(gòu)造函數(shù),初始化成員字段
  3. 將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ù)以單例的形式存在,減少資源消耗。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 單例模式(SingletonPattern)一般被認為是最簡單、最易理解的設(shè)計模式,也因為它的簡潔易懂,是項目中最...
    成熱了閱讀 4,530評論 4 34
  • 使用場景 要求生成唯一序列號的環(huán)境 在整個項目中需要一個共享訪問點或共享數(shù)據(jù)例如一個Web頁面上的計數(shù)器,可以不用...
    niaoge2016閱讀 451評論 0 0
  • 前言 本文主要參考 那些年,我們一起寫過的“單例模式”。 何為單例模式? 顧名思義,單例模式就是保證一個類僅有一個...
    tandeneck閱讀 2,623評論 1 8
  • 單例模式 定義:確保某一個類只有一個實例,自行實例化并且想整個系統(tǒng)提供這個實例。 使用場景:避免某個類產(chǎn)生多個對象...
    luoyoub閱讀 221評論 0 0
  • 簡書,我起初是被這個軟件的名字所吸引。 何謂簡?簡,竹子搭建的一座房屋。 簡潔大方的外觀,清雅別致的界面,以及美好...
    悒郁的肖像閱讀 947評論 14 23

友情鏈接更多精彩內(nèi)容