基本概念

volatile 的底层实现原理是内存屏障,Memory Barrier(Memory Fence)

  • volatile变量的写指令后会加入写屏障

  • volatile变量的读指令前会加入读屏障

保证可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        ready = true;
    }

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

    @Actor
    public void actor1(I_Result r) {
        // 读屏障
        // ready 是 volatile 读取值带读屏障
        // 读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据
        if(ready) {
            r.r1 = num + num;
        } else {
            r.r1 = 1;
        }
    }

image-20220803134001298

保证有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

    @Actor
    public void actor2(I_Result r) {
        num = 2;
        // 写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中
        ready = true;
    }

如上图: num = 2 不会被排在 ready=true 之后

volatile 不能解决指令交错:

  • 写屏障仅仅是保证之后的读能够读到最新的结果,但不能保证读跑到它前面去
  • 而有序性的保证也只是保证了本线程内相关代码不被重排序
  • 无法保证多个线程之间的指令执行的有序性

double check locking

基本介绍

double-checked locking 单例模式为例

代码案例

package cn.knightzz.pattern;

import lombok.extern.slf4j.Slf4j;

@Slf4j(topic = "Singleton")
public class Singleton {

    public Singleton() {
    }

    private static Singleton INSTANCE;

    public static Singleton getInstance(){

        if (INSTANCE != null) {
            //  首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class){
                if (INSTANCE != null) {
                    INSTANCE = new Singleton();
                }
            }
        }

        return INSTANCE;
    }
}

image-20220803214927038

以上的实现特点 :

  • 懒惰实例化

  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁

  • 有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外

问题分析

但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为

 0 getstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
 3 ifnull 37 (+34)
 6 ldc #3 <cn/knightzz/pattern/Singleton>
 8 dup //复制类对象的引用指针
 9 astore_0 // 临时存储类对象的引用指针

// 进入同步代码块
10 monitorenter
11 getstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
14 ifnull 27 (+13)
17 new #3 <cn/knightzz/pattern/Singleton>
20 dup 
21 invokespecial #4 <cn/knightzz/pattern/Singleton.<init> : ()V>
24 putstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
27 aload_0
28 monitorexit
// 离开同步代码块
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
40 areturn
  • 17 new #3 <cn/knightzz/pattern/Singleton> 表示创建对象,将对象引用入栈 // new Singleton
  • 20 dup 表示复制一份对象引用 // 引用地址
  • 21 invokespecial #4 <cn/knightzz/pattern/Singleton.<init> : ()V> 表示利用一个对象引用,调用构造方法
  • 24 putstatic #2 表示利用一个对象引用,赋值给 static INSTANCE

也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行

image-20220804074127193

创建对象核心步骤 :

  • 创建对象, 把对象的引用入栈 (此时还未调用构造函数初始化)
  • 调用构造函数初始化
  • 将引用对象赋值

可能会出现一个情况就是, 指令重排

  • 将引用对象赋值
  • 调用构造函数初始化

那么就会存在一个问题 , t1 线程赋值完以后, 线程上下文切换, 此时 INSTANCE 还未调用构造函数初始化,

t2 在获取INSTANCE时, 获取到的就是未实例化的对象

  1. 关键在于 0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取INSTANCE 变量的值

  2. 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例

  3. 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

解决方法

package cn.knightzz.pattern;

import lombok.extern.slf4j.Slf4j;

/**
 * @author 王天赐
 * @title: Singleton
 * @projectName hm-juc-codes
 * @description: 单例模式
 * @website <a href="http://knightzz.cn/">http://knightzz.cn/</a>
 * @github <a href="https://github.com/knightzz1998">https://github.com/knightzz1998</a>
 * @create: 2022-08-03 21:32
 */
@Slf4j(topic = "Singleton")
public class Singleton {

    public Singleton() {
    }

    private static volatile Singleton INSTANCE = null;

    public static Singleton getInstance(){

        if (INSTANCE != null) {
            synchronized(Singleton.class){
                if (INSTANCE != null) {
                    INSTANCE = new Singleton();
                }
            }
        }

        return INSTANCE;
    }
}

增加 volatile 关键字在读写 volatile 变量(指令级别)时会增加读写屏障, 保证以下两点 :

可见性 :

  • 写屏障保证, 在该写屏障之前的变量的改动都会同步到主存上
  • 读屏障保证, 在读屏障之后的线程对共享变量的读取都是

有序性 :

  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

  • synchronized 代码块中这些指令仍然是可以被重排序!

  • volatile 才可以阻止重排序

  • 如果共享变量在使用的过程中, 完全被 synchronized 管理, 共享变量是不会有有序性,原子性的问题的

  • 但是在上面的代码上 INSTANCE 没有被完全的被 synchronized 保护

 
// -------------------------------------> 加入对 INSTANCE 变量的读屏障
// 读取 INSTANCE
0 getstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
 3 ifnull 37 (+34)
 6 ldc #3 <cn/knightzz/pattern/Singleton>
 8 dup
 9 astore_0
10 monitorenter  // -----------------------> 保证原子性、可见性
11 getstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
14 ifnull 27 (+13)
17 new #3 <cn/knightzz/pattern/Singleton>
20 dup
21 invokespecial #4 <cn/knightzz/pattern/Singleton.<init> : ()V>
24 putstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
27 aload_0
28 monitorexit // ------------------------> 保证原子性、可见性
29 goto 37 (+8)
32 astore_1
33 aload_0
34 monitorexit
35 aload_1
36 athrow
37 getstatic #2 <cn/knightzz/pattern/Singleton.INSTANCE : Lcn/knightzz/pattern/Singleton;>
40 areturn

更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性

image-20220804165035640

image-20220804170402506