單例模式的六種寫法

定義

確保某個類只有一個實例,並且自行實例化並向整個系統提供這個實例java

UML結構圖

場景

  • 須要頻繁的實例化和銷燬的對象;
  • 有狀態的工具類對象
  • 頻繁訪問數據庫或文件對象;
  • 確保某個類只有一個對象的場景,好比一個對象須要消耗的資源過多,訪問io、數據庫,須要提供全局配置的場景

幾種單例模式

一、餓漢式

聲明靜態時已經初始化,在獲取對象以前就初始化android

優勢:獲取對象的速度快,線程安全(由於虛擬機保證只會裝載一次,在裝載類的時候是不會發生併發的)

缺點:耗內存(若類中有靜態方法,在調用靜態方法的時候類就會被加載,類加載的時候就完成了單例的初始化,拖慢速度)

public class EagerSingleton {
    //餓漢單例模式
    //在類加載時就完成了初始化,因此類加載較慢,但獲取對象的速度快
    private static EagerSingleton instance = new EagerSingleton();//靜態私有成員,已初始化
    
    private EagerSingleton() {
        //私有構造函數
    }
    
    public static EagerSingleton getInstance() //靜態,不用同步(類加載時已初始化,不會有多線程的問題) {
        return instance;
    }
    
}

複製代碼

二、懶漢式

synchronized同步鎖: 多線程下保證單例對象惟一性shell

優勢:單例只有在使用時才被實例化,必定程度上節約了資源

缺點:加入synchronized關鍵字,形成沒必要要的同步開銷。不建議使用。

//懶漢式單例模式
    //比較懶,在類加載時,不建立實例,所以類加載速度快,但運行時獲取對象的速度慢
    private static LazySingleton intance = null;//靜態私用成員,沒有初始化
    
    private LazySingleton() {
        //私有構造函數
    }
    
    public static synchronized LazySingleton getInstance() //靜態,同步,公開訪問點 {
        if(intance == null)
        {
            intance = new LazySingleton();
        }
        return intance;
    }
}
複製代碼

三、Double Check Lock(DCL)實現單例(使用最多的單例實現之一)

(雙重鎖定體如今兩次判空)數據庫

優勢:既能保證線程安全,且單例對象初始化後調用getInstance不進行同步鎖,資源利用率高

缺點:第一次加載稍慢,因爲Java內存模型一些緣由偶爾會失敗,在高併發環境下也有必定的缺陷,但機率很小。

代碼示例:設計模式

public class SingletonKerriganD {

    /** * 單例對象實例 */
    private volatile static SingletonKerriganD instance = null;//這裏加volatitle是爲了不DCL失效

    //DCL對instance進行了兩次null判斷
    //第一層判斷主要是爲了不沒必要要的同步
    //第二層的判斷則是爲了在null的狀況下建立實例。
    public static SingletonKerriganD getInstance() {
        if (instance == null) {
            synchronized (SingletonKerriganD.class) {
                if (instance == null) {
                    instance = new SingletonKerriganD();
               
            }
        }
        return instance;
    }
    
    private SingletonKerriganD() {
        //私有構造函數
    }
}
複製代碼

什麼是DCL失效問題?

假如線程A執行到instance = new SingletonKerriganD(),大體作了以下三件事:安全

  1. 給實例分配內存
  2. 調用構造函數,初始化成員字段
  3. 將instance 對象指向分配的內存空間(此時sInstance不是null)

若是執行順序是1-3-2,那多線程下,A線程先執行3,2還沒執行的時候,此時instance!=null,這時候,B線程直接取走instance ,使用會出錯,難以追蹤。JDK1.5及以後的volatile 解決了DCL失效問題(雙重鎖定失效)bash

四、靜態內部類單例模式

在調用 SingletonHolder.instance 的時候,纔會對單例進行初始化,多線程

優勢:線程安全、保證單例對象惟一性,同時也延遲了單例的實例化

缺點:須要兩個類去作到這一點,雖然不會建立靜態內部類的對象,可是其 Class 對象仍是會被建立,並且是屬於永久代的對象。

(綜合來看,私覺得這種方式是最好的單例模式)併發

public class SingletonInner {
    private static class SingletonHolder{
        private final static SingletonInner instance=new SingletonInner();
    }

    public static SingletonInner getInstance(){
        return SingletonHolder.instance;
    }
    
    private SingletonInner() {
        //私有構造函數
    }
}

複製代碼

這種方式如何保證單例且線程安全?

當getInstance方法第一次被調用的時候,它第一次讀取SingletonHolder.instance,內部類SingletonHolder類獲得初始化;而這個類在裝載並被初始化的時候,會初始化它的靜態域,從而建立Singleton的實例,因爲是靜態的域,所以只會在虛擬機裝載類的時候初始化一次,並由虛擬機來保證它的線程安全性。 這個模式的優點在於,getInstance方法並無被同步,而且只是執行一個域的訪問,所以延遲初始化並無增長任何訪問成本。jvm

這種方式可否避免反射入侵?

答案是:不能。網上不少介紹到靜態內部類的單例模式的優勢會提到「經過反射,是不能從外部類獲取內部類的屬性的。 因此這種形式,很好的避免了反射入侵」,這是錯誤的,反射是能夠獲取內部類的屬性(想了解更多反射的知識請看 java反射全解),入侵單例模式根本不在話下,直接看下面的例子:

單例類以下:

package eft.reflex;

public class Singleton {

    private int a;

    private Singleton(){
        a=123;
    }
    private static class SingletonHolder{
        private final static Singleton instance=new Singleton();
    }
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }

    public int getTest(){
        return a;
    }
}
複製代碼

入侵與測試代碼以下:

public static void main(String[] args) throws Exception {
        //經過反射獲取內部類SingletonHolder的instance實例fInstance
        Class cInner=Class.forName("eft.reflex.Singleton$SingletonHolder");
        Field fInstance=cInner.getDeclaredField("instance");

        //將此域的final修飾符去掉
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(fInstance, fInstance.getModifiers() & ~Modifier.FINAL);

        //打印單例的某個屬性,接下來要經過反射去篡改這個值
        System.out.println("a="+ Singleton.getInstance().getTest());

        //獲取該單例的a屬性fieldA
        fInstance.setAccessible(true);
        Field fieldA=Singleton.class.getDeclaredField("a");

        //經過反射類構造器建立新的實例newSingleton(這裏由於無參構造函數是私有的,不能經過Class.newInstance建立實例)
        Constructor constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton newSingleton= (Singleton) constructor.newInstance();

        //讓fInstance指向新的實例newSingleton,此時咱們的單例已經被偷樑換柱了!
        fInstance.set(null,newSingleton);
        //爲盜版的單例的屬性a設置新的值
        fieldA.setAccessible(true);
        fieldA.set(newSingleton,888);

        //測試是否成功入侵
        System.out.println("被反射入侵後:a="+ Singleton.getInstance().getTest());
        fieldA.set(newSingleton,777);
        System.out.println("被反射入侵後:a="+ Singleton.getInstance().getTest());
}
複製代碼

輸出結果:

a=123
被反射入侵後:a=888
被反射入侵後:a=777

複製代碼

注意: 上述四種方法要杜絕在被反序列化時從新聲明對象,須要加入以下方法:

private Object readResolve() throws ObjectStreamException{
    return sInstance;
}
複製代碼

爲何呢?由於當JVM從內存中反序列化地"組裝"一個新對象時,自動調用 readResolve方法來返回咱們指定好的對象

五、枚舉單例

優勢:線程安全,防止被反序列化

缺點:枚舉相對耗內存

public enum  SingletonEnum {
    instance;
    public void doThing(){
        
    }

}
複製代碼

只要 SingletonEnum.INSTANCE 便可得到所要實例。

這種方式如何保證單例?

首先,在枚舉中咱們明確了構造方法限制爲私有,在咱們訪問枚舉實例時會執行構造方法,同時每一個枚舉實例都是static final類型的,也就代表只能被實例化一次。在調用構造方法時,咱們的單例被實例化。 也就是說,由於enum中的實例被保證只會被實例化一次,因此咱們的INSTANCE也被保證明例化一次

上面示例中生成的字節碼文件對instance的描述以下:

...
public static final eft.reflex.SingletonEnum instance;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM


...

複製代碼

能夠看出,會自動生成 ACC_STATIC, ACC_FINAL這兩個修飾符

枚舉類型爲何是線程安全的?

咱們定義的一個枚舉,在第一次被真正用到的時候,會被虛擬機加載並初始化,而這個初始化過程是線程安全的。而咱們知道,解決單例的併發問題,主要解決的就是初始化過程當中的線程安全問題。因此,因爲枚舉的以上特性,枚舉實現的單例是天生線程安全的。

爲何使用枚舉類型的單例模式更耗內存?

這裏咱們從字節碼的角度分析,並對比靜態內部類的方式來講明 首先看下靜態內部類單例生成的字節碼:

Classfile /G:/demo/reflexDemo/out/production/reflexDemo/eft/reflex/SingletonInner.class
  Last modified 2019-8-8; size 500 bytes
  MD5 checksum c69eb5edd5eec02d87359065d8650f02
  Compiled from "SingletonInner.java"
public class eft.reflex.SingletonInner
  SourceFile: "SingletonInner.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
 #1 = Methodref #4.#19 // java/lang/Object."<init>":()V
 #2 = Methodref #5.#20 // eft/reflex/SingletonInner$SingletonHolder.access$000:()Left/reflex/SingletonInner;
 #3 = Class #21 // eft/reflex/SingletonInner
 #4 = Class #22 // java/lang/Object
 #5 = Class #23 // eft/reflex/SingletonInner$SingletonHolder
 #6 = Utf8 SingletonHolder
 #7 = Utf8 InnerClasses
 #8 = Utf8 <init>
 #9 = Utf8 ()V
 #10 = Utf8 Code
 #11 = Utf8 LineNumberTable
 #12 = Utf8 LocalVariableTable
 #13 = Utf8 this
 #14 = Utf8 Left/reflex/SingletonInner;
 #15 = Utf8 getInstance
 #16 = Utf8 ()Left/reflex/SingletonInner;
 #17 = Utf8 SourceFile
 #18 = Utf8 SingletonInner.java
 #19 = NameAndType #8:#9 // "<init>":()V
 #20 = NameAndType #24:#16 // access$000:()Left/reflex/SingletonInner;
 #21 = Utf8 eft/reflex/SingletonInner
 #22 = Utf8 java/lang/Object
 #23 = Utf8 eft/reflex/SingletonInner$SingletonHolder
 #24 = Utf8 access$000
{
  public eft.reflex.SingletonInner();
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 3: 0
        line 4: 4
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       5     0  this   Left/reflex/SingletonInner;

  public static eft.reflex.SingletonInner getInstance();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: invokestatic  #2                  // Method eft/reflex/SingletonInner$SingletonHolder.access$000:()Left/reflex/SingletonInner;
         3: areturn
      LineNumberTable:
        line 9: 0
}

複製代碼

再看枚舉單例生成的字節碼:

Classfile /G:/demo/reflexDemo/out/production/reflexDemo/eft/reflex/SingletonEnum.class
  Last modified 2019-8-9; size 989 bytes
  MD5 checksum b97cfb98be4e5ce15fd85e934cc9a75c
  Compiled from "SingletonEnum.java"
public final class eft.reflex.SingletonEnum extends java.lang.Enum<eft.reflex.SingletonEnum>
  Signature: #31                          // Ljava/lang/Enum<Left/reflex/SingletonEnum;>;
  SourceFile: "SingletonEnum.java"
  minor version: 0
  major version: 51
  flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
 #1 = Fieldref #4.#34 // eft/reflex/SingletonEnum.$VALUES:[Left/reflex/SingletonEnum;
 #2 = Methodref #35.#36 // "[Left/reflex/SingletonEnum;".clone:()Ljava/lang/Object;
 #3 = Class #14 // "[Left/reflex/SingletonEnum;"
 #4 = Class #37 // eft/reflex/SingletonEnum
 #5 = Methodref #10.#38 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
 #6 = Methodref #10.#39 // java/lang/Enum."<init>":(Ljava/lang/String;I)V
 #7 = String #11 // instance
 #8 = Methodref #4.#40 // eft/reflex/SingletonEnum."<init>":(Ljava/lang/String;I)V
 #9 = Fieldref #4.#41 // eft/reflex/SingletonEnum.instance:Left/reflex/SingletonEnum;
 #10 = Class #42 // java/lang/Enum
 #11 = Utf8 instance
 #12 = Utf8 Left/reflex/SingletonEnum;
 #13 = Utf8 $VALUES
 #14 = Utf8 [Left/reflex/SingletonEnum;
 #15 = Utf8 values
 #16 = Utf8 ()[Left/reflex/SingletonEnum;
 #17 = Utf8 Code
 #18 = Utf8 LineNumberTable
 #19 = Utf8 valueOf
 #20 = Utf8 (Ljava/lang/String;)Left/reflex/SingletonEnum;
 #21 = Utf8 LocalVariableTable
 #22 = Utf8 name
 #23 = Utf8 Ljava/lang/String;
 #24 = Utf8 <init>
 #25 = Utf8 (Ljava/lang/String;I)V
 #26 = Utf8 this
 #27 = Utf8 Signature
 #28 = Utf8 ()V
 #29 = Utf8 doThing
 #30 = Utf8 <clinit>
 #31 = Utf8 Ljava/lang/Enum<Left/reflex/SingletonEnum;>;
 #32 = Utf8 SourceFile
 #33 = Utf8 SingletonEnum.java
 #34 = NameAndType #13:#14 // $VALUES:[Left/reflex/SingletonEnum;
 #35 = Class #14 // "[Left/reflex/SingletonEnum;"
 #36 = NameAndType #43:#44 // clone:()Ljava/lang/Object;
 #37 = Utf8 eft/reflex/SingletonEnum
 #38 = NameAndType #19:#45 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
 #39 = NameAndType #24:#25 // "<init>":(Ljava/lang/String;I)V
 #40 = NameAndType #24:#25 // "<init>":(Ljava/lang/String;I)V
 #41 = NameAndType #11:#12 // instance:Left/reflex/SingletonEnum;
 #42 = Utf8 java/lang/Enum
 #43 = Utf8 clone
 #44 = Utf8 ()Ljava/lang/Object;
 #45 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
{
  public static final eft.reflex.SingletonEnum instance;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM

  public static eft.reflex.SingletonEnum[] values();
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: getstatic     #1                  // Field $VALUES:[Left/reflex/SingletonEnum;
         3: invokevirtual #2                  // Method "[Left/reflex/SingletonEnum;".clone:()Ljava/lang/Object;
         6: checkcast     #3                  // class "[Left/reflex/SingletonEnum;"
         9: areturn
      LineNumberTable:
        line 3: 0

  public static eft.reflex.SingletonEnum valueOf(java.lang.String);
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: ldc_w         #4                  // class eft/reflex/SingletonEnum
         3: aload_0
         4: invokestatic  #5                  // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
         7: checkcast     #4                  // class eft/reflex/SingletonEnum
        10: areturn
      LineNumberTable:
        line 3: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0      11     0  name   Ljava/lang/String;

  public void doThing();
    flags: ACC_PUBLIC
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 8: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
               0       1     0  this   Left/reflex/SingletonEnum;

  static {};
    flags: ACC_STATIC
    Code:
      stack=4, locals=0, args_size=0
         0: new           #4                  // class eft/reflex/SingletonEnum
         3: dup
         4: ldc           #7                  // String instance
         6: iconst_0
         7: invokespecial #8                  // Method "<init>":(Ljava/lang/String;I)V
        10: putstatic     #9                  // Field instance:Left/reflex/SingletonEnum;
        13: iconst_1
        14: anewarray     #4                  // class eft/reflex/SingletonEnum
        17: dup
        18: iconst_0
        19: getstatic     #9                  // Field instance:Left/reflex/SingletonEnum;
        22: aastore
        23: putstatic     #1                  // Field $VALUES:[Left/reflex/SingletonEnum;
        26: return
      LineNumberTable:
        line 4: 0
        line 3: 13
}


複製代碼

靜態對比: 能夠看出枚舉類默認繼承java.lang.Enum 對比兩個字節碼的常量池(Constant pool)個數,SingletonInner.class 24個,SingletonEnum.class 45個 對比兩個字節碼文件大小,SingletonInner.class 500字節,SingletonEnum.class 989字節,差了將近兩倍,咱們知道jvm虛擬機會將class文件中的常量池載入到內存中,並保存在方法區,因此單從這點看,枚舉會更耗內存(雖然這並不表明實際運行起來就所耗內存的差異),等有了更有說服力的證據再來更新~

爲何枚舉反序列化不會生成新的實例?

經過上面的字節碼,咱們能夠看出枚舉類默認繼承java.lang.Enum(而不是java.lang.Object),看下Enum類源碼:

/** * prevent default deserialization--阻止默認反序列化 */
    private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
        throw new InvalidObjectException("can't deserialize enum");
    }
    
    private void readObjectNoData() throws ObjectStreamException {
        throw new InvalidObjectException("can't deserialize enum");
    }
複製代碼

咱們知道,之前的全部的單例模式都有一個比較大的問題,就是一旦實現了Serializable接口以後,就再也不是單例得了,由於,每次調用 readObject()方法返回的都是一個新建立出來的對象,有一種解決辦法就是使用readResolve()方法來避免此事發生。可是,爲了保證枚舉類型像Java規範中所說的那樣,每個枚舉類型極其定義的枚舉變量在JVM中都是惟一的,在枚舉類型的序列化和反序列化上,Java作了特殊的規定,原文不貼了,大概意思就是說,在序列化的時候Java僅僅是將枚舉對象的name屬性輸出到結果中,反序列化的時候則是經過java.lang.Enum的valueOf方法來根據名字查找枚舉對象。同時,編譯器是不容許任何對這種序列化機制的定製的,所以禁用了writeObject、readObject、readObjectNoData、writeReplace和readResolve等方法。

在序列化過程當中,若是被序列化的類中定義了了writeObject 和 readObject 方法,虛擬機會試圖調用對象類里的 writeObject 和 readObject 方法,進行用戶自定義的序列化和反序列化。若是沒有這樣的方法,則默認調⽤用是 ObjectOutputStream 的 defaultWriteObject 方法以及ObjectInputStream 的 defaultReadObject 方法

六、使用容器實現單例模式

在程序的初始化,將多個單例類型注入到一個統一管理的類中,使用時經過key來獲取對應類型的對象,這種方式使得咱們能夠管理多種類型的單例,而且在使用時能夠經過統一的接口進行操做。 這種方式是利用了Map的key惟一性來保證單例。

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); 
 } 

}

複製代碼

總結

全部單例模式須要處理得問題都是:

  1. 將構造函數私有化
  2. 經過靜態方法獲取一個惟一實例
  3. 保證線程安全
  4. 防止反序列化形成的新實例等。

推薦使用:DCL、靜態內部類、枚舉

單例模式優勢

  1. 只有一個對象,內存開支少、性能好(當一個對象的產生須要比較多的資源,如讀取配置、產生其餘依賴對象時,能夠經過應用啓動時直接產生一個單例對象,讓其永駐內存的方式解決)
  2. 避免對資源的多重佔用(一個寫文件操做,只有一個實例存在內存中,避免對同一個資源文件同時寫操做)
  3. 在系統設置全局訪問點,優化和共享資源訪問(如:設計一個單例類,負責全部數據表的映射處理)

單例模式缺點

  1. 通常沒有接口,擴展難
  2. android中,單例對象持有Context容易內存泄露,此時須要注意傳給單例對象的Context最好是Application Context

android源碼中的單例模式

單例模式應用普遍,根據實際業務需求來,這裏只引出源碼中個別場景,再也不詳解,有興趣的讀者能夠深刻查看源碼

在平時的Android開發中,咱們常常會經過Context來獲取系統服務,好比ActivityManagerService,AccountManagerService等系統服務,實際上ContextImpl也是經過SystemServiceRegistry.getSystemService來獲取具體的服務,SystemServiceRegistry是個final類型的類。這裏使用容器實現單例模式

SystemServiceRegistry 部分代碼:

final class SystemServiceRegistry {
    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 SystemServiceRegistry() { }
    
    static {
        registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
        registerService(Context.ACTIVITY_SERVICE, ActivityManager.class,
                new CachedServiceFetcher<ActivityManager>() {
            @Override
            public ActivityManager createService(ContextImpl ctx) {
                return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
            }});
        .......
    }
    
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
    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);
    }
    ......
}
複製代碼
  1. WindowManagerImpl 中的WindowManagerGlobal(懶漢式)
public static WindowManagerGlobal getInstance() {
    synchronized (WindowManagerGlobal.class) {
        if (sDefaultWindowManager == null) {
            sDefaultWindowManager = new WindowManagerGlobal();
        }
        return sDefaultWindowManager;
    }
}
複製代碼

參考資源

  • 《Android源碼設計模式解析與實戰》
相關文章
相關標籤/搜索