话不多说, 直接贴代码
- class Singleton {
- private static volatile Singleton instance;
- private Singleton(){}
- // 双重判空
- public static Singleton getInstance() {
- if ( instance == null ) {
- synchronized (Singleton.class) {
- if ( instance == null ) {
- instance = new Singleton();
- }
- }
- }
- return instance;
- }
- }
这是一个大家耳熟能详的单例实现, 其中有两个关键要点, 一是使用双重检查锁定 (Double-Checked Locking) 来尽量延迟加锁时间, 以尽量降低同步开销; 二就是 instance 实例上加了 volatile 关键字. 那么为什么一定要加 volatile 关键字, volatile 又为我们做了什么事情呢?
要了解这个问题, 我们先要搞清楚三个概念: java 内存模型(JMM),happen-before 原则, 指令重排序.
1.java 内存模型(Java Memory Model)
Java 内存模型中规定了所有的变量都存储在主内存中, 每条线程还有自己的工作内存, 线程的工作内存中使用到的变量需要到主内存去拷贝, 线程对变量的所有操作 (读取, 赋值) 都必须在工作内存中进行, 而不能直接读写主内存中的变量. 不同线程之间无法直接访问对方工作内存中的变量, 线程间变量值的传递均需要在主内存来完成, 线程, 主内存和工作内存的交互关系如下图所示:
2.happen-before 原则
Java 语言中有一个 "先行发生"(happen-before)的规则, 它是 Java 内存模型中定义的两项操作之间的偏序关系, 如果操作 A 先行发生于操作 B, 其意思就是说, 在发生操作 B 之前, 操作 A 产生的影响都能被操作 B 观察到,"影响" 包括修改了内存中共享变量的值, 发送了消息, 调用了方法等, 它与时间上的先后发生基本没有太大关系. 这个原则特别重要, 它是判断数据是否存在竞争, 线程是否安全的主要依据.
下面是 Java 内存模型中的八条可保证 happen-before 的规则, 它们无需任何同步器协助就已经存在, 可以在编码中直接使用. 如果两个操作之间的关系不在此列, 并且无法从下列规则推导出来的话, 它们就没有顺序性保障, 虚拟机可以对它们进行随机地重排序.
单线程 happen-before 原则: 在同一个线程中, 书写在前面的操作 happen-before 后面的操作.
锁的 happen-before 原则: 同一个锁的 unlock 操作 happen-before 此锁的 lock 操作.
volatile 的 happen-before 原则: 对一个 volatile 变量的写操作 happen-before 对此变量的任意操作(当然也包括写操作了).
happen-before 的传递性原则: 如果 A 操作 happen-before B 操作, B 操作 happen-before C 操作, 那么 A 操作 happen-before C 操作.
线程启动的 happen-before 原则: 同一个线程的 start 方法 happen-before 此线程的其它方法.
线程中断的 happen-before 原则: 对线程 interrupt 方法的调用 happen-before 被中断线程的检测到中断发送的代码.
线程终结的 happen-before 原则: 线程中的所有操作都 happen-before 线程的终止检测.
对象创建的 happen-before 原则: 一个对象的初始化完成先于他的 finalize 方法调用.
3. 指令重排序
对主存的一次访问一般花费硬件的数百次时钟周期. 处理器通过缓存 (caching) 能够从数量级上降低内存延迟的成本, 这些缓存为了性能重新排列待定内存操作的顺序. 也就是说, 程序的读写操作不一定会按照它要求处理器的顺序执行.
JMM 通过 happens-before 法则保证顺序执行语义, 如果想要让执行操作 B 的线程观察到执行操作 A 的线程的结果, 那么 A 和 B 就必须满足 happens-before 原则, 否则, JVM 可以对它们进行任意排序以提高程序性能.
基于以上三个概念, 我们可以拆解 instance = newSingleton() 这段代码:
- // thread-A
- memory = allocate(); // 1: 分配对象的内存空间
- ctorInstance(memory); // 2: 初始化对象
- instance = memory; // 3: 设置 instance 指向刚分配的内存地址
然而, 由于 happen-before 原则并不能保证这段代码的顺序性, 这段代码可能被编译器优化为:
- //thread-B
- memory = allocate(); // 1: 分配对象的内存空间
- instance = memory; // 3: 设置 instance 指向刚分配的内存地址
- ctorInstance(memory); // 2: 初始化对象
在单线程中不论是以哪种顺序执行, 都不会对结果有任何影响, 然而在多线程下, 有可能出现 thread-B 的执行顺序, 尽管由于同步锁的存在, 不会出现两个线程同时进入 instance = newSingleton()的场景, 但是若 B 线程执行完 3 之后, 2 还没有执行, CPU 就切换时间片, 执行一个全新的 C 线程, 将导致 C 线程拿到一个非空的 instance, 然而这时候该 instance 还没有准备好.
而这一切, 仅仅需要在 instance 实例前加上 volatile, 就可以完美的解决.
那么, volatile 在例子中到底做了什么神奇的操作呢?
其一, 对于 volatile 修饰的 instance 变量, 若对 instance 的写操作执行在前, 那么该写操作的结果一定会被立刻刷新到主内存中, 之后所有线程对于该 instance 的所有读写操作必然可以观察到最新的值, 也即: volatile 保证了变量的内存可见性
其二, 对于 volatile 修饰的 instance 变量, 将不允许任何与其相关的操作进行指令重排序
来源: https://www.cnblogs.com/Jasonchan1994/p/10696930.html