前言
本篇文章将从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=2
happens-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年全栈工作经验,欢迎关注个人公众号