跳转到主内容
极星编程网:以代码为星,赴技术山海!

如何理解volatile关键字的可见性与禁止指令重排作用

volatile通过强制读写主内存和插入内存屏障解决可见性与有序性问题:写操作立即刷入主内存,读操作直接从主内存获取,并禁止相关指令重排;但它不保证原子性,不能替代锁或用于复合操作。 volatile 怎么解决“一个线程改了,另一个线程看不到”的问题 根本原因是 Java 内存模型(JMM)里每个线程有自己工作内存,变量读写默认走本地缓存,不自动同步到主内存。没加
volatile
时,
running
被主线程设为
false
,但子线程可能一直从自己缓存里读
true
,死循环。 加上
volatile
后: - 写操作:线程一修改
running = false
,JVM 强制把该值立刻刷入主内存 - 读操作:线程二每次读
running
,都绕过本地缓存,直接从主内存取最新值 - 底层靠内存屏障(
lock
前缀指令)保证这点,不是靠轮询或延迟 常见误用: - 把
volatile
当成锁用,比如对
count++
加 volatile —— 它不保证原子性,仍会丢数据 - 在只被单线程访问的变量上加 volatile,纯属冗余,无意义 为什么 volatile 能阻止“代码明明写在前面,却跑到后面执行” 编译器和 CPU 为了性能,会对指令重排。例如这段初始化代码:
context = new Context();
inited = true;
看似两步,但实际可能被重排为: 1. 分配
Context
对象内存 2. 把
inited
设为
true
3. 初始化
Context
字段(此时字段还是默认值) 另一个线程看到
inited == true
就去用
context
,结果拿到未初始化完的对象。
volatile
在这里加给
inited
就能破局: - 写
inited = true
前,插入 store 屏障 → 确保上面所有指令(包括
context
初始化)必须先完成 - 读
inited
后,插入 load 屏障 → 确保后续读
context
不会提前到
inited
判断之前 - 注意:这个效果只对 volatile 变量本身及其前后操作生效,不是全局禁止重排 volatile 和 synchronized 的关键区别在哪 两者都涉及内存语义,但作用范围和代价完全不同:
synchronized
进入时强制从主内存读所有共享变量,退出时强制把所有变更刷回主内存 —— 是“全量同步”
volatile
只对声明它的那个变量起效,读写都只针对它自己 —— 是“单点同步”
synchronized
有锁开销(阻塞、上下文切换),
volatile
是无锁的,但仅限于简单状态标志场景
synchronized
天然保证原子性和可见性;
volatile
只保可见性和有序性,不保原子性 典型错误: - 用
volatile int counter
替代
AtomicInteger
——
counter++
是三步操作,volatile 挡不住中间被打断 - 在
synchronized
块里再对某个变量加
volatile
—— 多余,synchronized 已经覆盖了内存语义 哪些场景适合用 volatile,哪些坚决不能用 适合: - 状态标志位:如
volatile boolean shutdownRequested
、
volatile boolean isRunning
- 一次性安全发布(Safe Publication):如双重检查锁里的 instance 字段必须 volatile,否则可能返回构造未完成的对象 - 与 Lock 或 CAS 配合使用时的状态协调变量(如 AQS 中的部分字段) 不适合: - 任何需要复合操作原子性的场合,比如 “读-改-写”(
value += 1
)、“检查后执行”(
if (count > 0) count--
) - 被多个线程频繁读写的计数器、累加器 —— 用
AtomicInteger
或
LongAdder
- 初始化逻辑复杂、依赖多个变量协同的场景 —— volatile 无法建立变量间的 happens-before 关系 最容易被忽略的一点:volatile 的有序性保障是“相对于它自己”的。如果代码里混用非 volatile 变量,屏障不会自动延伸过去 —— 必须靠明确的读写顺序或额外同步机制兜底。

相关文章