jmm--volatile
volatile
是Java中一个用于解决多线程内存可见性和禁止指令重排序问题的关键字,下面从多个方面深入且易懂地解析它与JVM的关系。
1. 内存可见性
- 问题背景:在多线程环境下,每个线程都有自己的工作内存,线程对变量的操作(读取、修改)都在工作内存中进行,而不是直接操作主内存中的变量。这就可能导致一个线程对变量的修改,其他线程不能及时看到。例如:
public class VisibilityProblem {private static boolean flag = false;public static void main(String[] args) {new Thread(() -> {while (!flag) {// 线程1在等待flag变为true}System.out.println("线程1结束等待");}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {flag = true;System.out.println("线程2修改了flag");}).start();}
}
在上述代码中,线程2修改了 flag
变量,但线程1可能一直无法感知到这个变化,导致线程1无限循环。
volatile
的作用:当一个变量被声明为volatile
时,线程对该变量的修改会立即同步到主内存,并且其他线程在读取该变量时,会强制从主内存中获取最新值,而不是使用自己工作内存中的缓存值。修改上述代码如下:
public class VolatileVisibility {private static volatile boolean flag = false;public static void main(String[] args) {new Thread(() -> {while (!flag) {// 线程1在等待flag变为true}System.out.println("线程1结束等待");}).start();try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}new Thread(() -> {flag = true;System.out.println("线程2修改了flag");}).start();}
}
此时,线程2修改 flag
后,线程1能及时获取到新值,从而结束循环。
2. 禁止指令重排序
- 指令重排序概念:为了提高程序执行效率,JVM和处理器会对指令进行优化,其中一种优化方式就是指令重排序。在单线程环境下,指令重排序不会影响最终执行结果,但在多线程环境下可能会导致问题。例如:
public class ReorderingProblem {private static int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {a = 1; // 语句1b = 2; // 语句2});Thread thread2 = new Thread(() -> {if (b == 2) { // 语句3System.out.println(a); // 语句4}});thread1.start();thread2.start();thread1.join();thread2.join();}
}
在理想情况下,线程1先执行完 a = 1
和 b = 2
,线程2执行时 b == 2
为真,会输出 1
。但由于指令重排序,线程1可能先执行 b = 2
,然后执行 a = 1
,此时线程2执行时 b == 2
为真,但 a
可能还未被赋值为 1
,输出结果可能为 0
。
volatile
对指令重排序的限制:volatile
关键字具有禁止指令重排序的语义。当一个变量被声明为volatile
时,在volatile
变量的写操作之前的所有操作,都先行发生于该volatile
变量的写操作;而volatile
变量的读操作,先行发生于在其之后的所有操作。这确保了volatile
变量相关的操作顺序与代码顺序一致。例如:
public class VolatileReordering {private static volatile int a = 0;private static int b = 0;public static void main(String[] args) throws InterruptedException {Thread thread1 = new Thread(() -> {a = 1; // 语句1b = 2; // 语句2});Thread thread2 = new Thread(() -> {if (b == 2) { // 语句3System.out.println(a); // 语句4}});thread1.start();thread2.start();thread1.join();thread2.join();}
}
这里 a
被声明为 volatile
,保证了语句1在语句2之前执行,并且语句1的结果对线程2可见,从而避免了指令重排序带来的问题。
3. volatile
与JVM内存模型
JVM内存模型定义了主内存和工作内存之间的交互关系,volatile
关键字正是基于这个模型来实现其功能的。
volatile
写操作:当一个线程对volatile
变量进行写操作时,JVM会将该变量在工作内存中的最新值刷新到主内存中,同时会确保在这个写操作之前的所有普通写操作都已经刷新到主内存。volatile
读操作:当一个线程对volatile
变量进行读操作时,JVM会强制从主内存中读取该变量的值,而不是使用工作内存中的缓存值,并且会确保在这个读操作之后的所有普通读操作都能看到volatile
变量的最新值。
4. volatile
的使用场景
- 状态标记:常用于标记某些状态,例如线程的运行状态。如一个线程负责数据加载,另一个线程负责数据处理,数据加载线程完成加载后通过修改
volatile
标记通知处理线程。
public class DataLoader {private static volatile boolean dataLoaded = false;public static void main(String[] args) {new Thread(() -> {// 模拟数据加载try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}dataLoaded = true;}).start();new Thread(() -> {while (!dataLoaded) {// 等待数据加载完成}System.out.println("开始处理数据");}).start();}
}
- 单例模式中的双重检查锁定(DCL):在实现单例模式时,使用
volatile
可以防止指令重排序导致的对象初始化问题。
public class Singleton {private static volatile Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton();}}}return instance;}
}
5. volatile
的局限性
- 不保证原子性:
volatile
不能保证复合操作的原子性。例如count++
操作,它实际上包含读取、增加和写入三个步骤,即使count
被声明为volatile
,在多线程环境下仍然可能出现数据竞争问题。对于这种情况,需要使用AtomicInteger
等原子类或者synchronized
关键字来保证原子性。
public class VolatileAtomicityProblem {private static volatile int count = 0;public static void main(String[] args) throws InterruptedException {Thread[] threads = new Thread[100];for (int i = 0; i < 100; i++) {threads[i] = new Thread(() -> {for (int j = 0; j < 1000; j++) {count++;}});threads[i].start();}for (Thread thread : threads) {thread.join();}System.out.println("最终count的值: " + count);}
}
上述代码中,由于 count++
不是原子操作,最终输出的 count
值往往小于预期的 100000
。
综上所述,volatile
关键字在多线程编程中是一个强大的工具,能够有效解决内存可见性和指令重排序问题,但需要清楚其适用场景和局限性,以确保多线程程序的正确性。