创建型 — 单例模式(Singleton)
为什么需要单例模式
单例模式是自己最先接触的一种设计模式,当时还是开发C++的代码。当时的应用场景是一个控制台程序,对于一个管理资源的类,也涉及初始化、启动等,这样的类只适合构造一个实例,然后不断的复用,保证在运行进程内只有一个实例,便于管理;同时也能减少资源的开销。
从面向对象的概念上讲,我们知道封装,将相关联的东西封装成一个类。在Java中,一种最简单的模式就是POJO(或者叫Java Bean),包括一些属性和对应的getter和setter方法。这种是对属性的封装,通常是比较明显的对应一些实体,他们提供对数据的封装。另外的,就是更常见的类,因为面向对象编程,特别是在Java中,类似的操作或者语义都放在一个类中。
当需要使用类中的方法时,可以声明一个类的实例,每一个类的实例都拥有这个类的非static属性的一份拷贝,方法也是类似。通常的做法是,在需要用的地方声明一个实例,然后用实例去调用对应的方法。但是有的时候,希望这个类只存在一个实例,也就是这个类中的属性只有一份状态,在各处使用时都是对那一份数据的操作。
简单的单例模式
在Java中,常见的模式如下:
public class Singleton { //私有,静态的一个实例 private static Singleton instance = new Singleton(); // 必须得实现一个私有的无参构造函数,防止调用方直接new实例 private Singleton() {} // 供使用者调用 public static Singleton getInstance() { return instance; } }
类的使用者要想使用这个类,只有一种方法得到这个类的实例;并且在类的内部,这个实例是静态的,只存在一份。因此,这就保证了“单例”。注意,这样的写法也是线程安全的。
但这中方法有一个问题,就是在类加载时就要初始化,如果如下初始化的内容比较多,加载起来就会比较慢,因此有了新的方法——延迟初始化(Lazy Initialize),如下:
public class Singleton { //私有,静态的一个实例,先不初始化 private static Singleton instance; // 必须得实现一个私有的无参构造函数,防止调用方直接new实例 private Singleton() {} // 供使用者调用 public static Singleton getInstance() { if (instance != null) { instance = new Singleton(); } return instance; } }
但是这样又出现了一个问题,就是会导致线程不安全!
其他方案
但是常见的模式在并发环境中会出现问题。当多个线程同时调用getInstance()方法时,有可能某个线程得到的instance还没有被初始化,这样将不会得到一个初始化好的实例。首先想的的方法是加同步。
这个时候通常还结合另一种技术——延迟初始化,也就是不采用简单单例模式中的,在声明变量是就初始化的方式,而是等到要用时在进行初始化。同时,还会加上双重检查,得到的通常模式如下:
class UnSafeSingleton { private static UnSafeSingleton instance; private UnSafeSingleton() {} public static UnSafeSingleton getInstance() { if (instance == null) { synchronized (UnSafeSingleton.class) { if (instance == null) { instance = new UnSafeSingleton(); } } } return instance; } }
可以看到,这种方式加上了同步处理,并且进行了延迟初始化,稍微不注意,以为这个会是线程安全的。但是,这边还是有问题,就是在给instance赋值时,还是会出现不一致的问题。
一种解决办法是,给instance属性加上volatile特征,这样保证instance对各个线程的可见性,保证只有一份存在。
另一种方法是采用基于类初始化的方法。JVM在类的初始化阶段(即在Class在被加载后,且被线程使用之前),会执行类的初始化。
在执行类的初始化期间,JVM回去获得一个锁。这个锁可以同步多个线程对同一个类的初始化。代码如下:
class SafeSingleton { private SafeSingleton() {} private static class SafeSingletonHolder { public static SafeSingleton intance = new SafeSingleton(); } public static SafeSingleton getInstance() { return SafeSingletonHolder.intance; } }
这样,即使多个线程去调用getInstance()方法,但是在初始化类SafeSingletonHolder时,都能同步进行,因此可以保证线程安全性。
小结
综上所述,单例模式本身很简单,但是如果要是线程安全,还是得有很多考虑。通过比较发现,基于类初始化的方法比较简单,代码简洁,可以常用。但是基于volatile的双重检查锁定方法,在延迟初始化非静态实例字段时,唯一可用。
补充
最近看了些《Effective Java》,发现在第一章第3条,讲到单例模式时,只列举了以下三种方式:
1)直接定义public static final Single INSTANCE= new Single(),此instance供外界直接调用。同时得有私有构造函数。
2)instance变为私有,但也是定义时初始化,对外提供一个接口,public static Single getInstance() { return INSTANCE; },这种方式也是书中所讲的工厂方法。
3)单元素的枚举类型。public enum Single { INSTANCE; }
作者在将前两种方式时,都强调,享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造函数。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。
当然,要想更简单的防止反射攻击,作者也写明,直接利用单元素枚举类型就可以天然的解决。
至于延迟初始化,作者放在了后面去讲,第71条"慎用延迟初始化"。作者觉得除非万不得已,就不用。同时给出两条建议。
1)如果出于性能考虑,而需要对静态域使用延迟初始化,就使用lazy initialization holder class模式。
2)如果处于性能考虑,而需要对实例域使用延迟初始化,就使用双重检查模式(doublecheck idiom)。域要声明为volatile。
也可以看出,如果是想用延迟初始化实现单例模式,建议的方式是lazy initialization holder class模式!!