单例模式主要分为
(一)、懒汉式单例
(二)、饿汉式单例(三)、双重检查锁定
(四)、静态(类级)内部类
(五)、单例和枚举
通常情况下,我们写单例模式的时候无非就是三个步骤:构造器私有化,声明私有静态变量,提供静态获取实例的方法
那么一个完美的单例需要做到哪些事呢?
- 单例
- 延迟加载
- 线程安全
- 没有性能问题
- 防止序列化产生新对象
- 防止反射攻击
- (一)懒汉式-------懒到第一次调用才去实例化
- public class Singleton {
- private static Singleton INSTANCE;
- private Singleton (){}
- public static Singleton getInstance() {
- if (INSTANCE == null) {
- INSTANCE = new Singleton();
- }
- return INSTANCE;
- }
- }
(二)饿汉式 ----------类加载即实例化
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
// 私有化构造函数
private Singleton(){}
public static Singleton getInstance(){
return INSTANCE;
}
}
三、四都是懒汉式的改进
(三)双重检查锁定
public static Singleton getSingleton() {
if (INSTANCE == null) { // 第一次检查
synchronized (Singleton.class) {
if (INSTANCE == null) { // 第二次检查
INSTANCE = new Singleton();
}
}
}
return INSTANCE ;
}
可不看:
这段代码看起来很完美,但仍旧存在问题,以下内容引用自黑桃夹克大神的如何正确地写出单例模式
这段代码看起来很完美,很可惜,它是有问题。主要在于instance = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
我们只需要将 instance 变量声明成 volatile 就可以了。
加了关键字
- public class Singleton {
- private volatile static Singleton INSTANCE; //声明成 volatile
- private Singleton (){}
- public static Singleton getSingleton() {
- if (INSTANCE == null) {
- synchronized (Singleton.class) {
- if (INSTANCE == null) {
- INSTANCE = new Singleton();
- }
- }
- }
- return INSTANCE;
- }
- }
使用 volatile 的主要原因是其另一个特性:禁止指令重排序优化。也就是说,在 volatile 变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完 1-2-3 之后或者 1-3-2 之后,不存在执行到 1-3 然后取到值的情况。从「先行发生原则」的角度理解的话,就是对于一个 volatile 变量的写操作都先行发生于后面对这个变量的读操作(这里的“后面”是时间上的先后顺序)。
- public class Singleton {
- /**
- * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例没有绑定关系,
- * 而且只有被调用到才会装载,从而实现了延迟加载
- */
- private static class SingletonHolder{
- /**
- * 静态初始化器,由JVM来保证线程安全
- */
- private static final Singleton instance = new Singleton();
- }
- /**
- * 私有化构造方法
- */
- private Singleton(){
- }
- public static Singleton getInstance(){
- return SingletonHolder.instance;
- }
- }
getInstance()
方法调用时,才会初始化 instance。同时,由于实例的建立是时在类加载时完成,故天生对多线程友好,
getInstance()
方法也无需使用同步关键字。
单例之枚举
- public enum Singleton{
- INSTANCE;
- }
这种方式的好处是:
- 利用的枚举的特性实现单例
- 由JVM保证线程安全
- 序列化和反射攻击已经被枚举解决
调用方式为Singleton.INSTANCE
, 出自《Effective Java》第二版第三条: 用私有构造器或枚举类型强化Singleton属性。
关于单例最佳实践的讨论可以看Stackoverflow:what-is-an-efficient-way-to-implement-a-singleton-pattern-in-java
下面将会介绍更为常见的单例模式,但是均未处理反射攻击,如果想了解更多可以看这篇文章:如何防止单例模式被JAVA反射攻击
上述一到四都无法抵抗反射攻击,而序列化攻击(对象序列化反序列化会出现对象不相同的情况)的话可以通过
用饿汉式做例子
- public class Singleton implements Serializable {
- private static final Singleton INSTANCE = new Singleton();
- // 私有化构造函数
- private Singleton(){}
- public static Singleton getInstance(){
- return INSTANCE;
- }
- /**
- * 如果实现了Serializable, 必须重写这个方法
- */
- private Object readResolve() throws ObjectStreamException {
- return INSTANCE;
- }
- }
重写readResolve即可!!