内存屏障:多线程编程中看不见的守护者

内存屏障(Memory Barrier,也叫内存栅栏、fence)确实是现代多线程编程中最隐蔽却又极其重要的守护者之一。

很多同学写出了“看起来没问题”的多线程代码,却在高负载、特定CPU架构、特定编译器优化等级下莫名其妙地出错了,十有八九都和内存乱序 + 缺少合适的屏障有关。

为什么需要内存屏障?核心原因只有两个字:乱序

现代CPU和编译器为了性能,会在你“看不见”的地方做两层乱序:

  1. 编译器乱序(as-if rule 允许的)
  • 只要单线程语义不变,编译器可以随意重排你写的语句
  1. CPU执行乱序 + 内存系统乱序(Store Buffer、Invalidate Queue、Load/Store forwarding 等)
  • 乱序执行(Out-of-Order)
  • Store → Load 重排序(最常见、最危险)
  • 不同核心的 Store 传播到其他核心的顺序不确定

结果就是:你写的代码顺序 ≠ 其他线程“看到”的顺序

经典的错误例子(双重检查锁的经典错误版本):

// 线程1(初始化)
instance = new Singleton();      // A
inited = true;                   // B

// 线程2(使用)
if (inited) {                    // 看到 true
    // 这里却可能看到 instance 还是 nullptr !
    instance->DoSomething();
}

在某些架构(尤其是 ARM、PowerPC、RISC-V),B 可能被看到,而 A 还没被看到。
→ 这就是典型的 Store-Load 重排序 导致的可见性问题。

内存屏障的本质作用(三件事)

屏障类型防止的重排序主要保证什么x86 是否需要显式写ARM 典型指令Java 中的对应(大致)
LoadLoadLoad ↔ Load后面的读不会跑到前面读之前基本不需要dmb ishld / ldar
StoreStoreStore ↔ Store后面的写不会跑到前面写之前基本不需要dmb ishst / stlr
LoadStoreLoad 越过 Store读不会跑到写后面基本不需要
StoreLoadStore 越过 Load写不会被后面的读看到太早必须(最贵)dmb ish / full barriervolatile 读/写、Lock
全屏障(Full)所有组合最严格mfence / lock adddmb sy / dsb syUnsafe.loadFence 等

记住口诀
x86/x86-64 最强的模型 → 只有 StoreLoad 才可能乱序(其他三种基本不允许),所以写 volatile 基本就够了。
ARM / RISC-V / Power / LoongArch → 四种乱序都可能发生,必须小心写屏障。

常见语言/框架里“偷偷”帮你插入屏障的地方

语言/机制实际上做了什么屏障典型场景
Java volatileStoreStore + StoreLoadDCL、状态标志
Java volatileLoadLoad + LoadStore安全发布
C++ std::atomic (seq_cst)全屏障(最强)默认最安全但最慢
C++ std::atomic (acq_rel)LoadAcquire + StoreReleaseLock-free 队列最常见
C++ std::atomic_thread_fence显式插入内存栅栏极致性能优化时
Linux smp_mb()全屏障内核驱动、lock-free 代码
Linux smp_store_mb()StoreStore + StoreLoad发布操作
Windows MemoryBarrier()全屏障

一个最经典的“必须加屏障”的模式 —— 无锁单例(DCL正确写法)

// C++ 风格(现代写法推荐用 std::call_once 或 Meyers 单例)
std::atomic<bool> inited{false};
Singleton* instance = nullptr;

Singleton* getInstance() {
    if (inited.load(std::memory_order_acquire)) {   // LoadAcquire
        return instance;
    }

    std::lock_guard lk(mtx);   // 加锁版本最安全

    if (!inited.load(std::memory_order_relaxed)) {
        instance = new Singleton();
        inited.store(true, std::memory_order_release);  // StoreRelease
    }
    return instance;
}

或者更激进的无锁版本(需要显式 fence):

// ARM 上比较经典的写法
instance = new Singleton();           // 1. 分配+构造
std::atomic_thread_fence(std::memory_order_release);  // 重要!
inited.store(true, std::memory_order_relaxed);        // 2. 写标志

最后总结一句话:

内存屏障不是用来解决数据竞争的,它解决的是“顺序与可见性”问题。
没有它,很多“看起来线程安全的代码”在弱内存模型机器上会变成定时炸弹。

你现在写多线程代码时,最常遇到哪种需要屏障的场景?
是单例、状态标志、生产者-消费者队列、无锁数据结构,还是其他?可以说说,我可以帮你分析具体要插哪种屏障。

文章已创建 4799

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

相关文章

开始在上面输入您的搜索词,然后按回车进行搜索。按ESC取消。

返回顶部