深入理解Java中的volatile关键字
1. 引言
在Java多线程编程中,volatile
关键字扮演着重要角色。它能够保证变量的可见性和有序性,是实现线程安全的重要工具之一。本文将深入探讨volatile
的实现原理,以及与之密切相关的指令重排概念。
2. volatile的作用
volatile
关键字主要有两个作用:
- 保证变量的可见性
- 禁止指令重排
2.1 保证可见性
在多核处理器系统中,每个处理器都有自己的缓存。当一个线程修改了共享变量的值,这个新值可能还没有从处理器的缓存刷新到主内存,导致其他线程看不到这个新值。volatile
保证,当一个线程修改了volatile
变量的值,新值会立即被刷新到主内存,并且其他线程的缓存中的值会被标记为无效,强制其从主内存重新读取。
2.2 禁止指令重排
指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,导致指令执行顺序发生变化。volatile
关键字会在指令序列中插入内存屏障,保证特定操作不会被重排序。
3. 内存屏障
内存屏障(Memory Barrier)是一种CPU指令,用于控制特定条件下的重排序和内存可见性。Java编译器会在volatile
变量的读写操作前后插入内存屏障。
3.1 内存屏障的类型
在讨论内存屏障类型之前,我们需要理解两个基本概念:
- Load: 表示读取操作,即从主内存加载数据到CPU缓存。
- Store: 表示写入操作,即将数据从CPU缓存写入到主内存。
有四种主要的内存屏障类型:
- LoadLoad屏障 (LL)
- StoreStore屏障 (SS)
- LoadStore屏障 (LS)
- StoreLoad屏障 (SL)
让我们详细解释每种类型:
LoadLoad屏障 (LL)
- 含义: 确保Load1数据的装载,在Load2及所有后续装载指令的装载之前完成。
- 作用: 防止从内存中读取的操作被重排。
StoreStore屏障 (SS)
- 含义: 确保Store1数据对其他处理器可见(刷新到内存),在Store2及所有后续存储指令的存储之前完成。
- 作用: 防止写入内存的操作被重排。
LoadStore屏障 (LS)
- 含义: 确保Load1数据装载,在Store2及所有后续的存储指令刷新到内存之前完成。
- 作用: 防止读操作与后面的写操作被重排。
StoreLoad屏障 (SL)
- 含义: 确保Store1数据对其他处理器变得可见(刷新到内存),在Load2及所有后续装载指令的装载之前完成。
- 作用: 是一个全能型的屏障,同时具有其他三个屏障的效果。
3.2 volatile操作中的内存屏障
在volatile变量的读写过程中,会插入不同类型的内存屏障:
- 写操作前插入StoreStore屏障:
- 保证在volatile写之前,所有的普通写操作已经对其他处理器可见。
- 写操作后插入StoreLoad屏障:
- 保证volatile写操作对其他处理器立即可见。
- 读操作后插入LoadLoad屏障:
- 保证volatile读操作后,后续所有普通读操作必须在volatile读操作完成后执行。
4. 指令重排
指令重排是指处理器为了提高程序运行效率,可能会对输入代码进行优化,导致指令执行顺序发生变化。这种重排序可能发生在以下几个层面:
- 编译器优化重排
- 指令级并行重排
- 内存系统重排
指令重排可以提高程序的执行效率,但在多线程环境下可能导致程序出现意料之外的行为。
5. volatile如何防止指令重排
volatile
通过插入内存屏障来禁止特定类型的指令重排,从而保证程序的正确性。以下是一个示例:
public class VolatileReorderingExample {private static int a = 0;private static boolean flag = false;// 使用volatile修饰flag// private static volatile boolean flag = false;public static void writer() {a = 1; // 1flag = true; // 2}public static void reader() {if (flag) { // 3if (a == 0) { // 4System.out.println("Reordering observed!");}}}public static void main(String[] args) throws InterruptedException {for (int i = 0; i < 1000000; i++) {a = 0;flag = false;Thread writerThread = new Thread(() -> writer());Thread readerThread = new Thread(() -> reader());writerThread.start();readerThread.start();writerThread.join();readerThread.join();}System.out.println("Test completed.");}
}
注意事项:
- 指令重排是一个微妙的现象,可能不会在每次运行或每个系统上都观察到。
- 即使不使用 volatile,你也可能需要多次运行才能观察到重排现象。
- 在某些现代 CPU 和 JVM 上,即使不使用 volatile,也可能很难观察到重排,因为它们可能有其他机制来保证顺序。
在这个例子中,volatile
保证了2不会被重排到1之前,3不会被重排到4之后。这是通过以下方式实现的:
- 在操作2(写入volatile变量)之前,插入StoreStore屏障,确保1的写入操作已经完成。
- 在操作2之后,插入StoreLoad屏障,确保2的结果对其他线程可见。
- 在操作3(读取volatile变量)之后,插入LoadLoad屏障,确保后续的读操作(如4)会读取到最新的值。
这个流程图展示了如何通过内存屏障防止指令重排:
- StoreStore屏障确保"写入a"在"写入flag"之前完成。
- StoreLoad屏障确保"写入flag"的结果对其他线程立即可见。
- LoadLoad屏障确保在"读取a"之前,先读取到flag的最新值。
通过这种方式,volatile
关键字有效地防止了可能导致程序错误的指令重排。
6. volatile的使用场景
- 状态标志
- 双重检查锁定
7. 总结
volatile
是Java并发编程中的重要工具,通过保证可见性和禁止指令重排来确保程序的正确性。理解volatile
的实现原理,特别是内存屏障的作用和指令重排的概念,对于编写高质量的并发程序至关重要。
参考资料
- 【Java面试】说一下volatile关键字