在 C# 并发代码里,很多问题并不是“线程没有执行”,而是一个线程已经改了值,另一个线程却暂时看不到,或者看到的执行顺序和你写下的顺序不一致。`volatile` 和 `Volatile` 类处理的正是这类“可见性”和“有序性”问题。
这篇文章会先拆开它为什么存在,再说明语法限制、底层内存屏障、典型写法,以及它和 `Interlocked`、`lock` 的边界。读到最后,你应该能判断一个共享字段到底属于“只需要被及时看见”,还是“必须被原子修改”。
为什么需要 volatile
CPU 缓存带来的可见性问题
现代 CPU 为了提升性能,会把频繁访问的数据放进核心自己的缓存(L1/L2/L3),而不是每次都直接访问主内存。这种优化在单线程里通常没有问题,但到了多线程场景就可能出现“值已经改了,别的线程却看不见”的情况。
- 线程 A 修改了共享字段的值,但值可能先停留在自己的
CPU缓存里,还没有同步到主内存; - 线程 B 读取同一个字段时,读到的可能仍是自己缓存里的旧值,因此看不到线程 A 的更新。
这就是典型的内存可见性问题。
编译器和 CPU 的指令重排序
除了缓存,编译器和 CPU 还会为了执行效率重排指令。前提是:在单线程语义下,程序结果不能变。但多线程观察到的顺序,可能因此和源码顺序不同。
// 原始代码
bool _isReady = false;
int _data = 100;
// 编译器/CPU可能重排序为:先赋值_data,再赋值_isReady(单线程无影响)
// 但多线程下,线程B可能看到_isReady=true,但_data还是旧值
这正是 volatile 的核心价值:它不是用来保证复合操作线程安全,而是用来约束共享字段的可见性和访问顺序。
- 插入内存屏障(
memory barrier); - 避免关键读写被不合适地重排序;
- 让字段的读取和写入更接近“所见即所得”。
volatile 能修饰什么,不能修饰什么
只能用于字段
volatile 只能修饰字段,不能修饰方法、参数、局部变量、属性或常量。这一点在语法层面就有限制。
// 正确:修饰实例字段
private volatile bool _isRunning;
// 正确:修饰静态字段
private static volatile int _counter;
// 错误:不能修饰方法/参数/局部变量/属性/常量
public volatile void DoWork() { } // 编译错误
private int VolatileProperty { get; set; } // 编译错误(属性不能加volatile)
支持的类型与限制
volatile 只支持一部分类型,这和底层读写的原子性要求有关。可用类型包括:
- 引用类型,如
object、string、自定义类; - 值类型:
byte、sbyte、short、ushort、int、uint、long、ulong、char、float、bool; - 上述类型的指针,例如
int*。
注意:不支持
double、decimal、struct(自定义值类型)、DateTime等类型;这些类型的读写不是它能单独保证正确性的范围。
等效写法:Volatile.Read / Volatile.Write
如果你不想直接在字段声明上使用关键字,.NET 还提供了 Volatile 静态类。它的 Read/Write 与 volatile 关键字语义一致,但使用位置更灵活。
// 等价于 volatile 修饰的 _isRunning = true
Volatile.Write(ref _isRunning, true);
// 等价于读取 volatile 修饰的 _isRunning
bool current = Volatile.Read(ref _isRunning);
在新代码里,尤其是只想让某次读取或某次写入带有明确内存语义时,Volatile.Read 和 Volatile.Write 往往更直观。
volatile 底层到底做了什么
关键是内存屏障
volatile 的底层不是“给变量加锁”,而是通过插入内存屏障(Memory Barrier)来约束访问行为。

- 读取时使用读屏障(
Load Barrier):要求从主内存获取值,并阻止后续读取被提前到前面; - 写入时使用写屏障(
Store Barrier):要求写入对其他线程可见,并阻止之前的写操作被推迟到后面。
在语义上,常会对应到两类屏障:
Acquire Fence:读取volatile字段前,禁止将后续读取提前;Release Fence:写入volatile字段后,禁止将之前写入推迟。
因此,volatile 解决的是“看见最新值”和“看见合理顺序”,而不是把一组操作打包成不可分割的原子事务。
最常见的几种用法
场景一:线程停止标志
这是 volatile 最典型也最安全的使用方式之一:一个线程循环读取标志位,另一个线程负责修改它。
public class ThreadSafeFlag
{
private volatile bool _isRunning = true;
public void Run()
{
// 线程1:循环直到标志关闭
while (_isRunning)
{
// 执行工作
Thread.SpinWait(1000);
}
Console.WriteLine("线程停止");
}
public void Stop()
{
// 线程2:设置标志
_isRunning = false;
Console.WriteLine("停止信号已发送");
}
}
使用方式如下:
var flag = new ThreadSafeFlag();
var worker = new Thread(flag.Run);
worker.Start();
Thread.Sleep(100);
flag.Stop(); // 另一个线程能立即看到变化
worker.Join();
如果这里不加 volatile,理论上就可能出现 _isRunning 被缓存,工作线程迟迟看不到新值、甚至一直不退出的情况。
场景二:用 Volatile 类显式读写
对于需要把“带屏障的读取”和“带屏障的写入”写得更明确的代码,Volatile 类通常更适合。
using System.Threading;
private int _value;
public int ReadValue() => Volatile.Read(ref _value);
public void WriteValue(int newValue) => Volatile.Write(ref _value, newValue);
Volatile.Read:带Acquire屏障的读取;Volatile.Write:带Release屏障的写入;- 相比关键字,优点是屏障方向和调用点更清晰。
场景三:双检查锁单例
volatile 另一个常见场景是双检查锁(Double-Checked Locking)中的实例字段,防止对象发布顺序被重排。
public sealed class Singleton
{
private static volatile Singleton? _instance;
public static Singleton Instance
{
get
{
if (_instance == null)
{
lock (typeof(Singleton))
{
if (_instance == null)
_instance = new Singleton();
}
}
return _instance!;
}
}
private Singleton() { }
}
这里的重点不是“它能代替 lock”,而是它能避免实例引用对外可见时,初始化顺序在多线程观察下变得不可靠。
优缺点与适用边界
先看它的优点和代价
| 方面 | 优点 | 缺点 |
|---|---|---|
| 性能 | 极低开销(仅内存屏障),远高于锁 | 仍比普通变量慢(禁用部分优化) |
| 易用性 | 简单关键字或方法调用 | 语义复杂,易误用 |
| 适用性 | 完美用于简单标志位、状态切换、双检查锁 | 不能用于计数器、复合操作 |
| 安全性 | 提供必要内存模型保证 | 不足以实现复杂同步 |
适合使用 volatile 的场景
- 布尔标志位,例如停止信号
_isRunning; - 状态枚举,例如
Ready/Running/Stopped; - 引用类型字段的双检查锁单例;
- 一写多读(
one writer, multiple readers)模式。
不适合使用 volatile 的场景
- 计数器、累加操作:应使用
Interlocked; - 复杂状态转换:应使用
lock或无锁结构; - 32 位进程中的 64 位值场景,例如
long/double:更应考虑Interlocked。
一句话概括:volatile 能保证“这次读写被正确观察”,但保证不了“读-改-写整个过程不可被打断”。
Volatile 和 Interlocked 怎么选
把这两个工具混在一起用,是并发代码里最常见的误区之一。它们解决的问题其实不同:

| 对比项 | Volatile | Interlocked |
|---|---|---|
| 原子性 | ❌ | ✅ |
| 内存屏障 | Acquire / Release | Full Fence |
| 返回旧值 | ❌ | ✅ |
| 适用场景 | 状态观察 | 状态修改 |
| 性能 | 更快 | 稍慢 |
如果你的需求是“让别的线程尽快看见这个状态”,优先考虑 Volatile;如果你的需求是“安全地改这个状态,并且这个修改不能丢”,就该用 Interlocked。
// 读:Volatile
var state = Volatile.Read(ref _state);
// 写:CAS / Exchange
if (state == A)
Interlocked.CompareExchange(ref _state, B, A);
Volatile 更像“观察者协议”,Interlocked 才是“修改者协议”。
结语
volatile 在 .NET 多线程编程里是一个基础但很容易被误解的工具。它非常适合简单标志位、一写多读状态和双检查锁这种场景,但它从来不是通用的线程安全方案。
实际开发里,只要问题已经涉及计数、累加、条件更新、多个字段一致性,通常就该优先考虑 Interlocked、lock、Lazy<T> 或并发集合,而不是继续给字段补一个 volatile 了。









