java volatile 看这一篇就够了

深入解析volatile
本文从Java内存模型和字节码角度解读volatile关键字,分析其如何保证内存可见性和防止指令重排,探讨多线程环境下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可见性衍生出的副作用

常见问题

  • 哪些数据可以共享?
    • 静态变量、实例数据、数组元素。只要数据存在共享就会存在内存可见性问题。
  • 哪些变量不存在共享?
    • 局部变量、方法参数是线程私有,所以不存在数据共享问题
  • 怎样理解指令重排与程序次序规则?
    • 如果认真思考一下,很容易想到,int i=1; int j=1; 指令重排序与int i=1 happens-beforeint j=1冲突吧?其实是不冲突的。happens-before 并不意味着代码的执行顺序,本例中int i=1 并一定在int j=1 之前执行,只要保证最终执行结果与happens-before的执行结果一致即可,以下引用java语言规范,在Stack Overflow也有类似提问。

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年全栈工作经验,欢迎关注个人公众号
在这里插入图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值