顾名思义,单例模式就是一个类有且只有一个实例,下面给出单例模式的定义:
The Singleton Pattern ensures a class has only one instance, and provides a global point of access to it.
解释:保证一个类仅有一个实例,并提供一个访问的它的全局访问点
如何才能保证一个类只会被实例化一次呢??
形象化记忆:饿了,就要马上吃,所以得先初始化对象准备好,用时直接取即可
public class Singleton {
// 类加载的时候就会初始化
private static final Singleton uniqueInstance = new Singleton();
// 私有构造函数
private Singleton() {}
// 全局访问点
public Singleton getInstance() {
return uniqueInstance;
}
}
优点:只在类加载的时候初始化一次实例,不存在多线程创建实例的情况,避免了多线程同步问题 (懒汉模式中会提到同步问题)
缺点:如果该单例没有被使用也会被创建,造成了内存的浪费
适用场景:单例占用内存较小,在初始化时就会被用到的情况
形象化记忆:懒了,用到了才会初始化返回
xxxxxxxxxx
public class Singleton {
private static Singleton uniqueInstance = null;
// 私有构造函数
private Singleton() {}
// 全局访问点
public static Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
上述的代码定义了一个私有的构造函数,这使得只有在类Singleton
中才能实例化对象,对于其他类来说,无法实例化Singleton
对象
同时,提供了全局访问点getInstance()
来获得Singleton
实例。只有当uniqueInstance = null
才会实例化,保证了一个类仅有一个实例
优点:只有在需要的时候才去创建,如果单例已经创建,再次调用获取接口将不会重新创建新的对象,而是直接返回之前创建的对象
适用场景:如果某个单例使用的次数少,并且创建单例消耗的资源较多,那么就需要实现单例的按需创建,这个时候懒汉模式就是一个不错的选择
小问题:不过,上述代码在多线程场景下存在问题!!直接看图:
当线程 1 刚刚进入if
内部但还未实例化uniqueInstance
,此时uniqueInstance
仍为null
,就在这个间隙,线程 2 以迅雷不及掩耳盗铃之势也进入了if
内部
这样就会导致uniqueInstance
被实例化了两次!!!
为了解决上面线程安全的问题,可以使用synchronized
关键字,具体如下方代码:
xxxxxxxxxx
public class Singleton {
private static Singleton uniqueInstance = null;
// 私有构造函数
private Singleton() {}
// 在全局访问点的方法上加了 synchronized 关键字
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
缺点:同步锁会增加锁竞争,带来系统性能开销,从而导致系统性能下降,因此这种方式也会降低单例模式的性能
在「懒汉模式 + synchronized 同步锁」方式中,我们对方法getInstance()
加了同步锁。由于getInstance()
会被调用多次,导致性能损耗较大
「双重校验锁」就是在此问题上进行了改进,先看代码:
x
public class Singleton {
private static Singleton uniqueInstance = null;
private Singleton() {}
public static Singleton getInstance() {
// 第一次判断,当 uniqueInstance 为 null 时,则实例化对象,否则直接返回对象
if (uniqueInstance == null) {
// 同步锁
synchronized (Singleton.class) {
// 第二次判断,当 uniqueInstance 为 null 时,则实例化对象,否则直接返回对象
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
为什么需要两次判断?
uniqueInstance
为什么比「懒汉模式 + synchronized 同步锁」好?
getInstance()
上,而getInstance()
会被调用多次;双重校验锁的同步锁在代码块上,大多数情况下,调用getInstance()
都不会执行到同步代码块,从而提高了性能小问题:不过,上述代码也存在一些问题!!
先介绍一下指令重排序优化:为了使处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致,因此如果存在一个计算任务依赖另外一个计算任务的中间结果,那么其顺序并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java 虚拟机的即时编译中也有指令重排序优化!
再来介绍一下对象的创建,先看图:
铺垫了这么多,现在正式来介绍「双重校验锁」存在的问题!!
由于指令重排优化的存在,导致「初始化 Singleton」和「将对象地址赋给 uniqueInstance」的顺序是不确定的。其中,「初始化 Singleton」就是「执行构造函数」的阶段
如果按照后者的顺序,在「初始化为零值」后,就「将对象地址赋给 uniqueInstance」,此时uniqueInstance
已经不为null
,但是对象还都是零值,这会导致把还都是零值的对象返回给其他调用getInstance()
的线程
更新:2022-10-05 23:52:10 (困扰了两天的一个疑惑)
参考文章 synchronized 的可见性理解
根据 JMM,每个线程的工作内存中的共享变量其实只是主内存中的一个副本,当线程处理完后再刷新到主内存中
那么问题来了,上述synchronized
修饰的代码块会在释放锁时刷新到主内存,所以在代码块执行结束前,其他线程应该看不到只初始化一半的对象呀??!!
注意:更加准确的来说,是在「释放锁之前」就会刷新到主内存,现在 jvm 的机制,已经尽量快速的将改变同步到缓存了
可以写个 demo 测试一下:
x
public class SynTest {
// 共享变量
private int a = 0;
public void write() {
new Thread(() -> {
synchronized (this) {
System.out.println("进入同步块");
// 修改共享变量
a = 1;
try {
// 3 秒后退出同步块
Thread.sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println("退出同步块");
}
}).start();
}
public void read() throws InterruptedException {
new Thread(() -> {
System.out.println("读线程开始");
// 读 5 次,每次休眠 1s
for (int i = 0; i < 5; i++) {
System.out.println("read: a = " + a);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
System.out.println("读线程结束");
}).start();
}
public static void main(String[] args) throws InterruptedException {
SynTest syn = new SynTest();
syn.read();
syn.write();
}
}
下面是执行结果:
x
读线程开始
read: a = 0
进入同步块
read: a = 1
read: a = 1
退出同步块
read: a = 1
read: a = 1
读线程结束
可以看到在还未退出同步块之前就可以读到更新后的值
不过还好,在 JDK1.5 及之后版本增加了 volatile 关键字
volatile 的一个语义是禁止指令重排序优化,也就保证了 uniqueInstance 被赋值的时候对象已经是初始化过的,从而避免了上面说到的问题
下面给出 volatile 优化的代码:
xxxxxxxxxx
public class Singleton {
// volatile 修饰 uniqueInstance,禁止指令重排序优化
private volatile static Singleton uniqueInstance = null;
private Singleton() {}
public static Singleton getInstance() {
// 第一次判断,当 uniqueInstance 为 null 时,则实例化对象,否则直接返回对象
if (uniqueInstance == null) {
// 同步锁
synchronized (Singleton.class) {
// 第二次判断,当 uniqueInstance 为 null 时,则实例化对象,否则直接返回对象
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}
}
类只会加载一次,且类变量只会在类加载的时候初始化一次
只要应用中不使用内部类,JVM 就不会去加载类UniqueInstanceHolder
,也就不会创建单例对象uniqueInstance
,从而实现懒汉式的延迟加载
xxxxxxxxxx
public class Singleton {
private Singleton() {}
private static class UniqueInstanceHolder {
private static final Singleton uniqueInstance = new Singleton();
}
public static Singleton getInstance() {
return UniqueInstanceHolder.uniqueInstance;
}
}
首先,在枚举中明确了构造方法为私有,在访问枚举实例时会执行构造方法
同时每个枚举实例都是 static final 类型的,也就表明只能被实例化一次。在调用构造方法时,单例被实例化
因为 enum 中的实例被保证只会被实例化一次,所以我们的 uniqueInstance 也被保证实例化一次。
xxxxxxxxxx
public class Resource {
private Resource() {}
private enum Singleton {
INSTANCE;
private final Resource uniqueInstance;
Singleton() {
uniqueInstance = new Resource();
}
private Resource getInstance() {
return uniqueInstance;
}
}
public static Resource getInstance() {
return Singleton.INSTANCE.getInstance();
}
}
只有懒汉模式是非线程安全