目录
[1. 基本概念](#1. 基本概念)
[2. 常见内存屏障](#2. 常见内存屏障)
[3.1. 内存屏障的基本分类](#3.1. 内存屏障的基本分类)
[1、读屏障(Load Barrier)](#1、读屏障(Load Barrier))
[2、写屏障(Store Barrier)](#2、写屏障(Store Barrier))
[3、全屏障(Full Barrier)](#3、全屏障(Full Barrier))
[3.2. 按处理器架构的分类](#3.2. 按处理器架构的分类)
[3.3. 内存屏障的四种组合类型](#3.3. 内存屏障的四种组合类型)
前言
读屏障(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)
修改后的流程:
java
// 咖啡师工作流程(写操作)
void 制作饮品() {
美式 = 1; // 写操作1
storeFence(); // 写屏障(相当于喊:"美式已做好!")
拿铁 = 1; // 写操作2
}
现在保证:
-
顾客要么看到"没有咖啡"
-
要么看到"只有美式"
-
要么看到"美式和拿铁都有"
但绝不会看到"只有拿铁"(因为写屏障确保美式先完成)
4. 加入读屏障(Load Barrier)
顾客查看流程(读操作):
java
void 查看饮品() {
int 看到的拿铁 = 拿铁; // 读操作1
loadFence(); // 读屏障(相当于确认:"我看到的是最新数据")
int 看到的美式 = 美式; // 读操作2
if (看到的拿铁 == 1) {
System.out.println("美式状态:" + 看到的美式);
}
}
现在保证 :
当顾客看到拿铁时,对美式的查看一定是最新值
5. 实际代码对应
java
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、内存屏障
内存屏障模型如下图所示:

代码示例如下:
java
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
关于更多final的介绍,可参考:对于final、finally和finalize不一样的理解-CSDN博客
主要是和final的初始化时候,使用构造函数执行,确保调用之前完成。
java
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。
代码示例:
java
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、并发容器中的屏障
在 ConcurrentHashMap
的结构修改(如扩容、链表转红黑树)过程中,需要确保其他线程能及时感知到这些变化,否则可能导致数据不一致或死循环。
volatile 标记位:
例如,TreeNode
的 red-black tree
转换标志位被设计为 volatile
,确保线程在读取节点类型时能获取最新的状态。
扩容时的协调:
扩容过程中,ConcurrentHashMap
通过 volatile
的 sizeCtl
和 transferIndex
字段,配合内存屏障,确保所有参与扩容的线程能同步进度并避免重复工作。
ConcurrentHashMap
中的示例:
java
// volatile 字段,确保修改对所有线程可见
private transient volatile int sizeCtl;
// CAS 操作(隐式插入内存屏障)
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i, Node<K,V> c, Node<K,V> v) {
return U.compareAndSetObject(tab, (long)i << ASHIFT, c, v);
}
// synchronized 块(隐式插入内存屏障)
final V putVal(K key, V value, boolean onlyIfAbsent) {
...
synchronized (f) { ... } // 确保对桶 f 的修改对其他线程可见
}
2.5、手动内存屏障
Java 通过 Unsafe
类提供手动屏障控制(Java 9+ 使用 VarHandle
):
java
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
)
-
java
VarHandle.fullFence(); // 替代 Unsafe 的全屏障
VarHandle.acquireFence(); // 读屏障
VarHandle.releaseFence(); // 写屏障
3、不同屏障类型对比
3.1. 内存屏障的基本分类

根据操作类型和作用范围,内存屏障可以分为以下三类:
1、读屏障(Load Barrier)
作用 :
确保在屏障之前的读操作 不会被重排到屏障之后,且屏障之后的读操作不会被重排到屏障之前。
例如:
java
A = read(X); // 读操作
barrier_read(); // 读屏障
B = read(Y); // 读操作
-
读屏障保证
read(X)
一定在read(Y)
之前执行。 -
应用场景 :
防止编译器或处理器将读操作的顺序打乱,确保读取到最新的数据(如共享变量的更新)。
2、写屏障(Store Barrier)
作用 :
确保在屏障之前的写操作 不会被重排到屏障之后,且屏障之后的写操作不会被重排到屏障之前。
例如:
java
write(X, A); // 写操作
barrier_write(); // 写屏障
write(Y, B); // 写操作
-
写屏障保证
write(X)
一定在write(Y)
之前执行。 -
应用场景 :
保证写操作的顺序性,确保其他线程或处理器看到的写操作顺序与代码一致(如初始化共享对象时)。
3、全屏障(Full Barrier)
作用 :
同时包含读屏障和写屏障的效果,禁止所有内存操作的重排序。
例如:
java
write(X, A); // 写操作
barrier_full(); // 全屏障
read(Y); // 读操作
-
全屏障确保
write(X)
一定在read(Y)
之前执行。 -
应用场景 :
在需要严格顺序性的场景中使用(如释放锁后更新共享状态)。
3.2. 按处理器架构的分类
不同处理器架构对内存屏障的支持和实现方式不同。以下是两种常见架构的对比:

3.3. 内存屏障的四种组合类型
根据屏障的作用方向,内存屏障可以进一步细分为四种组合类型:
4、屏障对性能的影响
测试数据(纳秒/操作):

最佳实践
-
尽量使用volatile:比手动屏障更安全高效
-
减少屏障使用:只在必要时插入,内存屏障会阻止指令重排序,可能导致处理器流水线效率降低。
-
了解硬件特性 :不同架构的处理器对内存屏障的支持和开销不同(如 x86 的
mfence
开销较高)。 -
屏障组合使用:如双重检查锁定模式中的用法
理解这些屏障机制可以帮助开发者编写出正确且高效的多线程程序。它通过禁止指令重排序和确保变量的可见性,保障了多线程环境下的数据一致性。
参考文章: