volatile 关键字的作用是什么?
相比于 synchronized 关键字(重量级锁)对性能影响较大,Java提供了一种较为轻量级的可见性和有序性问题的解决方案,那就是使用 volatile 关键字。由于使用 volatile 不会引起上下文的切换和调度,所以 volatile 对性能的影响较小,开销较低。
从并发三要素的角度看,volatile 可以保证其修饰的变量的可见性和有序性,无法保证原子性(不能保证完全的原子性,只能保证单次读/写操作具有原子性,即无法保证复合操作的原子性)。
下面将从并发三要素的角度介绍 volatile 如何做到可见和有序的。
1. volatile 如何实现可见性?
什么是可见性?
可见性指当多个线程同时访问共享变量时,一个线程对共享变量的修改,其他线程可以立即看到(即任意线程对共享变量操作时,变量一旦改变所有线程立即可以看到)。
1.1 可见性例子
/**
* volatile 可见性例子
* @author 单程车票
*/
public class VisibilityDemo {
// 构造共享变量
public static boolean flag = true;
// public static volatile boolean flag = true; // 如果使用volatile修饰则可以终止循环
public static void main(String[] args) {
// 线程1更改flag
new Thread(() -> {
// 睡眠3秒确保线程2启动
try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) {e.printStackTrace();}
// 修改共享变量
flag = false;
System.out.println("修改成功,当前flag为true");
}, "one").start();
// 线程2获取更新后的flag终止循环
new Thread(() -> {
while (flag) {
}
System.out.println("获取到修改后的flag,终止循环");
}, "two").start();
}
}
- 不使用 volatile 修饰 flag 变量时,运行程序会进入死循环,也就是说线程1对 flag 的修改并没有被线程2读到,也就是说这里的flag并不具备可见性。
- 使用 volatile 修饰 flag 变量时,运行程序会终止循环,打印提示语句,说明线程2读到了线程1修改后的数据,也就是说被 volatile 修饰的变量具备可见性。
1.2 volatile 如何保证可见性?
被 volatile 修饰的共享变量 flag 被一个线程修改后,JMM(Java内存模型)会把该线程的CPU内存中的共享变量 flag 立即强制刷新回主存中,并且让其他线程的CPU内存中的共享变量 flag 缓存失效,这样当其他线程需要访问该共享变量 flag 时,就会从主存获取最新的数据。
所以通过 volatile 修饰的变量可以保证可见性。
两点疑问及解答:
- 为什么会有CPU内存? 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1/L2/其他)后再进行操作,但是操作完后的数据不知道何时才会写回主存。所以如果是普通变量