java volatile 看这一篇就够了 - Go语言中文社区

java volatile 看这一篇就够了


前言

本篇文章将从java内存模型、字节码角度解读volatile,因为jvm屏蔽了系统、硬件的差异,所以从这个角度出发更直观、更易理解;网上不乏从多核cpu多级缓存或cpu lock指令去解读volatile的,私以为这种解读方式有问题,比如单核cpu存在内存可见性问题吗?似乎没有答案。再者,volatile为什么会防止指令重排?仅仅是因为lock指令吗,要知道lock是结果,原因是volatile的可见性及happens-before原则。在介绍volatile之前必须了解java内存模型。

java内存模型

在这里插入图片描述

java内存分为主内存线程本地内存(又称为缓存);主内存也称为堆内存,存储静态变量、实例数据、数组元素;在程序执行时,线程首先从主存copy变量到本地内存,修改完后,再将变量同步到主存。所以在多个线程共享同一块主内存时,就存在使用过期数据问题。比如

public class TestVolatile {
    static boolean shutdown = false;
    //t1线程
    static void m1(){
        while(!shutdown){
        }
        System.out.println("shutdown...");
    }

    //t2线程
    static void m2(){
        shutdown = true;
        System.out.println("shutdown 更改了");
    }
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            TestVolatile.m1();
        },"t1").start();
        //为了让t1充分的运行
        Thread.sleep(1000);
        //t2修改后,t1看不到volatile修改后的值
        new Thread(()->{
            TestVolatile.m2();
        },"t2").start();

    }
}

有两个线程t1、t2,共享了静态变量shutdown,它们分别从主内存copy了shutdown到本地内存,t1将shutdown更改为true,同步到主存,t2看不到shutdown已经改了,仍然认为shutdown为false,所以t1线程永远不会输出shutdown;注:下图中是t1修改了本地内存中的shutdown值,还未同步到主内存;
在这里插入图片描述
我们不禁会想,如果有办法,在变量修改后,其他线程能立刻看到修改后的值,那就好了!很幸运,volatile就是干这个事的!

volatile内存可见性

volatile[ˈvɒlətaɪl] 英文释义易变的,volatile修饰的变量,修改后,对其它线程可见。那么为什么volatile修饰的变量就对其它线程可见呢?将本例中的shutdown用volatile修饰,反编译代码javap -v TestVolatile 可看到shutdown多了一个acc_volatile描述符。

 static volatile boolean shutdown;
   descriptor: Z
   flags: ACC_STATIC, ACC_VOLATILE

查阅jvm字节码指令可知,acc_volatile不允许变量缓存,这就是原因了!

ACC_VOLATILE		Declared volatile; cannot be cached.

基于字节码的解释,t1直接操作主存中的shutdown,而非本地内存,而t2在使用shutdown时,也从主存取值而不再从本地内存,所以shutdown修改对t2可见。volatile的内存可见性明白了,指令重排又是怎么回事呢?

volatile防止指令重排

相信读者也发现了,很多介绍volatile的博文中,只提到volatile有防止指令重排作用,但究竟为什么volatile能防止指令重排,却语焉不详
我们先说一下什么是指令重排,比如以下代码经过编译器或者运行时都可能发生指令重排

//t1线程
int i = 1;
int j = 2;

变为

//t1线程
int j = 2;
int i = 1;

这是被允许的,因为i与j没有依赖性,所以指令重排不会影响最终结果正确性。这在单线程环境下是没问题的,但在多线程环境下会存在问题。比如

//t2线程
1、if(j==2){
2、 //认为此时的i肯定等于1,从而进行一些操作,其实此处的i可能等于初始值0(假设i是成员变量)
   }

那有什么办法可以防止指令重排?volatile!本例中,用volatile修改j

int i = 1;
volatile int j = 2;

那么为什么volatile能防止指令重排呢?这就要说说java内存模型中的happens-before原则了,happens-before规定了代码中的执行顺序和内存可见性,happens-before原则有很多条,其中有一条叫程序次序规则(program order rule),说的是在单线程里,书写在前面的代码happens-before书写在后边的代码。比如,actionA happens-before actionB,那么actionA对actionB是可见的。基于此,t1里的volatile j=2happens-before t2里的if(j===2) ,又t1线程里i=1 happens-before volatile j=2,所以 i=1 happens-before if(j===2),所以i==1 对t2中的第2行代码是可见的。正因为如此,就保证了i==1不能重排到j==2后边,所以防止了指令重排。从上可以看出指令重排是volatile内存可见性的副作用。同理volatile后边的语句,也不能指令重排到前边。
这个地方要好好理解,最初t1中的i、j没有依赖关系,所以可指令重排;但当t1中的j与t2中的j,产生了依赖后,导致了t1中的i与j也产生了依赖关系,所以i、j不能指令重排。

总结

volatile的作用

  • 保证内存可见性,变量不能缓存,可以认为线程直接修改主存中的变量、直接从主存中读取变量;
  • 防止指令重排序,这是由happens-before中的单线程内有序及volatile可见性衍生出的副作用

常见问题

It should be noted that the presence of a happens-before relationship between two actions does not necessarily imply that they have to take place in that order in an implementation. If the reordering produces results consistent with a legal execution, it is not illegal.

参考

When does java thread cache refresh happens?
loop doesnt see value changed by other thread without a printstatement
how to decompile volatile variable in java
instruction reordering happens before relationship in java

9年全栈工作经验,欢迎关注个人公众号
在这里插入图片描述

版权声明:本文来源CSDN,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/wangjun5159/article/details/52118767
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-06-28 02:08:35
  • 阅读 ( 1123 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢