内存模型之有序性

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第7天,点击查看活动详情

1 引入

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

如:

static int i;
static int j;
// 在某个线程内执行如下赋值操作
i = ...; 
j = ...; 

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

i = ...; 
j = ...;

也可能为

j = ...;
i = ...; 

即称为,指令重排

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 线程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 赋值带写屏障
 // 写屏障
}

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

public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

image.png

2 保证有序性

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

public void actor2(I_Result r) {
 num = 2;
 // 写屏障
 // ready 是 volatile 赋值带写屏障
 ready = true; 

}

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

public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

image.png

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

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

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 首次使用 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 "<init>":()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

说明:

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

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

image.png

在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;
     }
}

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

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

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

image.png

猜你喜欢

转载自juejin.im/post/7127058268819554334
今日推荐