单例模式

顾名思义,单例模式就是一个类有且只有一个实例,下面给出单例模式的定义:

The Singleton Pattern ensures a class has only one instance, and provides a global point of access to it.

解释:保证一个类仅有一个实例,并提供一个访问的它的全局访问点

如何才能保证一个类只会被实例化一次呢??

饿汉模式

形象化记忆:饿了,就要马上吃,所以得先初始化对象准备好,用时直接取即可

优点:只在类加载的时候初始化一次实例,不存在多线程创建实例的情况,避免了多线程同步问题 (懒汉模式中会提到同步问题)

缺点:如果该单例没有被使用也会被创建,造成了内存的浪费

适用场景:单例占用内存较小,在初始化时就会被用到的情况

懒汉模式

形象化记忆:懒了,用到了才会初始化返回

上述的代码定义了一个私有的构造函数,这使得只有在类Singleton中才能实例化对象,对于其他类来说,无法实例化Singleton对象

同时,提供了全局访问点getInstance()来获得Singleton实例。只有当uniqueInstance = null才会实例化,保证了一个类仅有一个实例

优点:只有在需要的时候才去创建,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象

适用场景:如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候懒汉模式就是一个不错的选择

小问题:不过,上述代码在多线程场景下存在问题!!直接看图:

12

当线程 1 刚刚进入if内部但还未实例化uniqueInstance,此时uniqueInstance仍为null,就在这个间隙,线程 2 以迅雷不及掩耳盗铃之势也进入了if内部

这样就会导致uniqueInstance被实例化了两次!!!

懒汉模式 + synchronized 同步锁

为了解决上面线程安全的问题,可以使用synchronized关键字,具体如下方代码:

缺点:同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能

双重校验锁

在「懒汉模式 + synchronized 同步锁」方式中,我们对方法getInstance()加了同步锁。由于getInstance()会被调用多次,导致性能损耗较大

「双重校验锁」就是在此问题上进行了改进,先看代码:

为什么需要两次判断?

为什么比「懒汉模式 + synchronized 同步锁」好?

小问题:不过,上述代码也存在一些问题!!

先介绍一下指令重排序优化:为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java 虚拟机的即时编译中也有指令重排序优化

再来介绍一下对象的创建,先看图:

13

铺垫了这么多,现在正式来介绍「双重校验锁」存在的问题!!

由于指令重排优化的存在,导致「初始化 Singleton」和「将对象地址赋给 uniqueInstance」的顺序是不确定的。其中,「初始化 Singleton」就是「执行构造函数」的阶段

如果按照后者的顺序,在「初始化为零值」后,就「将对象地址赋给 uniqueInstance」,此时uniqueInstance已经不为null,但是对象还都是零值,这会导致把还都是零值的对象返回给其他调用getInstance()的线程

更新:2022-10-05 23:52:10 (困扰了两天的一个疑惑)

参考文章 synchronized 的可见性理解

根据 JMM,每个线程的工作内存中的共享变量其实只是主内存中的一个副本,当线程处理完后再刷新到主内存中

那么问题来了,上述synchronized修饰的代码块会在释放锁时刷新到主内存,所以在代码块执行结束前,其他线程应该看不到只初始化一半的对象呀??!!

注意:更加准确的来说,是在「释放锁之前」就会刷新到主内存,现在 jvm 的机制,已经尽量快速的将改变同步到缓存了

可以写个 demo 测试一下:

下面是执行结果:

可以看到在还未退出同步块之前就可以读到更新后的值

 

不过还好,在 JDK1.5 及之后版本增加了 volatile 关键字

volatile 的一个语义是禁止指令重排序优化,也就保证了 uniqueInstance 被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题

下面给出 volatile 优化的代码:

静态内部类

类只会加载一次,且类变量只会在类加载的时候初始化一次

只要应用中不使用内部类,JVM 就不会去加载类UniqueInstanceHolder,也就不会创建单例对象uniqueInstance,从而实现懒汉式的延迟加载

枚举

首先,在枚举中明确了构造方法为私有,在访问枚举实例时会执行构造方法

同时每个枚举实例都是 static final 类型的,也就表明只能被实例化一次。在调用构造方法时,单例被实例化

因为 enum 中的实例被保证只会被实例化一次,所以我们的 uniqueInstance 也被保证实例化一次。

单例模式的线程安全性

只有懒汉模式是非线程安全