Java的线程开发之volatile关键字的又一些理解
提到java中的volatile,大家肯定会说,这个关键字啊,用它来修饰的变量对其他的线程是立即可见的。哈哈,不错不错,这个关键字的确具备这个功能,但是如果说基于volatile变量的运算在并发下是线程安全的,那么这句话就有待商榷了。
“从物理存储的角度看,各个线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题,但是java里面的运算操作符并非原子性,这导致volatile变量的运算在并发下一样是不安全的。”
package com.xh.lesson;
public class VolatileTest {
public static volatile int race = 0;
public static void increase(){
race++;
}
private static final int THREADS_COUNT = 20;
public static void main(String[] args) {
Thread[] threads = new Thread[THREADS_COUNT];
for (int i = 0; i < THREADS_COUNT; i++) {
threads[i] = new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <10000 ; j++) {
increase();
}
}
});
threads[i].start();
}
while (Thread.activeCount()>2) {
Thread.yield();
}
System.out.println(race);
}
}
上面的这段代码发起了20个线程,每个线程对race变量进行10000次自增操作,如果这段代码能够正常并发的话,最后输出的结果应该是200000.
看到这里,有些读者可能会有些疑问了,代码下面的那张大图的运行结果不就是200000吗,下面的几张小图都小于200000.
说实话,当我第二次运行代码时出现了这个结果也令我很吃惊(当时我就觉得我TM是不是就是传说中的天选之子!!),因为我之后有运行了几十次,发现每次都是小于200000的,我就又觉得我啥也不是了==。
好了,言归正传,下面我们来看一下使用Javap反编译这段代码后得到的代码清单
public static void increase();
Code:
Stack=2,Locals=0,Args_size=0
0: getstatic
3:iconst_1
4:iadd
5:putstatic
8:return
LineNumberTable:
line 14: 0
line 15: 8
从字节码层面上分析:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值改变了,而操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同步回主内存中。
这样分析下来,上面等于200000的情况是说明每个线程对race变量执行指令的时候,其他的线程都未改变race的值。由此看来,说我是天选之子也不为过嘛!
还有一点,使用字节码来分析并发问题仍然是不严谨的,因为即使编译出来只有一跳字节码指令,也并不意味执行这条指令就是毅哥原子操作。一条字节码指令在解释执行时,解释器都要运行许多行代码才能实现它的语义。如果是编译执行,一条字节码指令也可能转化成若干条本地机器码指令。此处使用-XX:+ PrintAssembly 参数输出反汇编来分析才会更加严谨一些。
由于volatile变量只能保证可见性,在不符合一下两条规则的运算场景中,我们仍然要通过加锁(使用synchronized、java.util.concurrent中的锁或原子类)来保证原子性:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2、变量不需要与其他的状态变量共同参与不变约束。
还是上面那个代码,在increase()方法上添加 synchronized 关键字即可保证原子性。
以上摘录至《深入理解Java虚拟机》,想要学习JVM的值得一读!
后续还会更新!