内存屏障解决什么问题
多线程环境下,
volatile修饰的字段只能防止编译器重排序和部分 CPU 重排序,但无法阻止所有指令重排,尤其在弱内存模型 CPU(如 ARM、ARM64)上,读写操作可能被乱序执行,导致其他线程看到不一致的状态。内存屏障就是用来显式插入同步点,强制约束指令执行顺序和内存可见性。
MemoryBarrier 和 Thread.MemoryBarrier 的区别
Thread.MemoryBarrier()是 .NET Framework 2.0 引入的全屏障(full fence),它同时禁止编译器和 CPU 的读写重排:前面的读写不能移到屏障后,后面的读写也不能移到屏障前。而
Thread.MemoryBarrier()在 .NET Core / .NET 5+ 中已被标记为过时,推荐使用更明确的
Thread.VolatileRead()/
Thread.VolatileWrite()或
Interlocked系列操作。
真正底层、仍在使用的屏障是
Thread.MemoryBarrier()对应的 IL 指令
monitorenter/
monitorexit隐含的语义,以及
Interlocked方法内部隐含的屏障——例如
Interlocked.CompareExchange()在 x86 上会生成
lock cmpxchg,天然带全屏障;在 ARM64 上则会插入
dmb ish指令。
Thread.MemoryBarrier():已过时,仅用于兼容旧代码,不推荐新项目使用
Thread.VolatileRead(ref int)/
Thread.VolatileWrite(ref int, int):分别提供 acquire-load 和 release-store 语义,比全屏障轻量
Interlocked.*():如
Interlocked.Increment(),不仅原子,还自带 full barrier,适合需要修改+同步的场景
volatile 字段 + 内存屏障的典型误用
很多人以为给字段加
volatile就能安全实现双检锁(Double-Check Locking),但这是错的。比如下面这个单例模式片段:
private static volatile Singleton _instance;
private static readonly object _lock = new object();
<p>public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (_lock)
{
if (_instance == null)
{
_instance = new Singleton(); // ⚠️ 构造函数可能被重排到赋值之后!
}
}
}
return _instance;
}
}问题在于:即使
_instance是
volatile,C# 编译器和 CPU 仍可能把
new Singleton()的三步(分配内存 → 调用构造函数 → 赋值给 _instance)重排成「分配 → 赋值 → 构造」,导致其他线程拿到一个未完全初始化的对象。
正确做法是用
Interlocked.CompareExchange()或 C# 6+ 的
Lazy<t></t>(其内部使用了正确的屏障):
private static readonly Lazy<Singleton> _lazy =
new Lazy<Singleton>(() => new Singleton(), isThreadSafe: true);
<p>public static Singleton Instance => _lazy.Value;何时该手动插入屏障(极少需要)
绝大多数情况你不需要手写
Thread.MemoryBarrier()。现代 C# 提供了更高层、更安全的抽象: 用
Interlocked替代自增/比较交换 用
ConcurrentQueue<t></t>/
ConcurrentDictionary<tkey tvalue></tkey>替代手动加锁+屏障 用
ManualResetEventSlim或
SpinWait替代忙等+裸屏障 只有在极少数性能敏感、且必须绕过 .NET 同步原语(如实现无锁队列)时,才需结合
Unsafe、
Interlocked和平台相关屏障(如
Thread.MemoryBarrier()或
Atomic.Read/Write在 .NET 8+)
真正容易被忽略的是:ARM64 上
volatile的语义比 x64 更弱,仅保证单个读/写不被重排,不保证前后访存顺序——这意味着依赖
volatile实现状态协同的代码,在跨平台部署时可能突然出错。
