一、概述
volatile是Java中的关键字,用来修饰会被不同线程访问和修改的变量,它可以保证并发编程三大特征(原子性、可见性、有序性)中的可见性和有序性,不能保证原子性。
二、作用
1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 保证有序性:禁止指令重排序。
(1)线程的可见性
先通过例子看下:
public class VolatileDemo {
boolean show = true;
public void updateShow() {
this.show = false;
System.out.println("修改show值为:" + this.show);
}
public static void main(String[] args) {
VolatileDemo demo = new VolatileDemo();
new Thread(() -> {
while (demo.show) {
}
System.out.println(Thread.currentThread().getName() + "结束");
}, "Thread1").start();
new Thread(() -> {
try {
Thread.sleep(2000);
demo.updateShow();
} catch (InterruptedException e) {
}
}, "Thread2").start();
}
}
打印结果可知,虽然线程Thread2已经把show 修改为false了,但是线程Thread1没有读取到show 修改后的值,线程一直在运行。
我们把show变量加上volatile:
volatile boolean show = true;
打印结果可知 ,Thread1结束,说明Thread1读取到了show 修改后的值。
由此可知,加了volatile关键字修饰的变量,只要有一个线程将主内存中的变量值做了修改,其他线程都将马上收到通知,立即获得最新值。当写线程写一个volatile变量时,JMM会把该线程对应的本地工作内存中的共享变量值刷新到主内存。当读线程读一个volatile变量时,JMM会把该线程对应的本地工作内存置为无效,线程将到主内存中重新读取共享变量。
volatile可见性的实现是借助了CPU的lock指令,lock指令在多核处理器下,可以将当前处理器的缓存行的数据写回到系统内存,同时使其他CPU里缓存了该内存地址的数据置为无效。通过在写volatile的机器指令前加上lock前缀,使写volatile具有以下两个原则:
1.写volatile时处理器会将缓存写回到主内存。
2.一个处理器的缓存写回到内存,会导致其他处理器的缓存失效。
(2)保证有序性(禁止指令重排序)
计算机在执行程序时,为了提高计算性能,编译器和处理器常常会对指令进行重排序,一般分为如下3种:
源代码 ——> 编译器优化的重排 ——> 指令并行的重排 ——>内存系统的重排 ——> 最终执行的指令
有序性的实现原理:
volatile有序性的保证就是通过禁止指令重排序来实现的。指令重排序包括编译器和处理器重排序,JMM会分别限制这两种指令重排序。
volatile通过加内存屏障来实现禁止指令重排序。JMM为volatile加内存屏障有以下4种情况:
1)在每个volatile写操作的前面插入一个StoreStore屏障,防止写volatile与后面的写操作重排序。
2)在每个volatile写操作的后面插入一个StoreLoad屏障,防止写volatile与后面的读操作重排序。
3)在每个volatile读操作的后面插入一个LoadLoad屏障,防止读volatile与后面的读操作重排序。
4)在每个volatile读操作的后面插入一个LoadStore屏障,防止读volatile与后面的写操作重排序。
(3)不保证原子性
原子性指的是,当某个线程正在执行某件事情的过程中,是不允许被外来线程打断的。也就是说,原子性的特点是要么不执行,一旦执行就必须全部执行完毕。而volatile是不能保证原子性的,即执行过程中是可以被其他线程打断甚至是加塞的。
volatile变量的原子性与synchronized的原子性是不同的。synchronized的原子性是指,只要声明为synchronized的方法或代码块,在执行上就是原子操作的,而volatile是不修饰方法或代码块的,它只用来修饰变量,对于单个volatile变量的读和写操作都具有原子性,但类似于volatile++这种复合操作不具有原子性。
因此,volatile的原子性是受限制的,在多线程环境中,volatile并不能保证原子性。