Java 内存模型中的读、写屏障
目录
1. 基本概念
1.1、读屏障 (Load Barrier)
1.2、写屏障 (Store Barrier)
1.3、咖啡店例子
2. 常见内存屏障
2.1、volatile
1、缓存可见性
2、指令重排序
3、内存屏障
2.2、final
2.3、synchronized关键字
2.4、手动内存屏障
3、不同屏障类型对比
4、实际CPU屏障指令
5、并发容器中的屏障应用
6、屏障对性能的影响
前言
读屏障(Read Barrier)和写屏障(Write Barrier)是 Java 内存模型(JMM)中的重要概念,用于控制内存可见性和指令重排序。
Memory Barrier,
一种特殊的CPU指令,用于控制内存操作的顺序,确保指令的执行顺序和数据的可见性。
如下图所示:
1. 基本概念
处理器为了提高性能,会对指令进行重排序,这在单线程环境下不会有问题,但在多线程环境下可能导致数据不一致的问题。内存屏障通过禁止指令重排序,确保多线程环境下的操作有序进行。
java对象的模型如下图所示:
具体如下:
从主内存读取称为load,从本地内存修改往主内存称为store。
1.1、读屏障 (Load Barrier)
作用:确保在该屏障之后的读操作能看到屏障之前的所有写操作结果。
功能:刷新处理器缓存,使当前线程能看到其他线程的最新写入。
1.2、写屏障 (Store Barrier)
作用:确保在该屏障之前的写操作对其他处理器可见。
功能: 将写缓冲区的数据刷入主内存。
1.3、咖啡店例子
我用一个咖啡店的例子帮你理解内存屏障的概念,保证你看完就懂!
1. 基础概念类比
想象一个咖啡店的工作流程:
-
Store(写操作):就像咖啡师把做好的咖啡放在取餐台
-
Load(读操作):就像顾客从取餐台拿走咖啡
-
内存屏障:就像店里的"请按顺序取餐"提示牌
2. 没有屏障的情况(问题场景)
咖啡店流程:
-
咖啡师A做美式咖啡(写操作:
美式=1
) -
咖啡师B做拿铁咖啡(写操作:
拿铁=1
) -
顾客看取餐台(读操作)
可能的问题:
由于没有顺序保证,顾客可能看到:
-
只有拿铁(美式还没放上来)
-
只有美式(拿铁还没放上来)
-
两者都看到(正确的顺序)
3. 加入写屏障(Store Barrier)
修改后的流程:
// 咖啡师工作流程(写操作)
void 制作饮品() {美式 = 1; // 写操作1storeFence(); // 写屏障(相当于喊:"美式已做好!")拿铁 = 1; // 写操作2
}
现在保证:
-
顾客要么看到"没有咖啡"
-
要么看到"只有美式"
-
要么看到"美式和拿铁都有"
但绝不会看到"只有拿铁"(因为写屏障确保美式先完成)
4. 加入读屏障(Load Barrier)
顾客查看流程(读操作):
void 查看饮品() {int 看到的拿铁 = 拿铁; // 读操作1loadFence(); // 读屏障(相当于确认:"我看到的是最新数据")int 看到的美式 = 美式; // 读操作2if (看到的拿铁 == 1) {System.out.println("美式状态:" + 看到的美式); }
}
现在保证:
当顾客看到拿铁时,对美式的查看一定是最新值
5. 实际代码对应
class CoffeeShop {int 美式 = 0; // 0=没有,1=有int 拿铁 = 0;// 咖啡师制作(写操作)public void 制作饮品() {美式 = 1;Unsafe.getUnsafe().storeFence(); // 写屏障拿铁 = 1;}// 顾客查看(读操作)public void 查看饮品() {int 看到的拿铁 = 拿铁;Unsafe.getUnsafe().loadFence(); // 读屏障int 看到的美式 = 美式;if (看到的拿铁 == 1) {System.out.println("一定有美式:" + 看到的美式); // 因为写屏障保证美式先完成,读屏障保证看到最新值}}
}
6. 关键结论
-
Store(写)屏障:
-
像喊"前面的写操作都完成了!"
-
保证屏障前的写操作先于屏障后的写操作完成
-
-
Load(读)屏障:
-
像喊"我要看最新数据!"
-
保证屏障后的读操作能看到屏障前所有写操作的结果
-
-
为什么需要:
-
没有屏障时,CPU/编译器可能重排序指令
-
就像咖啡师可能为了效率调整制作顺序
-
小结:
现在你应该能明白:内存屏障就像咖啡店里的"顺序提示牌",确保制作(Store)和取餐(Load)按照预期的顺序进行!
2. 常见内存屏障
2.1、volatile
更多volatile的介绍可参考:对于Synchronized和Volatile的深入理解_线程的volatile和synchronize-CSDN博客
volatile是Java中用来处理内存可见性问题的一种机制。被声明为volatile的变量会在每次读写时都强制刷新到主内存,并从主内存加载最新的值,从而避免了缓存一致性问题。
1、缓存可见性
关于volatile的数据结构原理,如下所示:
2、指令重排序
3、内存屏障
内存屏障模型如下图所示:
代码示例如下:
class VolatileExample {private volatile boolean flag = false;private int value = 0;public void writer() {value = 42; // 普通写flag = true; // volatile写(隐含写屏障)}public void reader() {if (flag) { // volatile读(隐含读屏障)System.out.println(value); // 保证能看到value=42}}
}
屏障分析:
-
flag = true
之前插入写屏障:-
确保
value = 42
先于flag = true
对其他线程可见
-
-
if (flag)
之后插入读屏障:-
确保读取
value
时能获取最新值。
-
2.2、final
class FinalExample {final int x;int y;public FinalExample() {x = 42; // final写y = 50; // 普通写}public void reader() {if (y == 50) {System.out.println(x); // 保证看到x=42}}
}
屏障分析:
-
final字段写入后会有写屏障,确保构造器结束前final字段对其他线程可见
2.3、synchronized关键字
关于更多synchronized的介绍可参考:对于Synchronized和Volatile的深入理解_线程的volatile和synchronize-CSDN博客
synchronized不仅可以用来实现互斥锁,还可以用来实现内存可见性。进入和退出synchronized块时,会自动插入内存屏障,确保变量的可见性。
synchronized在进入临界区时会插入一个load barrier,在退出临界区时会插入一个store barrier。
代码示例:
public class SynchronizedExample {private boolean flag = false;public void writer() {synchronized (this) {flag = true;// JVM会在这里插入一个store barrier}}public void reader() {synchronized (this) {if (flag) {System.out.println("Flag is true!");// JVM会在这里插入一个load barrier}}}
}
在这个例子中,writer方法和reader方法都被synchronized修饰,确保了writer线程对flag的修改能够被reader线程及时看到。
在JVM中,进入和退出synchronized块时,会调用monitorenter和monitorexit指令。这两个指令会插入必要的内存屏障,确保内存的可见性。
2.4、手动内存屏障
Java 通过 Unsafe
类提供手动屏障控制(Java 9+ 使用 VarHandle
):
import sun.misc.Unsafe;class ManualBarrierExample {private int x;private int y;private static final Unsafe unsafe = Unsafe.getUnsafe();public void write() {x = 1;// 手动插入写屏障unsafe.storeFence();y = 2;}public void read() {int localY = y;// 手动插入读屏障unsafe.loadFence();int localX = x;System.out.println("x=" + localX + ", y=" + localY);}
}
-
使用
Unsafe
类在实际项目中是不推荐的,因为它:-
是内部API,可能在不同JDK版本中变化
-
直接操作内存,容易导致JVM崩溃
-
通常有更好的替代方案(如
VarHandle
)
-
VarHandle.fullFence(); // 替代 Unsafe 的全屏障
VarHandle.acquireFence(); // 读屏障
VarHandle.releaseFence(); // 写屏障
3、不同屏障类型对比
关于不同屏障类型可参考如下:
具体的作用范围可参考:
4、实际CPU屏障指令
不同架构的实现:
-
x86:
mfence
(全屏障),lfence
(读屏障),sfence
(写屏障) -
ARM:
dmb
(数据内存屏障) -
PowerPC:
sync
5、并发容器中的屏障应用
ConcurrentHashMap
中的示例:
final V putVal(K key, V value) {// ...tab[index] = new Node<K,V>(hash, key, value, null); // 普通写// 隐式的StoreStore屏障synchronized (this) { // 确保节点插入先于桶的链表指针更新}// ...
}
6、屏障对性能的影响
测试数据(纳秒/操作):
最佳实践
-
尽量使用volatile:比手动屏障更安全高效
-
减少屏障使用:只在必要时插入
-
了解硬件特性:x86的TSO模型已经提供较强一致性
-
屏障组合使用:如双重检查锁定模式中的用法
理解这些屏障机制可以帮助开发者编写出正确且高效的多线程程序。它通过禁止指令重排序和确保变量的可见性,保障了多线程环境下的数据一致性。
参考文章:
1、什么是内存屏障?-CSDN博客