目录
单例模式介绍
单例模式(Singleton Pattern)是 Java 中最简单、最常用的设计模式之一。单例模式提供了一种在多线程环境下保证实例唯一性的解决方案。即:对象一经初始化,后需可以直接访问,不需要再次实例化该类的对象。属于设计模式三大类中的创建型模式。在Java中,一般常用在工具类的实现、对象创建开销较大的情景下。
单例模式具有典型的三个特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。(自我实例化)
- 单例类必须给所有其他对象提供这一实例。(提供全局访问点)
单例模式虽然比较简单,但实现方式却多种多样,本篇列举了7种实现方式,并从线程安全
、高性能
、懒加载
三个维度对其进行评估,比较其优劣。
一、饿汉式
饿汉模式,顾名思义,就是采用静态初始化的方式在类被加载时就将自身实例化,所以被形象地称之为饿汉式单例模式。
// final不允许被继承
public final class HungrySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
//第一步:私有化构造器,不允许外部new操作
private HungrySingleton(){
}
//第二步:在类初始化时立即实例化该对象,从而保证线程安全
private static HungrySingleton instance = new HungrySingleton();
//第三步:提供一个获取全局访问点的方法
public static HungrySingleton getInstance() {
return instance;
}
//other methods
}
饿汉式把instance
作为"类变量"并且直接初始化,当主动使用Singleton 类时会完成instance
的创建,包括其中的实例变量都会得到初始化,比如上例中的data数组将被创建并占用1K的空间。如果instance
被ClassLoader加载后很长一段时间才被使用,那就意味着instance
实例所开辟的堆内存会驻留更久的时间,如果一个类的成员占用的内存资源较多,那么采用饿汉式就有些不妥。
总结
- 线程安全。
- getInstance方法的性能比较高。
- 无法进行懒加载。
注意:上面代码中有使用final关键字,强制该类不允许被继承。若父类所有的构造器都是私有的(private修饰),那么JVM规定该父类不允许被继承,因为子类的构造器都必须显示或隐式调用父类的构造器。此时可以不使用final关键字声明。
JDK饿汉式单例举例:
二、懒汉式
懒汉式就是在第一次使用类实例的时候再去创建,和饿汉式在类初始化时就提前创建实例不同,所以就被称为懒汉式单例模式。
//final不允许被继承
public final class LazySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
// 未实例化的类变量
private static LazySingleton instance = null;
// 私有化构造器
private LazySingleton() {
}
// 运行时加载对象
public static LazySingleton getInstance() {
//判断是否已经初始化过(没有同步机制控制,多线程不安全)
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//other methods
}
总结
- 线程不安全。instance是共享资源,当多个线程对其访问时需要保证共享资源的同步性,因此线程不安全,无法保证单例的唯一性。(更加具体的原因不摊开说明了)
- 性能和懒加载,就不讨论了,因为这种方法本身就不正确。
三、懒汉式+synchronized同步
上述的懒汉式保证了实例的懒加载,但无法保证实例的唯一性,需要增加对共享资源instance的同步访问机制,可以采用synchronized关键字实现。
//final不允许被继承
public final class LazySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
// 未实例化的类变量
private static LazySingleton instance = null;
// 私有化构造器
private LazySingleton() {
}
// 运行时加载对象(增加了synchronized,每次只能有一个线程能够进入)
public static synchronized LazySingleton getInstance() {
//判断是否已经初始化过(没有同步机制控制,多线程不安全)
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
//other methods
}
总结
- 线程安全,能够保证实例的唯一性;
- getInstance方法采用synchronized关键字所以性能较低
- 懒加载。
四、Double-Check式(注意有坑)
Double-Check的方式是一种更加高效的数据同步策略,只有首次初始化时才加锁,之后多个线程获取实例时都无需同步控制。
// final不允许被继承
public final class LazySingleton {
//类的成员变量(一般类都有成员变量)
private byte[] data = new byte[1024];
// 未实例化的类变量
private static LazySingleton instance = null;
// 私有化构造器
private LazySingleton() {
}
public static LazySingleton getSingleton() {
// 若instance不为null,则不用获取锁,提升了效率
if (instance == null) {
//同步加锁是为了线程安全,确保只有一个线程创建实例
synchronized (LazySingleton.class) {
//再次判空是为了保证单例对象的唯一性,只有没被创建才去创建
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
//other methods
}
当两个线程同时发现instance == null
成立时,只有一个线程有资格进入synchronized同步代码块完成instance的实例化,随后的线程进入synchronized同步代码块后发现instance == null
不成立则无需再次实例化,以后对getSingleton方法的访问也不需要执行synchronized同步代码块,大大提升了性能。满足懒加载、线程安全、高性能这三个标准,一切看起来很完美。但这种方式在多线程环境下可能会导致空指针异常,原因如下。
首先,我们要理解new LazySingleton()
做了什么,详细的介绍请查看《java new一个对象的过程中发生了什么》。本篇简单介绍new一个对象需要的4个步骤,如下:
- 看class对象是否加载,如果没有就先加载class对象。(加载)
- 为类的静态变量分配内存空间并为其初始化默认值(连接阶段),为静态变量赋予正确的初始值(初始化阶段)。
- 调用构造函数。(单例比较复杂时,有很多的成员变量需要初始化)
- 返回地址给引用。
然后,cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配(或是只实例化了部分成员变量),就被使用了,导致空指针。下面举例:
线程A执行到
new LazySingleton()
,开始初始化实例对象,由于存在指令重排序,先执行步骤4,先把引用instance
赋值了,此时还没有执行构造函数(或执行还未完成,只实例化了部分成员变量),这时CPU时间片耗尽,切换到线程B执行,线程B调用new LazySingleton()
方法,发现instance == null
不成立,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量,报空指针错误。
五、Volatile + Double-Check式(最终版)
Double-Check是一种巧妙的设计,但由于JVM在运行new LazySingleton()
时可能对指令重排序,导致空指针异常。而volatile关键字可以防止重排序,因此还需要对Double-Check方式稍加修改。关于volatile关键字的用法,可参考另一篇博文《深入理解volatile关键字》。
// 加volatile 修饰
private static volatile LazySingleton instance = null;
至此,就有了一个线程安全、高性能、懒加载版本的双重检查加锁式单例模式。但这种写法对于初学者很棘手,一下子很难理解。
六、Holder式
直接上代码,然后给出说明。
// 不允许被继承
public final class HolderSingleton {
// 类的成员变量
private byte[] data = new byte[1024];
// 私有化构造器
private HolderSingleton() {
}
// 静态内部类 Holder中持有实例instance
private static class Holder{
private static HolderSingleton instance = new HolderSingleton();
}
// 调用getSingleton,返回Holder的instance类属性
public static HolderSingleton getSingleton() {
return Holder.instance;
}
//other methods
}
这种方式利用了类加载的特点。HolderSingleton 类中没有持有静态的instance实例,而是放在静态内部类Holder中,该方式仍然需要私有化构造器。当Holder被主动引用时(懒加载)会创建HolderSingleton 的实例,JVM保证实例的唯一性,性能高。是目前广泛采用的一种单例设计。
七、枚举式
八、防止反射/反序列化攻击单例类
上面的单例实现方式中,除了枚举类型
外,其他的实现方式是可以被JAVA的反射机制攻击的。享有特权的客户端可以借助AccessibleObject.setAccessible
方法通过反射机制调用私有构造器,如果需要抵御这种攻击,可以修改构造器,让它的被要求创建第二个实例的时候抛出一个异常。具体方式请参考 《如何防止JAVA反射对单例类的攻击?》 《设计模式——单例模式》
参考资料
- 《java高并发编程详解》 汪文君
- 《Effective Java 中文版 第2版》
- 如何防止JAVA反射对单例类的攻击?
- 单例模式为什么要用Volatile关键字
- java new一个对象的过程中发生了什么
- 设计模式——单例模式
- 深入理解Java枚举类型(enum)