單例模式的六種寫法

酸辣湯發表於2019-08-06

定義

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

UML結構圖

單例模式的六種寫法

場景

  • 需要頻繁的例項化和銷燬的物件;
  • 有狀態的工具類物件
  • 頻繁訪問資料庫或檔案物件;
  • 確保某個類只有一個物件的場景,比如一個物件需要消耗的資源過多,訪問io、資料庫,需要提供全域性配置的場景

幾種單例模式

1、餓漢式

宣告靜態時已經初始化,在獲取物件之前就初始化

優點:獲取物件的速度快,執行緒安全(因為虛擬機器保證只會裝載一次,在裝載類的時候是不會發生併發的)

缺點:耗記憶體(若類中有靜態方法,在呼叫靜態方法的時候類就會被載入,類載入的時候就完成了單例的初始化,拖慢速度)

public class EagerSingleton {
    //餓漢單例模式
    //在類載入時就完成了初始化,所以類載入較慢,但獲取物件的速度快
    private static EagerSingleton instance = new EagerSingleton();//靜態私有成員,已初始化
    
    private EagerSingleton() 
    {
        //私有建構函式
    }
    
    public static EagerSingleton getInstance()    //靜態,不用同步(類載入時已初始化,不會有多執行緒的問題)
    {
        return instance;
    }
    
}

複製程式碼

2、懶漢式

synchronized同步鎖: 多執行緒下保證單例物件唯一性

優點:單例只有在使用時才被例項化,一定程度上節約了資源

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


    //懶漢式單例模式
    //比較懶,在類載入時,不建立例項,因此類載入速度快,但執行時獲取物件的速度慢
    private static LazySingleton intance = null;//靜態私用成員,沒有初始化
    
    private LazySingleton()
    {
        //私有建構函式
    }
    
    public static synchronized LazySingleton getInstance()    //靜態,同步,公開訪問點
    {
        if(intance == null)
        {
            intance = new LazySingleton();
        }
        return intance;
    }
}
複製程式碼

3、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失效問題(雙重鎖定失效)

4、靜態內部類單例模式

在呼叫 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方法並沒有被同步,並且只是執行一個域的訪問,因此延遲初始化並沒有增加任何訪問成本。

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

答案是:不能。網上很多介紹到靜態內部類的單例模式的優點會提到“通過反射,是不能從外部類獲取內部類的屬性的。 所以這種形式,很好的避免了反射入侵”,這是錯誤的,反射是可以獲取內部類的屬性(想了解更多反射的知識請看 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方法來返回我們指定好的物件

5、列舉單例

優點:執行緒安全,防止被反序列化

缺點:列舉相對耗記憶體

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 方法

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

在程式的初始化,將多個單例型別注入到一個統一管理的類中,使用時通過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原始碼設計模式解析與實戰》

相關文章