深入理解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 {
privatestaticint a = 0;
privatestaticboolean flag = false;
// 使用volatile修饰flag
// private static volatile boolean flag = false;
public static void writer() {
a = 1; // 1
flag = true; // 2
}
public static void reader() {
if (flag) { // 3
if (a == 0) { // 4
System.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
的实现原理,特别是内存屏障的作用和指令重排的概念,对于编写高质量的并发程序至关重要。
原文始发于微信公众号(shy好好学习):深入理解Java中的volatile关键字
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/300686.html