单例模式

几种实现方式

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

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才会实例化,保证了一个类仅有一个实例

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

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

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

11

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

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

懒汉模式 + synchronized 同步锁

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

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

双重校验锁

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

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

为什么需要两次判断?

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

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

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

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

11

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

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

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

------------------------ begin ------------------------

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

参考文章 synchronized 的可见性理解

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

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

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

可以写个 demo 测试一下:

下面是执行结果:

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

------------------------ end ------------------------

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

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

下面给出 volatile 优化的代码:

静态内部类

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

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

枚举

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

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

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

单例模式的线程安全性

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

面试现场

作为面试官,大多数情况下之所以问单例模式,是因为这个题目可以问到很多知识点

比如线程安全、类加载机制、synchronized 的原理、volatile 的原理、指令重排与内存屏障、枚举的实现、反射与单例模式、序列化如何破坏单例、CAS、CAS 的 ABA 问题、Threadlocal 等知识

一般情况下,只需要从单例开始问起,大概就可以完成一场面试的整个流程,把想问的东西都问完,可以比较全面的了解一个面试者的水平

以下,是一次面试现场的还原,从单例模式开始:

Q:你知道怎么不使用 synchronized 和 lock 实现一个线程安全的单例吗?

A:我知道,可以使用「静态内部类」实现

Q:除了静态内部类还会其他的方式吗?

A:还有就是饿汉模式

Q:那你上面提到的几种都是线程安全的吗?

A:是线程安全的

Q:那是如何做到线程安全的呢?

A:应该是因为我使用了 static,然后类加载的时候就线程安全的

Q:其实你说的并不完全对,因为以上几种虽然没有直接使用 synchronized,但是也是间接用到了

(OS:这里面根据回答情况会朝两个不同的方向展开:1、类加载机制、模块化等;2、继续深入问单例模式)

类加载过程的线程安全性保证

Q:那还回到刚开始的问题,你知道怎么不使用 synchronized 和 lock 实现一个线程安全的单例吗?

(OS:并不是故意穷追不舍,而是希望能可以引发面试者的更多思考)

A:额、、、那枚举吧,枚举也可以实现单例

Q:那你知道枚举单例的原理吗?如何保证线程安全的呢?

枚举单例的线程安全问题

Q:所以,枚举其实也是借助了 synchronized 的,那你知道哪种方式可以完全不使用 synchronized 的吗?

A:en....我想想

Q:(过了一会他好像没有思路) 你知道 CAS 吗?使用 CAS 可以实现单例吗?

(OS:面试中,如果面试者对于锁比较了解的话,那我大多数情况下都会继续朝两个方向深入问:1、锁的实现原理;2、非锁,如CAS、ThreadLocal等)

A:哦,我知道,CAS是一项乐观锁技术,当多个线程尝试使用CAS同时更新一个变量时,只有其中一个线程能更新成功

借助 CAS(AtomicReference)实现单例模式:

Q:使用 CAS 实现的单例有没有什么优缺点呀?

A:用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS 是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度

Q:你说的好像是优点?那缺点呢?

CAS 实现的单例的缺点

Q:好的,除了使用 CAS 以外,你还知道有什么办法可以不使用 synchronized 实现单例吗?

A:这回真的不太知道了

Q:(那我再提醒他一下吧) 可以考虑下 ThreadLocal,看看能不能实现?

(OS:面试者没有思路的时候,我几乎都会先做一下提醒,实在没有思路再换下一个问题)

A:ThreadLocal?这也可以吗?

Q:你先说下你理解的 ThreadLocal 是什么吧!

(OS:通过他的回答,貌似对这个思路有些疑惑,不着急。先问一个简单的问题,让面试者放松一下,找找自信,然后再继续问)

ThreadLoacal

Q:那理论上是不是可以使用 ThreadLocal 来实现单例呢?

A:应该也是可行的

Q:嗯嗯,好的,那有关单例模式的实现的问题我就问的差不多了。

(OS:ThreadLocal 这种写法主要是考察面试者对于 ThreadLocal 的理解,以及是否可以把知识活学活用,但是实际上,这种所谓的「单例」,其实失去了单例的意义...)

(OS:但是说实话,能回答到这一题的人很少,大多数面试者基本上在前面几道题就已经没有思路了,大多数情况下根本不会问到这个问题就要改方向了)

A:(心中窃喜)嗯嗯,学习到很多,感谢

Q:那...你知道如何破坏单例吗?

(OS:单例问题,必问的一个。通过这个引申到序列化和反射的相关知识)

A:(额....)