一、java内存模型与线程可见性
介绍volatile关键字之前,需先了解下Java的内存模型,如下图:
由上图可知,每个线程都有自己的工作内存,并且该线程的所有操作都是在这个工作内存中完成的,即每个线程都将主内存中的共享数据复制到自身的工作内存来进行操作,所以不同线程之间的操作是相互不可见的。
而关键字volatile可以使变量对于其他线程可见。以下为volatile关键字的使用demo
/**
* volatile关键字与线程可见性
* 启动子线程(循环判断开关是否打开,若打开,则执行doWork方法。),主线程在休眠5秒后,打开开关(onCtrl = true)
* onCtrl变量加关键字volatile,与不加的结果。
*/
public class VolatileDemo {
// private static boolean onCtrl;
//该变量使用volatile关键词修饰
private volatile static boolean onCtrl;
public void open(){
onCtrl = true;
}
public void doWork(){
System.out.println("现在开始工作!");
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(!onCtrl){
}
demo.doWork();
}
});
thread.start();
try {
Thread.sleep(2000);
System.out.println("开始打开开关!");
demo.open();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
首先,不使用volatile关键字修饰onCtrl变量,程序输出“开始打开开关!”后,会出现程序阻塞不动。
当使用了volatile关键字之后,程序输出“开始打开开关!”后,子线程发现主线程将开关修改为打开,thread的while判断为false,执行doWork()方法,打印“现在开始工作”。
二、volatile关键字不具备原子性
相对于synchronized,volatile关键字更加轻量级,但volatile关键字不保证线程安全和原子性。如需要变量原子性和线程安全的,可使用jdk封装的类,如java并发包下的原子类,如integer的AtomicInteger,HashMap的ConcurrentHashMap类,若没有相应的封装类,可使用ThreadLocal。
三、禁止指令重排
volatile关键字还有一个很重要的作用就是禁止指令重排,在程序运行过程中,编译器会优化指令执行顺序,以便得到更高的执行效率。在单线程中是没问题的,但多线程中,A线程执行代码时,指令调整可能会会导致B线程出错。
volatile关键字修饰的变量不会与相邻的变量进行指令重排。
public class VolatileDemo2 {
private int sum = 1;
private boolean isUpdate = false;
//使用volatile关键字修复,避免指令重排。
// private boolean isUpdate = false;
public void updateSum(){
//指令重排可能会导致isUpdate = true;先执行,sum=2;后执行。
sum = 2;
isUpdate = true;
}
public void doWork(){
//指令重排后,子线程调用doWork(),可能会导致下面的system.out.println已经执行,而主线程的sum=2最后才执行,导致输出结果为2。
if(isUpdate){
sum = sum +1;
}
System.out.println("预期结果为3,实际结果为:"+sum);
}
public static void main(String[] args) {
VolatileDemo2 demo2 = new VolatileDemo2();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
while(!demo2.isUpdate){}
demo2.doWork();
}
});
thread.start();;
demo2.updateSum();
}
}