上次写了一篇《单例模式那件小事,看了你不会后悔》的文章,总结了常用的单例模式的实现。本文是上文的延续,单例模式绝不是一件小事,想弄清楚,真不是那么简单的。上文提到了常用的三种单例模式的实现方法:饿汉式(除了提前占用资源,没毛病。),懒汉式(DCL优化过后,没毛病?),静态内部类式(优雅的方法,没毛病。)。文末最后还提到,反射会破坏单例。
本文继续,双重检查锁定优化过后的懒汉式,真的没毛病吗?其实不是,这里涉及到java编译器编译时的一些细节,对象初始化时的写操作与写入 sSingleton 字段的操作可以是无序的。这样的话,如果某个线程调用 getInstance 方法可能看到sSingleton 字段指向了一个 Singleton 对象,但看到该对象里的字段值却是默认值,而不是在 Singleton 构造方法里设置的那些值。这也就是上文提到的,如果不加入 volatile 关键字,编译器可能会失去大量优化的机会或者可能会在编译时出现一些不可预知的错误。那么加了该关键字之后呢,性能会大大降低,有兴趣并且由能力的人可以阅读《Java并发编程实践》一书,该书将 DCL 懒汉式单例模式形容为“臭名昭著”,不赞成使用。这里给个延伸链接:新的内存模型是否修复了双重锁检查问题?
下面继续说说我对单例模式的一些理解。
先从上文讲到的饿汉式说起:
public class Singleton { /** * 构造方法私有化 */ private Singleton() { } /** * 定义一个私有的静态的实例 */ private static Singleton sSingleton = new Singleton(); /** * 提供静态的方法给外界访问 * * @return */ public static Singleton getInstance() { return sSingleton; } }
下面我将代码修改为下面的形式:
public class Singleton { public static final Singleton SINGLETON = new Singleton(); private Singleton() { } }
我们不提供对外的 getInstance() 方法获取实例了,将 SINGLETON 定义为 public,同时将其定义为 final 类型,直接通过 Singleton.SINGLETON 获取,也没有问题。私有的构造方法仅会被调用一次,一旦 SINGLETON 被实例化,就只会存在一个实例,外界任何地方都再也不会改变它,我们知道常量就是这么定义的。当然,跟之前的集中方式一样,利用反射,还是可以通过私有构造方法创建新对象。除此之外,将对象序列化之后,在反序列化过程中,也会重新创建对象。
如何防止反射破坏单例模式呢?原理上就是在存在一个实例的情况下,再次调用构造方法时,抛出异常。下面以静态内部类的单例模式为例:
public class Singleton { private static boolean flag = false; private Singleton(){ synchronized(Singleton.class) { if(flag == false) { flag = !flag; } else { throw new RuntimeException("单例模式被侵犯!"); } } } private static class InnerClassSingleton { private final static Singleton sSingleton = new Singleton(); } public static Singleton getInstance() { return InnerClassSingleton.sSingleton; } }
定义了一个 boolean 类型的标志,判断是不是第一次调用构造方法,如果不是,即抛出异常。下面测试一下:
public class Test { public static void main(String[] args) { try { Class<Singleton> classType = Singleton.class; Constructor<Singleton> c = classType.getDeclaredConstructor(null); c.setAccessible(true); Singleton s1 = (Singleton)c.newInstance(); Singleton s2 = Singleton.getInstance(); System.out.println(s1==s2); } catch (Exception e) { e.printStackTrace(); } } }
运行结果如下:
Exception in thread "main" java.lang.ExceptionInInitializerError at com.joy.example.Singleton.getInstance(Singleton.java:27) at com.joy.example.Test.main(Singleton.java:17) Caused by: java.lang.RuntimeException: 单例模式被侵犯! at com.joy.example.Singleton.<init>(Singleton.java:16) at com.joy.example.Singleton.<init>(Singleton.java:7) at com.joy.example.Singleton$SingletonHolder.<clinit>(Singleton.java:22) ... 2 more
通过序列化可以讲一个对象实例写入到磁盘中,通过反序列化再读取回来的时候,即便构造方法是私有的,也依然可以通过特殊的途径,创建出一个新的实例,相当于调用了该类的构造函数。要避免这个问题,我们需要在代码中加入如下方法,让其在反序列化过程中执行 readResolve 方法时返回 sSingleton 对象。
private Object readResolve() throws ObjectStreamException { return sSingleton; }
那有没有一种方式实现的单例模式在任何情况下都是一个单例呢?有。
枚举单例
枚举,就能保证在任何情况下都是单例的,并且是线程安全的。写法也很简单:
虽然枚举实现单例很简单,也很安全。但是经验丰富的 Android 开发人员都会尽量避免使用枚举。官方文档有说明:相比于静态常量Enum会花费两倍以上的内存。
不管以哪种方式实现单例模式,核心思想都是一样:将构造方法私有化,然后通过静态方法获取唯一的实例对象。这个过程中对线程安全、反序列化操作、对立对象资源消耗、JDK版本等等问题都要考虑到。