当前位置: 首页 > news >正文

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. 没有屏障的情况(问题场景)

咖啡店流程:

  1. 咖啡师A做美式咖啡(写操作:美式=1

  2. 咖啡师B做拿铁咖啡(写操作:拿铁=1

  3. 顾客看取餐台(读操作)

可能的问题
由于没有顺序保证,顾客可能看到:

  • 只有拿铁(美式还没放上来)

  • 只有美式(拿铁还没放上来)

  • 两者都看到(正确的顺序)

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. 关键结论

  1. Store(写)屏障

    • 像喊"前面的写操作都完成了!"

    • 保证屏障前的写操作先于屏障后的写操作完成

  2. Load(读)屏障

    • 像喊"我要看最新数据!"

    • 保证屏障后的读操作能看到屏障前所有写操作的结果

  3. 为什么需要

    • 没有屏障时,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}}
}

屏障分析

  1. flag = true 之前插入写屏障

    • 确保 value = 42 先于 flag = true 对其他线程可见

  2. 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);}
}
  1. 使用 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、屏障对性能的影响

测试数据(纳秒/操作):

最佳实践

  1. 尽量使用volatile:比手动屏障更安全高效

  2. 减少屏障使用:只在必要时插入

  3. 了解硬件特性:x86的TSO模型已经提供较强一致性

  4. 屏障组合使用:如双重检查锁定模式中的用法

        理解这些屏障机制可以帮助开发者编写出正确且高效的多线程程序。它通过禁止指令重排序和确保变量的可见性,保障了多线程环境下的数据一致性。


参考文章:

1、什么是内存屏障?-CSDN博客

http://www.xdnf.cn/news/589447.html

相关文章:

  • 文化基因算法(Memetic Algorithm)详解:原理、实现与应用
  • 服务器磁盘按阵列划分为哪几类
  • MySQL8.0新特性:新特性深度应用解析
  • 【深度学习新浪潮】2025年谷歌I/O开发者大会keynote观察
  • 场景化应用实战系列五:互联网舆情检测
  • 技术分享 | MySQL大事务导致数据库卡顿
  • Java—— IO流 第三期
  • 使用 OpenCV 构建稳定的多面镜片墙效果(镜面反射 + Delaunay 分块)
  • MinerU教程第二弹丨MinerU 本地部署保姆级“喂饭”教程
  • Oracle 物理存储与逻辑管理
  • 偏微分方程数值方法指南及AI推理
  • 深入理解Diffusers: 从基础到Stable Diffusion
  • (07)数字化转型之产品材料管理:从基础数据到BOM的全生命周期管理
  • Basic concepts for seismic source - Finite fault model
  • 【 开源:跨平台网络数据传输的万能工具libcurl】
  • DOM API-JS通过文档对象树操作Doc和CSS
  • 【Linux 学习计划】-- makefile
  • shell脚本总结5
  • 当AI遇上科研:北大“科学导航”重塑学术探索全流程
  • LeetCode Hot100 (哈希)
  • x-cmd install | cargo-selector:优雅管理 Rust 项目二进制与示例,开发体验升级
  • OpenCV计算机视觉实战(7)——色彩空间详解
  • 网络图片的缓存和压缩
  • 海康相机---采集图像
  • 如何解决鸿蒙应用闪退问题
  • Flutter 3.32 新特性
  • 鸿蒙Flutter实战:21-混合开发详解-1-概述
  • flutter getx路由管理、状态管理、路由守卫中间件、永久储存get_storage
  • 汇川EasyPLC MODBUS-RTU通信配置和编程实现
  • S7-1500PLC通过工艺对象实现V90总线伺服定位控制(105报文)