本文对应原书条目3,原书仅仅提到了如何实现单例模式,本文想在此基础上做一定的拓展,力求较为全面地介绍单例模式,探讨单例模式的应用场景、优缺点及多种实现方式,以及如何防范序列化和反射导致的安全性问题。如有问题或建议,欢迎指教,谢谢~
单例模式是一个只会被实例化一次的类,它会自行实例化,并提供可全局访问的方法。
有三种实现单例的方式,公共静态不可变成员、静态工厂方法和枚举。前两种比较类似,都是通过私有构造方法+公共静态成员的方式提供单例。而第三种枚举的方式是在Java1.5以后引入的,事实上我们在后面会看到这是Java语言实现单例的最佳实践。
这种方式的具体实现如下:
public class Singleton { public static final Singleton INSTANCE = new Singleton(); private Singleton() {} }
这种方式实现起来比较简单,而且可以清楚地标明这是个单例类,但是缺点正如第一篇学习笔记中提到的,对它的访问不如静态工厂方法来得清晰,所以就有了下面使用静态工厂方法实现单例的方式。
我们先来看看最典型的静态工厂方法实现的单例。
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }
静态工厂方法实现单例有几个好处 [1]
。首先,它具备 灵活性
,在不改变对外发布的API的前提下,我们可以改变它内部的实现,比如从单例变成非单例,或是每个线程一个单例。其次,它的 可扩展性
强,可以使用泛型单例工厂的方式提供单例的访问(这个我们以后再讨论)。最后是它的 便利性
,可以支持方法引用,像 Singleton::instance
这样。
使用静态工厂方法实现单例有很多种玩法 [2]
,上面那种被称为 饿汉式
,它的 优点
是线程安全、便于使用; 缺点
是应用初始化时较慢,如果这个单例对象一直没有使用,会浪费内存空间。
下面是它的一个变种:
public class Singleton { private static final Singleton INSTANCE; static { // 一些前置操作 INSTANCE = new Singleton(); } private Singleton() {} public static Singleton getInstance() { return INSTANCE; } }
这种变种其实就是把单例对象的初始化过程放到了静态代码块中,优缺点同上。我理解主要适用于实例化单例类需要一些前置操作的情况。
除此之外,还有其他的写法——
public class Singleton { private static final Singleton INSTANCE; private Singleton() {} public static Singleton getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton(); } return INSTANCE; } }
懒汉式将初始化单例变量的时机放在了第一次调用的时候(懒加载),这样做的 优点 在于可以加快启动速度,且不会像饿汉式那样造成可能的内存空间浪费,但是 缺点 在于无法保证线程安全性。
public class Singleton { private static final Singleton INSTANCE; private Singleton() {} public static synchronized Singleton getInstance() { if (INSTANCE == null) { INSTANCE = new Singleton(); } return INSTANCE; } }
这一变种形式的 优点
同上,并解决了上面的线程不安全问题,但是 缺点
在于对 getInstance()
方法进行了同步,并发性能较差。
public class Singleton { // 这里加了volatile关键字修饰 private static volatile final Singleton INSTANCE; private Singleton() {} public static Singleton getInstance() { // 双重检查 if (INSTANCE == null) { synchronized(Singleton.class) { if (INSTANCE == null) { INSTANCE = new Singleton(); } } } return INSTANCE; } }
这种方式的 优点
是在保证线程安全的前提下提高了多线程访问的性能。因为采用了volatile关键字+代码块加锁+两次是否null检查,当一个线程初始化了 INSTANCE
后,其他线程马上可见了。它的 缺点
是实现起来比较复杂。
public class Singleton { private Singleton() {} private static class SingletonHolder() { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
这种方式本质上也是懒加载的,拥有懒加载方式的优点。它采用类加载的机制实现懒加载和保证线程安全,只有第一次调用 getInstance()
方法的时候才会装载内部类 SingletonHolder
。
public enum Singleton { INSTANCE; public void yourOwnMethod() {} }
你或许会觉得枚举这种方式很奇怪,但是它事实上兼具了上述所有的优点,加载效率高,并发性能好,而且易于编写。并且在后面我们还可以看到,它的安全性也非常高,不需要我们采取额外的防范。
有一些手段能够破坏类的单例模式,比如通过 序列化 和 反射 的方式。
Java语言的序列化主要依靠 ObjectOutputStream
和 ObjectInputStream
这两个类。前者负责将对象序列化为二进制数组,而后者负责反序列化。通过 ObjectOutputStream
的 writeObject()
方法将单例对象写入外部文件,再通过 ObjectInputStream
的 readObject()
方法从外部读取一个二进制数组进来写入单例中,这个时候单例就成了另外一个对象了。如下面的代码所示 [2]
:
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } public static void main(String[] args) throws IOException, ClassNotFoundException { Singleton singleton1 = Singleton.getInstance(); ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_bin_file")); oos.writeObject(singleton1); // 序列化 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_bin_file")); Singleton singleton2 = (Singleton) ois.readObject(); // 反序列化 System.out.println(singleton1 == singleton2); // 会返回false } }
为了防止被这种方式攻击,我们可以在单例类中加入 readResolve()
方法。如下所示:
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() {} public static Singleton getInstance() { return INSTANCE; } private Object readResolve() { return INSTANCE; } }
为什么这样可行呢?因为Java的序列化机制在允许类自己实现一个 readResolve()
方法,在 ObjectInputStream
执行了 readObject()
之后,如果存在 readResolve()
方法,则会调用,并对 readObject()
的结果进行处理,之后作为最终的结果返回。像我们上面那样在 readResolve()
中返回了原本的 INSTANCE
,这样就能保证不会因 readObject()
生成新的对象,从而确保了单例机制不被破坏 [2]
。
另外,如果单例中有成员变量,应当声明为 transient
类型 [1]
,这样,在序列化的时候会跳过这个字段,而反序列化时会获得一个默认值或者null。我理解这样做的目的是保护单例的成员变量,不让它们泄露出去,也不会被乱赋值。没有值总比被赋了错值要好。
反射对单例的破坏主要是通过调用成员变量或者构造方法的 setAccessible()
方法,来访问原本 private
的变量或者方法,从而破坏了单例模式。
对反射攻击的防御可以通过在构造方法中增加校验的方式实现,如下所示:
public class Singleton { private static final Singleton INSTANCE = new Singleton(); private Singleton() { if (INSTANCE != null) { throw new RuntimeException("INSTANCE already exists!"); } } public static Singleton getInstance() { return INSTANCE; } }
这种方式只对 饿汉式
单例实现有效,而对 懒汉式
无效。因为前者的单例在类加载时即被初始化了,类加载的时机一定是在反射前的;而后者则是在 getInstance()
被调用时才初始化单例,不能保证在反射之前执行。 [2]
不论是通过公共静态不可变成员还是静态工厂方法来实现单例,都有缺陷,需要程序员自己去保证性能和安全。然而,正如前面所看到的,还有一种更好的方式来实现单例,那就是 枚举 。
public enum Singleton { INSTANCE; private String yourOwnField; public String getYourOwnField() { return yourOwnField; } public void setYourOwnField(String yourOwnField) { this.yourOwnField = yourOwnField; } public void yourOwnMethod() {} }
枚举有如下几个优点 [2] :
public static final
修饰,而静态变量会在类加载时被初始化,因此JVM会保证其线程安全性。 序列化、反射和枚举这几部分参考资料[2]中讲得很透彻,建议大家阅读下~
单例模式提供了对某一对象的受控访问,适用于很多场景。用枚举来实现单例是最好的方式。下面是单例模式的优缺点 [2] [3] :
小小的感慨:虽然Effective Java上面这个条目的内容非常少,但是自己去深挖以后发现居然有这么多值得研究的东西。个人感觉书上讲得还是太简单,好多地方都没有讲透,也没有相应的例子。还是得靠自己多搜索资料,多思考,才能吃透一块知识。
本文仅用于学习交流,请勿用于商业用途。转载请注明出处,谢谢。