作用
- 保证多核CPU中线程的可见性
- 禁止指令重排
底层原理
volatile 通过在适当位置插入内存屏障来实现其可见性和有序性保证。
什么是 内存屏障
内存屏障就是一个 CPU 指令,用于控制指令执行顺序和内存操作顺序。由于编译器优化和处理器内部的重排序机制,指令执行顺序可能与程序的逻辑顺序不一致。内存屏障通过抑制这种重排序,确保内存操作按预期顺序执行。
内存屏障类型
- StoreStore:你先读我再读
- StoreLoad:你先读我再存,保证你读之前的,不被我存的影响
- LoadLoad:你先存我再存,不能让你覆盖我 -LoadStore:你读完写完,我再读写,你先处理完我再处理
Jvm 对volatile 的屏障设定的规则
| 第二个操作 | 第二个操作 | 第二个操作 | |
|---|---|---|---|
| 第一个操作 | 普通 读/写 | volatile 读 | volatile 写 |
| 普通 读/写 | NO | ||
| volatile 读 | NO | NO | NO |
| volatile 写 | NO | NO |
-
在每个volatile写操作的前面插入一个StoreStore屏障。
-
在每个volatile写操作的后面插入一个StoreLoad屏障。
-
在每个volatile读操作的后面插入一个LoadLoad屏障。
-
在每个volatile读操作的后面插入一个LoadStore屏障。
作者:monkeysayhi
链接:juejin.cn/post/684490…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
什么是线程的可见性
可见性是指一个线程对共享变量变量的修改对其他线程是可见的,并且这种问只会在多核CPU中出现,因为单核CPU使用的都是同一个CPU缓存,因此单核CPU不存在可见性问题。
通常我们通过 volatile 来实现每次强制从主存中读取,但是为什么会有会有线程之间的修改无法可见的这种情况的出现呢?
这是因为 CPU 缓存的存在,比如说下面这个例子,开始 stop 在主存中的值为 false ,Worker 线程拿到后将 stop = false载入CPU1缓存,等到Controller线程 拿到时 stop = false载入CPU2缓存,然后将其改为 stop = true刷新到主存,但是 worker 线程对此没有感知,也就还是继续使用旧值,导致一直死循环。
private boolean stop = false;
private long counter = 0;
@Test
public void test2() throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("Worker线程开始");
while (!stop) {
counter++; // 纯计算,没有IO操作
// 不使用println,避免同步副作用
}
System.out.println("Worker线程结束,counter = " + counter);
});
Thread controller = new Thread(() -> {
try {
Thread.sleep(2000);
System.out.println("Controller线程:设置stop = true");
stop = true; // 修改stop,可能只在缓存中
System.out.println("Controller线程:设置完成");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
worker.start();
controller.start();
try {
worker.join(5000); // 等待5秒
if (worker.isAlive()) {
System.out.println("❌ Worker线程仍在运行,可见性问题重现!");
worker.interrupt();
} else {
System.out.println("✅ Worker线程正常停止");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
# 加上volatile 前
Worker线程开始
Controller线程:设置stop = true
Controller线程:设置完成
❌ Worker线程仍在运行,可见性问题重现!
# 加上volatile 后
Worker线程开始
Controller线程:设置stop = true
Controller线程:设置完成
Worker线程结束,counter = 5803390503
true
✅ Worker线程正常停止
什么是指令重排
编译器/CPU为了优化性能,改变程序指令的执行顺序,但是在多线程环境下可能会造成很多问题
场景1
在使用单例模式时,线程 A 第一次使用,所以会 new 一个新的 对象,new 操作分为三步
- 分配内存
- 初始化对象
- 将 instance 指向分配的内存
但是由于指令重排,顺序可能变成
- 分配内存
- 将 instance 指向分配的内存
- 初始化对象
此时如果有另一个 线程 B 获取单例的话就会获取到一个不为 null 的索引,但是索引指向的内存却没有初始化好的对象,这就相当于装货员还没把东西放进箱子里你就拿走用了,会造成各种错误
public class CompleteDemo {
static class SingletonWithProblems {
// 不同类型的字段
private String name = null; // 会导致 NPE
private int value = 0; // 会导致逻辑错误
private List<String> list = null; // 会导致 NPE
private final long createTime; // final 字段
public SingletonWithProblems() {
this.name = "Initialized";
this.value = 42;
this.list = new ArrayList<>();
this.list.add("item1");
this.createTime = System.currentTimeMillis();
// 增加重排概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 测试方法
public void testState() {
System.out.println("=== 对象状态测试 ===");
System.out.println("name: " + name);
System.out.println("value: " + value);
System.out.println("list: " + list);
System.out.println("createTime: " + createTime);
// 尝试使用字段
try {
System.out.println("name.length(): " + name.length());
} catch (NullPointerException e) {
System.err.println("❌ NPE in name.length(): " + e.getMessage());
}
try {
System.out.println("list.size(): " + list.size());
} catch (NullPointerException e) {
System.err.println("❌ NPE in list.size(): " + e.getMessage());
}
// 逻辑检查
if (value == 0) {
System.err.println("❌ 逻辑错误: value 应该是 42,但现在是 0");
}
}
}
private static SingletonWithProblems instance;
public static void runTest() {
System.out.println("=== 完整问题演示 ===");
final int[] errorCount = {0};
final int[] testCount = {0};
// 运行多次测试
for (int i = 0; i < 1000; i++) {
testCount[0]++;
instance = null; // 重置
Thread creator = new Thread(() -> {
instance = new SingletonWithProblems();
});
Thread tester = new Thread(() -> {
try {
Thread.sleep(50); // 增加问题发生概率
} catch (InterruptedException e) {
e.printStackTrace();
}
if (instance != null) {
// 检查是否是未初始化的对象
if (instance.createTime == 0) { // final 字段默认值
errorCount[0]++;
System.out.println("❌ 发现未初始化对象!测试 #" + i);
instance.testState();
}
}
});
creator.start();
tester.start();
try {
creator.join();
tester.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("\n=== 测试结果 ===");
System.out.println("总测试次数: " + testCount[0]);
System.out.println("发现错误次数: " + errorCount[0]);
System.out.println("错误率: " + (errorCount[0] * 100.0 / testCount[0]) + "%");
}
}
volatile 如何防止指令重排
使用 内存屏障,在操作之间设置屏障,保证操作 x 在操作 y 之前进行。