java volatile关键字内存原理

内存屏障(Memory Barrier)

1.可见性

  • 写屏障(Sfence)保证该屏障之前的,对共享变量改动都同步到主内存中去
  • 读屏障(Ifence)保证该屏障之后的,对共享变量读取加载的为主内存中最新数据

2.有序性

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

volatile原理

volatile底层原理基于内存屏障

  • 对volatile变量写指令会在之后加入写屏障
  • 对volatile变量读指令会在之前加入读屏障
如何保证可见性?
  • 写屏障(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;
 }
}
T1线程 num volatile ready=false T2 num=2 写屏障ready = true num=2 读屏障ready=true T1线程 num volatile ready=false T2

如何保证有序性?
  • 写屏障在指令重排序时,确保不会将写屏障之前代码排到写屏障之后
public void actor2(I_Result r) {
 num = 2;
 ready = true; // ready 是 volatile 赋值带写屏障
 // 写屏障
}
  • 写屏障在指令重排序时,确保不会将读屏障之后代码排到读屏障之前
public void actor1(I_Result r) {
 // 读屏障
 // ready 是 volatile 读取值带读屏障
 if(ready) {
 r.r1 = num + num;
 } else {
 r.r1 = 1;
 }
}

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


double-checking lock问题

  • 方式一
public class Singleton {
    // 私有化构造
    private void Singleton(){}
    private static Singleton singleton = null;
    //在静态方法上添加synchronized锁住的事类对象
    public static synchronized Singleton getInstance(){
       if(singleton == null){
           singleton = new Singleton();
       }
       return singleton;
    }
    //与getInstance相同
    public static  Singleton getInstance1(){
        synchronized(Singleton.class){
            if(singleton == null){
                singleton = new Singleton();
            }
        }
        return singleton;
    }
}

上述代码本质上是没有问题,但是在性能方面不太优化。因为多线程情况下,每次调用getInstance方法,都要进行判断并且都要获取锁。


  • 方式二
public class Singleton {
    // 私有化构造
    private void Singleton(){}
    private static Singleton singleton = null;
 
    //解决了如果已经存在实例对象无须进行加解锁操作,提高性
    public static  Singleton getInstance(){
        if(singleton == null){
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

特点:

  • 懒惰实例化
  • 首次使用 getInstance() 才使用 synchronized 加锁,后续使用时无需加锁
    有隐含的,但很关键的一点:第一个 if 使用了 INSTANCE 变量,是在同步块之外
    但在多线程环境下,上面的代码是有问题的,getInstance 方法对应的字节码为:
0: getstatic #2 // 获取静态变量Instance Field INSTANCE:com/single/Singleton; 
3: ifnonnull 37 // 判断不是null 跳转到37行
6: ldc #3 // 获取类对象锁 class com/single/Singleton
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field INSTANCE:com/single/Singleton;
14: ifnonnull 27
17: new #3 //创建实例 class com/single/Singleton
20: dup    //复制引用
21: invokespecial #4 // 复制的引用调用构造方法Method "<init>":()V
24: putstatic #2 // 赋值 Field INSTANCE:com/single/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:com/single/Singleton;
40: areturn

其中

  • 17 表示创建对象,将对象引用入栈 // new Singleton
  • 20 表示复制一份对象引用 // 引用地址
  • 21 表示利用一个对象引用,调用构造方法
  • 24 表示利用一个对象引用,赋值给 static INSTANCE
    也许 jvm 会优化为:先执行 24,再执行 21。如果两个线程 t1,t2 按如下时间序列执行:
T1 T2 INSTANCE 17:new Singleton 20:dup 21:putstatic 对象引用赋值 0:获取静态变量Instance 3: ifnonnull 37 // 判断不是null 跳转到37行 37: getstatic 获取变量 40: areturn 使用对象 21:nvokespecial T1 T2 INSTANCE
  • 关键在于 0: getstatic 这行代码在 monitor 控制之外,一些线程可以越过 monitor 读取
    INSTANCE 变量的值
  • 这时 t1 还未完全将构造方法执行完毕,如果在构造方法中要执行很多初始化操作,那么 t2 拿到的是将是一个未初始化完毕的单例
  • 对 INSTANCE 使用 volatile 修饰即可,可以禁用指令重排,但要注意在 JDK 5 以上的版本的 volatile 才会真正有效

double-checking lock解决

public class Singleton {
    // 私有化构造
    private void Singleton(){}
    private static volatile Singleton singleton = null;
 
    //解决了如果已经存在实例对象无须进行加解锁操作,提高性
    public static  Singleton getInstance(){
        if(singleton == null){
            // 首次访问会同步,而之后的使用没有 synchronized
            synchronized(Singleton.class){
                if(singleton == null){
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

字节码上看不出volatile执行

// -------------------------------------> 加入对 INSTANCE 变量的读屏障
0: getstatic #2 // Field INSTANCE:com/single/Singleton;
3: ifnonnull 37
6: ldc #3 // class com/single/Singleton
8: dup
9: astore_0
10: monitorenter -----------------------> 保证原子性、可见性
11: getstatic #2 // Field INSTANCE:com/single/Singleton;
14: ifnonnull 27
17: new #3 // class com/single/Singleton
20: dup
21: invokespecial #4 // Method "<init>":()V
24: putstatic #2 // Field INSTANCE:com/single/Singleton;
// -------------------------------------> 加入对 INSTANCE 变量的写屏障
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:com/single/Singleton;
40: areturn

如上面的注释内容所示,读写 volatile 变量时会加入内存屏障(Memory Barrier(Memory Fence)),保证下面
两点:

可见性
  • 写屏障(sfence)保证在该屏障之前的 t1 对共享变量的改动,都同步到主存当中
  • 而读屏障(lfence)保证在该屏障之后 t2 对共享变量的读取,加载的是主存中最新数据
有序性
  • 写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后
  • 读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前
  • 更底层是读写变量时使用 lock 指令来多核 CPU 之间的可见性与有序性
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值