单例模式的六种写法

4,659 阅读10分钟

定义

确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例

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源码设计模式解析与实战》