单例模式是设计模式里面最常见的,也是在面试过程中面试官最容易考到的,通过单例模式还可以引申到其他的一些并发问题。今天我们来聊聊如何正确的构建一个单例。
首先我们来写一个最简单的单例,该单例只能使用在单线程的情况下。
1 | public class UnsafeLazyInitialization { |
我们来分析一下这个单例如果放到多线程模式下会有什么问题,如果线程 A 和 B 同时到达 if (resource == null)
的时候(或者是由于可见性的原因,线程 B 没有看到线程 A 已经对引用赋值了),此时判断逻辑都为 true
于是就会创建 Resource
对象,两个线程创建了两个对象(如果有多个线程,可能会创建更多的对象),不符合单例的要求。相对这个问题而言,这里还隐藏了更危险的问题,由于指令的重排序的原因,可能导致一些线程能够获取这个单例,但是单例对象还在初始化,内部处于一个不确定的状态。
饿汉单例(静态类模式)
下面我们改进一下这个单例,让其变为一个线程安全的单例。
1 | public class EagerInitialization { // 饿汉模式 |
上面的代码如何做到的线程安全,是由于利用了静态初始化器。
静态初始化器是由 JVM 再类的初始化阶段执行,即在类被加载后并且被线程使用之前。由于 JVM 将在初始化期间获得一个锁,并且每个线程都至少获取一次这个锁以确保这个累已经加载,因此在静态初始化期间,内存写入操作将自动对所有线程可见。
懒汉单例 (静态类模式)
上面的模式展示了如何使用 Java 本身类加载的机制来保证单例的,使用同样的方式也可以构建一种懒汉模式的单例。
1 | public class ResourceFactory { |
双重检测锁 Double Checked Locking
在江湖上还传说着一种既高效又线程安全的单例写法:
1 | // 不要这么做 |
上面的 DCL 真正的问题在于在没有同步的情况下读取一个共享对象。可见性问题会造成有些线程可能看到失效的值,这个值可能是一个 null
,更加糟糕的情况下,这个值可能是一个不确定的值,因为该对象还没有初始化完成就发布了。
上面的例子可以改写成以下代码,通过使用 volatile
来保证共享对象的可见性
1 | public class DoubleCheckedLocking { |
然而,DCL 的这种使用方法已经被广泛地废弃了——促使该模式出现的驱动力(无竞争同步的执行速度很慢,以及 JVM 启动很慢)已经不复存在了,因此它不是一种高效的优化措施。延迟初始化能带来同样的优势,并且更容易理解。
枚举类模式实现枚举
1 | public interface MySingleton { |
effective java 推荐使用枚举来做单例
使用枚举实现单例的方法虽然还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
使用枚举来做单例的优势是:写法简单,单例线程安全由 JMM 提供、反序列化得到的对象也能保证是同一个。
单例线程安全:如果反编译单例的代码之后,会发现是一些 static 的属性和代码块。
反序列化的保障:
Java的序列化机制针对枚举类型是特殊处理的。简单来讲,在序列化枚举类型时,只会存储枚举类的引用和枚举常量的名称。随后的反序列化的过程中,这些信息被用来在运行时环境中查找存在的枚举类型对象。
总结
推荐使用枚举去实现单例模式!