DCL单例要不要加volatile

3/2/2021 设计模式单例volatile

DCL单例要不要加volatile关键字呢?要!

# volatile和synchronized

volatile特性: 可见性 和 有序性

volatile保证可见性的原理: 凡是被volatile修饰的变量,等价于告诉JVM这个变量是不稳定的,每次使用的时候,都会从主内存读取到工作内存;每次修改的时候,都会刷新到主内存,换句话说,被volatile修饰的变量的修改,是实时反馈到主内存的。

volatile保证有序性的原理: 防止指令重排,这个不多废话。

volatile不能保证原子性,原子性 很明显是 把 非原子操作变为原子操作的,也就是把多个操作合并为一个操作的,volatile只能修饰变量,不能修饰方法,不能修饰代码块,怎么会需要合并操作呢,所以volatile肯定是不保证原子性的,它跟原子性一点关系都没有,原子性是函数或代码块的, 这应该找能修饰他们的东西,也就是synchronized。

synchronized特性: 可见性、有序性 和 原子性

synchronized保证可见性的原理: synchronized获取锁的时候,会清空当前工作内存,然后从主存拷贝变量到工作内存,释放锁的时候,会将工作内存写回到主存,说白了就是:

加锁 -> 清空工作内存 -> 从主存拷贝到工作内存 -> 执行逻辑 -> 写回主存 -> 释放锁
1

也就是说,synchronized的可见性发生在加锁和解锁的时刻,不是实时的

synchronized有序性的原理: 将并行变为串行,说白了就是将访问锁的线程进行排队,使得同一时刻只有一个线程进入竞争区。

synchronized保证原子性的原理: 还是排队,因为排队,使得同一时刻只有一个线程执行,也就是只有正在执行的线程执行完毕,其他线程才能执行,所以操作不会被其他线程插入,也就保证了原子性。

# DCL单例

我们来看一个经典的DCL单例

public class SingleInstance {
    private static (volatile) SingleInstance instance;
    private SingleInstance(){

    }

    public static SingleInstane getInstance(){
        if(instance == null) { // 1 此处不在同步代码块内部,所以不一定会从主存中获取
            synchronized(SingleInstane.class) {
                if(instance == null) { //2 此时会从主存读取
                    instance = new SingleInstance(); //3 这里同步块还没出去,所以还没刷入内存
                }
            } //4 synchronized块结束,刷入内存
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

我们直接看注释,比如1的地方,因为还没进入synchronized块内,所以1处的instance不一定从主存获取。

  • 案例1: 假如有两个线程同时调用getInstance(),Thread1已经创建了instance了,但是Thread2跑到1处的时候,没有从主存获取,那么他看到的instance就是null,就会阻塞排队,直到Thread1释放锁, 此时Thread2继续向下执行,跑到了2处,才从主存读取,发现已经有值了,于是直接返回,可以看到,Thread2的阻塞是徒劳的,自己干等了一会毛都没干,白忙活!
  • 案例2: 同上,假如Thread2在1处从主存读取了数据,而Thread1的已经执行过了3,正在结束synchronized块,但是没有写入内存(因为synchronized块还没结束),那么Thread2又读了个null,于是乎,再次阻塞,被唤醒,忙活一圈最终拿着Thread1创建的对象回去了。

此时有人会问,3处执行完后,synchronized块应该已经出去了,怎么会还没出去呢?因为我们看到的是一行代码:

instance = new SingleInstance();
1

但是,编译为JVM指令后,是好几行代码,大概就是:

创建对象
初始化对象
将对象赋值给instance引用
1
2
3

而且,这几个指令的顺序是不确定的(因为指令重排),所以3执行完,instance不一定立即有值!这一切都是因为synchronized的有序性不是实时的。

假如加上volatile呢,那么在1处会立刻从主存读取数据,在3处会立刻将数据写回主存,就有效避免了案例中的两次阻塞,所以加了volatile后,使得代码更加高效,不加也没事,但是不完美,所以,加上才是王道。

Last Updated: 1/29/2022, 2:35:56 PM