• 内存模型之有序性


    1 引入

    在代码中,JVM 会在不影响正确性的前提下,可以调整语句的执行顺序.

    如:

    static int i;
    static int j;
    // 在某个线程内执行如下赋值操作
    i = ...; 
    j = ...; 
    
    • 1
    • 2
    • 3
    • 4
    • 5

    先执行 i 还是 先执行 j ,对最终的结果不会产生影响所以执行方式可能为

    i = ...; 
    j = ...;
    
    • 1
    • 2

    也可能为

    j = ...;
    i = ...; 
    
    • 1
    • 2

    即称为,指令重排

    2 案例

    int num = 0;
    boolean ready = false;
    // 线程1 执行此方法
    public void actor1(I_Result r) {
     if(ready) {
     r.r1 = num + num;
     } else {
     r.r1 = 1;
     }
    }
    // 线程2 执行此方法
    public void actor2(I_Result r) { 
     num = 2;
     ready = true; 
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    分析:

    • 情况1 线程1 先执行,这时 ready = false,所以进入 else 分支结果为 1
    • 情况2 线程2 先执行 num = 2,但没来得及执行 ready = true,线程1 执行,还是进入 else 分支,结果为1
    • 情况3 线程2 执行到 ready = true,线程1 执行,这回进入 if 分支,结果为 4(因为 num 已经执行过了)
    • 情况4 线程2 执行 ready = true,切换到线程1,进入 if 分支,相加为 0,再切回线程2 执行 num = 2

    指令重排,是 JIT 编译器在运行时的一些优化,这个现象需要通过大量测试才能复现

    可借助Java并发压测工具jcstress工具

    解决办法

    volatile 修饰的变量,可以禁用指令重排.

    3 volatile原理

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

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

    1 保证可见性

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

    public void actor2(I_Result r) {
     num = 2;
     ready = true; // ready 是 volatile 赋值带写屏障
     // 写屏障
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5

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

    public void actor1(I_Result r) {
     // 读屏障
     // ready 是 volatile 读取值带读屏障
     if(ready) {
     r.r1 = num + num;
     } else {
     r.r1 = 1;
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    2 保证有序性

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

    public void actor2(I_Result r) {
     num = 2;
     // 写屏障
     // ready 是 volatile 赋值带写屏障
     ready = true; 
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7

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

    public void actor1(I_Result r) {
     // 读屏障
     // ready 是 volatile 读取值带读屏障
     if(ready) {
     r.r1 = num + num;
     } else {
     r.r1 = 1;
     }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    在这里插入图片描述

    但是不能解决指令交错问题.

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

    3 dcl问题

    即double-checked locking 单例模式

    public final class Singleton {
         private Singleton() { 
             
         }
        
         private static Singleton INSTANCE = null;
        
         public static Singleton getInstance() { 
                 if(INSTANCE == null) { // t2
                     // 首次访问会同步,而之后的使用没有 synchronized
                     synchronized(Singleton.class) {
                         if (INSTANCE == null) { // t1
                         INSTANCE = new Singleton();
                         } 
                     }
                 }
             return INSTANCE;
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19

    说明:

    • 1 懒惰实例化
    • 2 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
    • 3 第一个 if 使用了 INSTANCE 变量,是在同步块之外

    在多线程环境下,代码可能存在问题. 查看对应字节码:

    0: getstatic #2 // Field INSTANCE:Lcn/cf/n5/Singleton;
    3: ifnonnull 37
    6: ldc #3 // class cn/cf/n5/Singleton
    8: dup
    9: astore_0
    10: monitorenter
    11: getstatic #2 // Field INSTANCE:Lcn/cf/n5/Singleton;
    14: ifnonnull 27
    17: new #3 // class cn/cf/n5/Singleton
    20: dup
    21: invokespecial #4 // Method "":()V
    24: putstatic #2 // Field INSTANCE:Lcn/cf/n5/Singleton;
    27: aload_0
    28: monitorexit
    29: goto 37
    32: astore_1
    33: aload_0
    34: monitorexit
    35: aload_1
    36: athrow
    37: getstatic #2 // Field INSTANCE:Lcn/cf/n5/Singleton;
    40: areturn
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22

    说明:

    • 17 表示创建对象,将对象引用入栈 // new Singleton
    • 20 表示复制一份对象引用 // 引用地址
    • 21 表示利用一个对象引用,调用构造方法
    • 24 表示利用一个对象引用,赋值给 static INSTANCE

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

    在这里插入图片描述

    在0: getstatic 这行代码在 monitor 控制之外,它就像之前举例中不守规则的人,可以越过 monitor 读取 INSTANCE 变量的值, 此时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初 始化完毕的单例.(存在问题)

    4 dcl问题解决

    使用 volatile 修饰即可,可以禁用指令重排

    public final class Singleton {
        
         private Singleton() { }
         private static volatile Singleton INSTANCE = null;
        
         public static Singleton getInstance() {
             // 实例没创建,才会进入内部的 synchronized代码块
             if (INSTANCE == null) { 
                 synchronized (Singleton.class) { // t2
                     // 也许有其它线程已经创建实例,所以再判断一次
                     if (INSTANCE == null) { // t1
                     INSTANCE = new Singleton();
                     }
                 }
             }
             return INSTANCE;
         }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面 两点:

    • 可见性
      • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
      • 读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
    • 有序性
      • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
      • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

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

    在这里插入图片描述

  • 相关阅读:
    SpringBoot(三) - Slf4j+logback 日志,异步请求,定时任务
    机器学习笔记之最优化理论与方法(十)无约束优化问题——共轭梯度法背景介绍
    三年之约!亚马逊联手哈佛大学倾力打造量子互联网
    angular学习笔记
    电子邮件解决方案有哪些?邮件系统的问题?
    vue2升级vue3指南(一)—— 环境准备和构建篇
    呼叫系统使用webRTC网页软电话到底好不好?
    WEB APIs day6
    面试官:你确定Redis是单线程的进程吗?
    Spring Boot最核心的25个注解
  • 原文地址:https://blog.csdn.net/ABestRookie/article/details/126114232