一文学会Volatile关键字
引言
在 Java 多线程实战中,volatile 是一个重要的关键字,用于修饰变量,经常在JUC源码中出现,本文详细解析一下这个关键字的奥秘
1. 基本概念
volatile 关键字的主要作用是保证变量的可见性以及在一定程度上禁止指令重排序。在多线程环境下,它可以确保一个线程对被 volatile 修饰的变量所做的修改能立即被其他线程看到。
2. 内存可见性
2.1 Java 内存模型(JMM)基础
在 Java 内存模型中,每个线程都有自己的工作内存,线程对变量的操作(读取、赋值等)都是在工作内存中进行的,而变量的实际存储位置是主内存。当一个线程修改了某个变量的值,它首先会将修改后的值存储在自己的工作内存中,之后才会在某个时刻将这个值刷新到主内存。其他线程读取该变量时,也是先从自己的工作内存中读取,如果工作内存中的值不是最新的,就会导致数据不一致的问题。
2.2 volatile 保证可见性的原理
当一个变量被声明为 volatile 时,对该变量的写操作会强制将修改后的值立即刷新到主内存中,而读操作会强制从主内存中读取最新的值。这样,当一个线程修改了 volatile 变量的值,其他线程能够立即看到这个修改。
public class VolatileVisibilityExample {
// 使用 volatile 修饰变量
private static volatile boolean flag = false;
public static void main(String[] args) {
// 启动一个线程修改 flag 的值
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("Flag is set to true");
}).start();
// 主线程不断检查 flag 的值
while (!flag) {
// 循环等待
}
System.out.println("Flag is now true, exiting loop");
}
}
在上述代码中,如果 flag 变量没有被声明为 volatile ,主线程可能会一直处于循环中,因为它看不到另一个线程对 flag 的修改。而使用 volatile 修饰后,主线程能够及时看到 flag 的变化,从而退出循环。
3. 禁止指令重排序
3.1 指令重排序概念
在 Java 中,为了提高程序的执行效率,编译器和处理器会对指令进行重排序。指令重排序可以分为编译器重排序和处理器重排序。在单线程环境下,指令重排序不会影响程序的最终执行结果,但在多线程环境下,指令重排序可能会导致数据不一致的问题。
3.2 volatile 禁止指令重排序的原理
volatile 关键字可以禁止指令重排序,它通过内存屏障来实现。内存屏障是一种特殊的指令,它可以确保在屏障之前的指令不会被重排序到屏障之后,反之亦然。
public class VolatileReorderingExample {
private static int a = 0;
private static volatile boolean flag = false;
public static void writer() {
a = 1; // 1
flag = true; // 2
}
public static void reader() {
if (flag) { // 3
int i = a; // 4
System.out.println(i);
}
}
}
在上述代码中,由于 flag 被声明为 volatile,编译器和处理器不会将 flag = true 重排序到 a = 1 之前,从而保证了 reader 方法在 flag 为 true 时能够读取到 a 的最新值。
4. volatile存在的局限性
虽然 volatile 关键字可以保证变量的可见性和一定程度上禁止指令重排序,但它并不能保证原子性。例如,对于 i++ 这样的操作,它实际上包含了读取、加 1 和写入三个操作,这三个操作不是原子的。即使 i 被声明为 volatile,在多线程环境下,仍然可能会出现数据不一致的问题。如果需要保证原子性,需要使用 synchronized 关键字或 Atomic 类。
总结
volatile 是 Java 里用于修饰变量的关键字,其核心作用体现在保证变量的内存可见性以及一定程度上禁止指令重排序,在多线程编程中扮演着重要角色。
在 Java 内存模型的框架下,volatile 确保对变量的写操作会立即将修改后的值刷新到主内存,读操作则强制从主内存获取最新值,有效避免了因线程工作内存与主内存数据不一致而引发的问题。 通过内存屏障机制,volatile 能够禁止编译器和处理器对指令进行重排序,保障了多线程环境下代码按照预期顺序执行,避免因重排序导致的数据不一致问题。
volatile 存在局限性,它无法保证操作的原子性。像 i++ 这类复合操作,在多线程环境中即便使用 volatile 修饰,依然可能出现数据不一致的情况。