volatile是java虚拟机提供的轻量级同步规则。它具备两种特性
1. 保证被volatile修饰的变脸对所有线程的可见性。
可见性是指当一条线程修改了这个变量的值的时候,新值对于其他线程来说是可以立即得知的。普通变量做不到这一点。
但需要注意的是,这并不能得出:volatile变量的运算在并发下是安全的这个结论。原因是java里面的运算并不是原子操作,导致volatile变量的运算在并发下一样是不安全的。
可以通过一个例子说明这个问题:
public class VolatileTest {
public static volatile int race = 0;
private static void increase(){
race++;//非原子操作
}
//final is always the first
private static final int THREAD_COUNT = 20;
public static void main(String[] args){
Thread[] threads = new Thread[THREAD_COUNT];
for(int i=0; i<threads.length; i++){
threads[i]= new Thread(new Runnable() {
@Override
public void run() {
for(int i = 0; i<10000;i++){
//race++;
increase();
}
}
});
threads[i].start();
}
//等待所有线程累加结束
while (Thread.activeCount()>1){
Thread.yield();
System.out.println(race);
}
}
}
这段代码发起了20个线程,每个线程对race变量进行10000次自增操作。如果这段代码能够正确并发的话,最后输出的结果应该是200000. 但是运行完之后并不会得到正确的结果,并且每次的输出的结果都不一样,都是一个小于200000的数字,这是为什么呢?
问题出在自增运算"race++"之中,利用javap反编译这段代码后会得到如下代码:
public static void increase();
Code:
Stack=2, Locals=0, Args_size=0
0: getstatic #13; //Field race:I
3: iconst_1
4: iadd
5: putstatic #13; //Field race:I
8: return
LineNumberTable:
Line 14: 0
Line 15: 8
发现代码increase()虽然只有一行,但是在Class文件是由4条字节码指令构成的。从字节码层面上很容易就分析出并发失败的原因了:当getstatic指令把race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但在执行iconst_1,iadd这些指令的时候,其他线程可能已经别把race的值加大了饿,而在操作栈顶的值就变成了过期的数据,所以putstatic指令执行后就可能把较小的race值同回内存之中。
事实上这仍然是不严谨的,因为即时编译出来只有一条字节码指令,也并不一位执行这条指令就是一个原子操作。
由于volatile变量只能保证可见性,只有在以下两条规则的运算场景中,在其他场景仍然要通过加锁来保证原子性。
- 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程改变变量的值。
- 变量不需要与其他状态变量共同参与不变约束。
除了volatile之外,java还有两个关键字能实现可见性,即synchronized和final。
2.第二个语义是禁止指令重排序优化
普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果。而不能保证变量赋值操作顺序与程序代码中的执行顺序一致。在一个线程的方法执行过程中无法感知到这点,这也就是Java内存模型中描述的所谓的"线程内表现为串行的语义“。
在代码的例子当中就是一句在后面的代码可能会提前执行,而volatile关键字则可以避免此类情况的发生。在使用volatile修饰的变量,复制后,字节码里面会多一个”lock add1“操作,它相当于一个内存屏障,指令重排序时不能把后面的指令重排序到内存屏障之前的位置,相当于设了一个重排序的界限。