Double check 为什么须要 volatile?

对于单例的双重检锁,为什么要对变量加上 volatile 修饰关键字?缓存

Prerequisite:对象建立的过程

要理解一个对象的建立过程,须要从运行时数据区进行分析,首先须要对JVM运行时数据区布局有深刻的理解,同时掌握类加载过程当中各个阶段的行为。详见后续的《从JVM角度分析 new 一个对象的详细过程》。bash

本文无需深刻分析这两个主题,只须要了解建立对象的整体流程便可。多线程

核心:非原子操做、指令重排序函数

private static volatile Singleton instance;

public static Singleton getInstance() {
    if(instance == null) {
        synchronized(Singleton.class) {
            if(instance == null) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

复制代码

目的

避免重排序问题致使其余线程看到一个已经分配内存和地址,可是没有初始化的对象(对象还处于不可用状态),就被其余线程引用了(报异常)。布局

建立对象的指令重排序分析

下面代码在多线程环境下不是原子操做ui

instance = new Singleton();
复制代码

这条指令坑被重排序,分以下两种可能的指令排序场景。spa

场景1:不重排序(正常步骤)

正常的底层执行顺序会分3步走:线程

一、给 instance 分配内存

二、调用实例 instance 的构造函数来初始化成员变量

三、将 instance 这个在栈中的引用,指向在步骤1和2中打包好的对象
复制代码

不管线程A当前执行到一、二、3哪一步,对于线程B,可能看到的 instance 的状态只有两种:null 和 非 null。code

步骤 1 和 2 中的 instance 对象都是 null 的,第3步看到的是非 null,对于正常顺序来讲,这是没问题的。对象

场景2:重排序

若是线程A 在重排序的状况下,可能会变成 1,3,2,假如线程A执行到第二步“3”时,instance 虽然已经不是 null,但还没初始化,不可用。

此时CPU时间片切换,从线程A 切换到线程B,线程B来调用 double check 这个 getInstance 单例方法,那么第一个 null check 时,看到的 instance 引用因为已经被线程 A 指向了内存块,不为 null,则直接返回这个instance。

可是,当使用这个对象的某个字段时,因为还没被初始化,处于不可用状态,会致使异常发生。

解决方案:使用 volatile 修饰变量,禁止指令重排序

volatile 的原理

使用 volatile 修饰成员变量,那么在变量赋值时,会有一个内存屏障,也就是说只有执行完123步操做以后,其余线程读取操做时才能看到 instance 这个变量的值,不会形成误判,解决了对象状态不完整的问题。

同时,volatile 会强制将缓存中修改的数据刷新到主内存中,确保对其余线程的可见性。

此时,invalidate 其余CPU的缓存行,当其余CPU须要使用这个缓存行的变量时,就会去从新到主内存读取,保证数据是最新的。

相关文章
相关标签/搜索