Java 内存模型中的读、写屏障_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、并发容器中的屏障
2.5、手动内存屏障
3、不同屏障类型对比
3.1. 内存屏障的基本分类
1、读屏障(Load Barrier)
2、写屏障(Store Barrier)
3、全屏障(Full Barrier)
3.2. 按处理器架构的分类
3.3. 内存屏障的四种组合类型
4、屏障对性能的影响
前言
读屏障(Read Barrier)和写屏障(Write Barrier)是 Java 内存模型(JMM)中的重要概念,用于控制内存可见性和指令重排序。
Mermory 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; // 写操作1 storeFence(); // 写屏障(相当于喊:\"美式已做好!\") 拿铁 = 1; // 写操作2}
现在保证:
-
顾客要么看到\"没有咖啡\"
-
要么看到\"只有美式\"
-
要么看到\"美式和拿铁都有\"
但绝不会看到\"只有拿铁\"(因为写屏障确保美式先完成)
4. 加入读屏障(Load Barrier)
顾客查看流程(读操作):
void 查看饮品() { int 看到的拿铁 = 拿铁; // 读操作1 loadFence(); // 读屏障(相当于确认:\"我看到的是最新数据\") int 看到的美式 = 美式; // 读操作2 if (看到的拿铁 == 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博客https://blog.csdn.net/weixin_50055999/article/details/147519091?sharetype=blogdetail&sharerId=147519091&sharerefer=PC&sharesource=weixin_50055999&spm=1011.2480.3001.8118
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
关于更多final的介绍,可参考:对于final、finally和finalize不一样的理解-CSDN博客
主要是和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博客https://blog.csdn.net/weixin_50055999/article/details/147519091?sharetype=blogdetail&sharerId=147519091&sharerefer=PC&sharesource=weixin_50055999&spm=1011.2480.3001.8118
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、并发容器中的屏障
在 ConcurrentHashMap 的结构修改(如扩容、链表转红黑树)过程中,需要确保其他线程能及时感知到这些变化,否则可能导致数据不一致或死循环。
volatile 标记位:
例如,TreeNode
的 red-black tree 转换标志位被设计为 volatile,确保线程在读取节点类型时能获取最新的状态。
扩容时的协调:
扩容过程中,ConcurrentHashMap 通过 volatile 的 sizeCtl 和 transferIndex 字段,配合内存屏障,确保所有参与扩容的线程能同步进度并避免重复工作。
ConcurrentHashMap 中的示例:
// volatile 字段,确保修改对所有线程可见private transient volatile int sizeCtl;// CAS 操作(隐式插入内存屏障)static final boolean casTabAt(Node[] tab, int i, Node c, Node 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):
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、不同屏障类型对比
3.1. 内存屏障的基本分类
根据操作类型和作用范围,内存屏障可以分为以下三类:
1、读屏障(Load Barrier)
作用:
确保在屏障之前的读操作不会被重排到屏障之后,且屏障之后的读操作不会被重排到屏障之前。
例如:
A = read(X); // 读操作barrier_read(); // 读屏障B = read(Y); // 读操作
-
读屏障保证 read(x) 一定在 read(Y) 之前执行。
-
应用场景:
防止编译器或处理器将读操作的顺序打乱,确保读取到最新的数据(如共享变量的更新)。
2、写屏障(Store Barrier)
作用:
确保在屏障之前的写操作不会被重排到屏障之后,且屏障之后的写操作不会被重排到屏障之前。
例如:
write(X, A); // 写操作barrier_write(); // 写屏障write(Y, B); // 写操作
-
写屏障保证 write(X) 一定在 write(Y) 之前执行。
-
应用场景:
保证写操作的顺序性,确保其他线程或处理器看到的写操作顺序与代码一致(如初始化共享对象时)。
3、全屏障(Full Barrier)
作用:
同时包含读屏障和写屏障的效果,禁止所有内存操作的重排序。
例如:
write(X, A); // 写操作barrier_full(); // 全屏障read(Y); // 读操作
-
全屏障确保 write(X) 一定在 read(Y) 之前执行。
-
应用场景:
在需要严格顺序性的场景中使用(如释放锁后更新共享状态)。
3.2. 按处理器架构的分类
不同处理器架构对内存屏障的支持和实现方式不同。
以下是两种常见架构的对比:
1、x86 架构
- 内存模型:强一致性(Total Store Order, TSO)
- 特点:
- 写操作(Store)不能重排到前面的读操作(Load)之前。
- 读操作(Load)不能重排到前面的写操作(Store)之后。
- JVM 实现:
- volatile 写入时插入
Lock Addl $0x0, (%
esp)
指令(强制刷新缓存)。 - synchronized 使用 monitorenter/monitorexit 指令,底层依赖
LOCK
前缀指令。
- volatile 写入时插入
2、ARM 架构
- 内存模型:弱一致性(Out-of-Order)
- 特点:
- 读写操作可以任意重排,除非显式插入内存屏障。
- JVM 实现:
- 需要显式插入 dmb(Data Memory Barrier)指令。
3.3. 内存屏障的四种组合类型
根据屏障的作用方向,内存屏障可以进一步细分为四种组合类型:
4、屏障对性能的影响
测试数据(纳秒/操作):
最佳实践
-
尽量使用volatile:比手动屏障更安全高效
-
减少屏障使用:只在必要时插入,内存屏障会阻止指令重排序,可能导致处理器流水线效率降低。
-
了解硬件特性:不同架构的处理器对内存屏障的支持和开销不同(如 x86 的
mfence
开销较高)。 -
屏障组合使用:如双重检查锁定模式中的用法
理解这些屏障机制可以帮助开发者编写出正确且高效的多线程程序。它通过禁止指令重排序和确保变量的可见性,保障了多线程环境下的数据一致性。
参考文章:
1、什么是内存屏障?-CSDN博客https://blog.csdn.net/qq_33326733/article/details/139266028?ops_request_misc=%257B%2522request%255Fid%2522%253A%252290c36dbedebb6e68e2044e426a650908%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=90c36dbedebb6e68e2044e426a650908&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_click~default-2-139266028-null-null.142^v102^pc_search_result_base1&utm_term=%E5%86%85%E5%AD%98%E5%B1%8F%E9%9A%9C&spm=1018.2226.3001.4187
2、对于final、finally和finalize不一样的理解-CSDN博客文章浏览阅读625次,点赞21次,收藏4次。finally 是异常处理机制的一部分,用于定义无论是否发生异常都需要执行的代码块。它通常与 try 和 catch 配合使用,确保资源释放或关键操作在程序流程中始终执行。try {// 可能抛出异常的代码// 异常处理逻辑// 无论是否发生异常,都会执行的代码。https://blog.csdn.net/weixin_50055999/article/details/148116640?spm=1001.2014.3001.5501