Java 内存模型中的读、写屏障

目录

[1. 基本概念](#1. 基本概念)

1.1、读屏障 (Load Barrier)

1.2、写屏障 (Store Barrier)

1.3、咖啡店例子

[2. 常见内存屏障](#2. 常见内存屏障)

2.1、volatile

1、缓存可见性

2、指令重排序

3、内存屏障

2.2、final

2.3、synchronized关键字

2.4、并发容器中的屏障

2.5、手动内存屏障

3、不同屏障类型对比

[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. 内存屏障的四种组合类型)

4、屏障对性能的影响


前言

读屏障(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)

修改后的流程:

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

  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、内存屏障

内存屏障模型如下图所示:

代码示例如下:

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
        }
    }
}

屏障分析

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

    • 确保 value = 42 先于 flag = true 对其他线程可见
  2. 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 标记位

例如,TreeNodered-black tree 转换标志位被设计为 volatile,确保线程在读取节点类型时能获取最新的状态。

扩容时的协调

扩容过程中,ConcurrentHashMap 通过 volatilesizeCtltransferIndex 字段,配合内存屏障,确保所有参与扩容的线程能同步进度并避免重复工作。

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

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

最佳实践

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

  2. 减少屏障使用:只在必要时插入,内存屏障会阻止指令重排序,可能导致处理器流水线效率降低。

  3. 了解硬件特性 :不同架构的处理器对内存屏障的支持和开销不同(如 x86 的 mfence 开销较高)。

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

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


参考文章:

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

2、对于final、finally和finalize不一样的理解-CSDN博客

相关推荐
YUNYINGXIA5 分钟前
Python实现Web请求与响应
开发语言·前端·python
玉带湖水位记录员21 分钟前
Qt+线段拖曳示例代码
开发语言·c++·qt
fashia27 分钟前
Java转Go日记(五十四):gin路由
开发语言·后端·golang·go·html·gin
_WndProc29 分钟前
【C++】控制台小游戏
开发语言·c++·vscode
小王同学的C++39 分钟前
C++中的菱形继承问题
开发语言·c++
zy happy44 分钟前
黑马点评前端Nginx启动失败问题解决记录
java·运维·前端·spring boot·nginx·spring
麻辣香蝈蝈1 小时前
【Vue3】一文学会动态路由和编程式路由的使用
开发语言·前端·javascript·vue.js
lxyker1 小时前
MongoDB大数据量的优化——mongoTemplate.stream()方法使用
java·数据库·mongodb·性能优化·数据库调优
flex88881 小时前
一个由微软开源的 Python 工具,用于将多种文件格式转换为 Markdown 格式
开发语言·python·microsoft
煤灰2421 小时前
简单用c++的类实现的string
java·开发语言·c++