连载中 5/21

volatile(上):线程 A 改了变量,线程 B 为什么看不见

2026-10-25 · 68 阅读 · 0 评论 · 0 赞

一个死循环,卡住全组人

经典翻车:后台线程 while (!stop) 空转干活,主线程点停止按钮把 stop 改成 true——循环纹丝不动。变量明明改了,另一个线程怎么就是看不见?这就是并发第二宗罪:可见性。

元凶:各核心私有的 CPU 缓存

现代多核 CPU,每个核心自带私有的 L1/L2 缓存,L3 共享,主存最慢。线程跑在不同核心上,读的都是自己那份缓存副本:核心 0 改了自己缓存里的 stop,核心 1 依然读自己缓存里的旧值,两边鸡同鸭讲。寄存器、写缓冲、各级缓存——数据从核心到主存隔着好几层,「我改了」和「你看见」之间,隔着一整套存储层次。

JMM:给乱象立规矩的抽象

Java 内存模型(JMM)把这一切抽象成:所有共享变量住主内存,每个线程有自己的工作内存(变量副本),线程只能读写自己的工作内存,跨线程传值必须经过主内存中转。注意:工作内存不是某块真实的缓存,是抽象模型——它把寄存器、缓存、写缓冲一锅端进一个概念,方便在语言层立规矩。可见性问题翻译成 JMM 的话就一句:线程 B 的写,什么时候对线程 A 可见?JMM 不承诺,除非你加了同步。

volatile:把可见性写进契约

volatile 变量两条硬性规则:写 volatile 变量——JVM 在生成的指令里插入内存屏障(字节码上只是字段多一个 ACC_VOLATILE 标志),硬件层面靠 MESI 这类缓存一致性协议把其他核心的缓存行置为失效;读 volatile 变量——强制放弃本地副本,重新从主内存加载。于是一次 volatile 写对后续的 volatile 读必然可见(这层关系的学名 happens-before,下一篇细讲)。死循环 Demo 加一个 volatile 就活了:

class Worker {
    volatile boolean stop = false;   // 去掉 volatile,就是死循环

    void run() {
        while (!stop) { /* 干活 */ }
        System.out.println("收到停止信号");
    }
}

别高兴太早:volatile 治不了原子性

顺手把 volatile 加回第 1 篇的 stock-- 上——超卖依旧。因为 volatile 只保证「看见最新值」,不保证「读-改-写三步不被插队」:两个线程同时看见 stock=1000,各自减一,照样写回两个 999。可见性和原子性是两宗独立的罪,volatile 只治其一。它适合的场景:一写多读的状态开关、配置刷新;不适合:计数、累加——那是 CAS 和锁的地盘,第 7 篇起展开。

可见性讲完。volatile 还有另一半本领——禁止指令重排,以及整个 JMM 的秩序总纲 happens-before,下一篇见。

☕
503

10 年全栈工程师 · 503咖啡馆主理人

#Java#volatile#可见性#JMM#JUC

评论 (0)

热门推荐

连载中 11/22

主从搭建实操:从零配出一主两从

光讲原理不过瘾?手把手搭一主两从:my.cnf 六个参数、复制账号、GTID、CHANGE REPLICATION SOURCE TO、SHOW REPLICA STATUS 验收,附翻车排查清单。

#MySQL#主从复制#GTID#主从搭建#高可用
2026-05-07 · 10113 阅读 · 0 评论 · 0 赞
连载中 16/22

连接池:HikariCP 参数与连接风暴

连接池不是越大越好:8 核机器配 1000 连接反而更慢的数学原理,HikariCP 四个必调参数,maxLifetime 与 wait_timeout 的隐形陷阱。

#MySQL#连接池#HikariCP#maxLifetime#连接风暴
2026-05-10 · 9876 阅读 · 0 评论 · 0 赞
连载中 4/16

缓存穿透:恶意 ID 打穿 MySQL 的四道防线

请求的数据在缓存和数据库里都不存在时,缓存形同虚设。聊聊参数校验、空值缓存、布隆过滤器、限流熔断四道防线的原理与组合打法。

#Redis#缓存穿透#布隆过滤器#高可用
2026-05-16 · 9306 阅读 · 21 评论 · 287 赞