volatile 关键字是JAVA虚拟机提供的最轻量级的同步机制,在了解volatile的特性后,会让我们在多线程数据竞争方面更准确的使用。
一、概念解释
一个volatile 变量,具备两种特性,第一是保证此变量多所有线程的可见性,这里的“可见性”是指当一个线程修改了该变量之后,新值对其他线程来说是可以立即得知的,而普通变量做不到,普通变量需要传递和主内存来完成,比如:线程A修改了普通变量的值,然后向主内存进行回写,另一条线程B在A线程回写完成之后再从主内存进行读取操作,变量的新值才会对线程B可见。(参考上一篇内存模型图)
volatitle 变量的可见性描述是:“volatile变量对所有线程是立即可见的,对volatile 变量的所有写操作都能理解反应到其他线程中,也就是说volatile 变量再各个线程中是一致的。”这句话是对的,但是常常有点人就会通过上面描述得出:“基于volatile 变量在并发下是安全的。”这样的结论。volatile 变量再各个线程的工作内存中不存在一致性问题(因为volatile 变量再使用前,都会先刷新,即使刷新前不一致,执行引擎都是看到刷新后的值,因此可认为是一致的)。而Java 里面的运算并非原子性操作,导致volatile 变量的运算在并发下一样是不安全的,看代码:
package com;
/**
* volatile 测试
* @author Ran
*/
public class VolatileTest {
public static volatile int race = 0;
public static void increase(){
race ++ ;
}
// 线程数
public 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 i = 0;i<10000;i++){
increase();
}
}
});
threads[i].start();
}
// 等待所有累加线程结束
while(Thread.activeCount() > 1){
Thread.yield();
}
System.out.println(race);
}
}
以上代码理论结果应该是200000,但是你会发现值始终是小于这个数的,这是因为race 的自增运算不是原子性的,我们看反编译的字节码:
从字节码看出,假设A线程执行getstatic 指令把race 的值取到操作栈顶时,volatile 保证了race 的的值此时是正确的假设现在是10,然后执行iconst_1 和iadd 的时候,可能另外B线程已经完成了上面的所有操作,值已经变成11了,这个时候A线程拿到的10 已经是过期数据,这时候A继续完成下面的操作的时候,即使增加了还是11(理论两个线程完成,会变成12)。在多线程情况下,累加的值也就小于预期了。
这里从上篇的内存模型来理解:
A,B 线程,分别从主内存拿到(read) volatile 变量race=0.然后放到(load)A,B的工作内存,这时候A线程把变量传递(use)给执行引擎,按字节码进行操作。同时B 执行同样的动作,由于JVM的不确定性,A在执行到iconst_1 和iadd 的时候,B已经执行完成,这时候A继续执行,最后刷新主内存的只1,结果就不是预期的了。
上述简单的解释:A,B 线程获取volatille 变量,每次都要从新从主内存读取,并且A线程改变了变量值,会告诉B线程告诉B线程,我已经改变了,你读取必须从主内存读取。但是在A线程改变,写入主内存和发送通知时,B线程获得的变量已经是主内存中读取的了,不需要从新读取,那么此时错误就产生了。
二、解决指令从排序
这里先看一段有趣的代码:
// 变量
private static boolean flag = false;;
private static int number;
// 模拟初始化数字number
private static class B extends Thread {
@Override
public void run() {
number = 100;
flag = true;
}
}
public static void main(String[] args) {
new A().start();
new B().start();
}
// 模拟获得B 初始化后的值
private static class A extends Thread {
@Override
public void run() {
while(!flag){
System.out.println("A 线程 获得变量:"+number +":"+flag);
Thread.yield();
}
System.out.println("B 线程执行完成:"+number+":"+flag);
}
}
上面操作是模拟多线程A线程要检测和获得B线程初始化Number 的值,也就是说B线程中当number = 100,flag = true .这是一个顺序操作。但是多次执行可能会得出这样的结果:
是否重排序 | 第二个操作 | ||
第一个操作 | 普通读/写 | volatile读 | volatile写 |
普通读/写 | NO | ||
volatile读 | NO | NO | NO |
volatile写 | NO | NO |
我们以A,B操作,V,T变量,A:普通变量V 读/写 和 B:volatile变量T 写进行解释(第三行,最后一个格子)