文章目录
Java内存模型(JMM)及其对并发编程的影响:
1、可见性: JMM定义了线程如何和何时可以看到其他线程写入的共享变量的值,关键字如volatile在此起作用。
2、原子性: JMM定义了哪些操作是原子性的,即不可分割的。
3、顺序性: JMM通过happens-before原则,定义了一个线程对共享数据的写入何时对其他线程可见。
📢 库存超卖赔 2 万,我先扒了 JMM 的 “可见性” 坑|Java 并发篇 1

零、引入
你以为库存超卖是因为 “代码没加锁”?我当初也这么想,把if (stock > 0) stock–;包上synchronized,结果线上照样超卖。直到领导把 2 万赔偿单拍我桌上,我才明白:真正的元凶是 Java 内存模型(JMM)的 “可见性” 坑 —— 线程改了库存,其他线程根本看不见,就像你同事改了公共文件没放回柜子,你还在看旧版本,不翻车才怪!
今天这篇先扒透 JMM 的核心概念和 “可见性” 问题,下一篇再解决原子性、有序性,看完你不仅能修复超卖,还能明白 “为什么本地测不出线上的并发 bug”。记得点赞 + 关注,不然下次遇到 JMM 坑,赔的可能就不是 2 万了!
一、先破误区:JMM 不是 “内存”,是 “线程操作内存的交通规则”

我对着超卖日志抓头发时,隔壁王哥叼着煎饼凑过来:“你别把 JMM 想成内存条那堆硬件,它是 Java 定的‘线程操作内存的规则手册’—— 就像公司的《文件管理规范》,规定了员工(线程)怎么从文件柜(主内存)拿文件(变量)、怎么在自己桌子(工作内存)改文件、改完怎么放回去。”
王哥的 “办公三件套” 比喻(秒懂 JMM 核心)

“你之前的库存代码,问题就出在‘放回去’这步,” 王哥点开我写的代码,“线程 A 把库存 100 从文件柜拿到桌面,改成 99,还没放回柜子,线程 B 就去拿 —— 拿到的还是 100,俩线程都扣成 99,库存就多扣了一次。本地单 CPU 线程排队执行,改完立刻放回去;线上多 CPU,每个 CPU 有自己的‘办公桌’,线程改完没同步,其他 CPU 的线程根本看不见,这不就乱套了?”

➡️ 你的 “超卖预备役” 代码(本地稳,线上炸)
package cn.tcmeta.jmm;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* @author: laoren
* @description: TODO
* @version: 1.0.0
*/
public class StockProblemSample {
// 主内存中的共享变量:库存100件
static int stock = 100;
// 123个线程抢单(模拟线上高并发)
static final int THREAD_NUM = 123;
// 等待所有线程执行完
static CountDownLatch latch = new CountDownLatch(THREAD_NUM);
static void main() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < THREAD_NUM; i++) {
pool.submit(() -> {
try {
// 模拟用户抢单:库存>0就扣减
if (stock > 0) {
// 模拟网络延迟(线上环境常见,放大并发问题)
Thread.sleep(10);
stock--;
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
// 等所有线程跑完,看最终库存
latch.await();
pool.shutdown();
System.out.println("最终库存:" + stock); // 本地可能0,线上大概率负数
}
}

你本地跑 10 次可能有 8 次是 0,但扔到线上多 CPU 环境,必出超卖 —— 这就是 JMM 规则被破坏的后果。而所有问题的起点,都是第一个核心坑:可见性。
二、JMM 第一坑:可见性 —— 线程改了 “不吱声”,别人白干活
为了让我彻底信服,王哥写了段极简代码,我跑起来直接懵了:主线程改了stopFlag,线程 A 却一直死循环,根本没看到变化!
package cn.tcmeta.jmm;
/**
* @author: laoren
* @date: 2025/12/5 14:28
* @description: TODO
* @version: 1.0.0
*/
public class VisibilityDeadLoop {
// 共享变量:标记线程是否停止(没加JMM规则约束)
static boolean stopFlag = false;
public static void main(String[] args) throws InterruptedException {
// 线程A:只要stopFlag是false就一直跑
new Thread(() -> {
System.out.println("线程A启动,开始循环...");
while (!stopFlag) {
// 空循环,模拟业务逻辑
}
System.out.println("线程A停止,循环结束"); // 永远不会执行!
}, "线程A").start();
// 主线程:3秒后把stopFlag改成true
Thread.sleep(3000);
stopFlag = true;
System.out.println("主线程:已将stopFlag设为true,喊线程A停手");
}
}

问题根源:JMM 的 “缓存优化” 坑
-
王哥解释:“JVM 为了提高效率,会把工作内存的变量缓存到 CPU 缓存里 —— 线程 A 反复读stopFlag,JVM 就把它存到 CPU 缓存,根本不读主内存了;
-
主线程改了主内存的stopFlag,但线程 A 的 CPU 缓存没更新,所以它永远觉得stopFlag是 false。这就是可见性问题:变量修改没同步,缓存没失效。”
📢解决办法:用 volatile 给变量 “装个广播喇叭”
“volatile 就是 JMM 规则里的‘文件修改通知’,” 王哥说,“加了 volatile 的变量,会强制线程遵守两个规则:
- 改完必须立刻同步回主内存,不准存在工作内存里;
- 其他线程再读这个变量,必须先清空自己的缓存,去主内存拿最新值。”
修复后的代码(加 volatile 就活了)
package cn.tcmeta.jmm;
/**
* @author: laoren
* @date: 2025/12/5 14:28
* @description: TODO
* @version: 1.0.0
*/
public class VisibilityDeadLoop {
// 共享变量:标记线程是否停止(没加JMM规则约束)
static volatile boolean stopFlag = false;
public static void main(String[] args) throws InterruptedException {
// 线程A:只要stopFlag是false就一直跑
new Thread(() -> {
System.out.println("线程A启动,开始循环...");
while (!stopFlag) {
// 空循环,模拟业务逻辑
}
System.out.println("线程A停止,循环结束"); // 永远不会执行!
}, "线程A").start();
// 主线程:3秒后把stopFlag改成true
Thread.sleep(3000);
stopFlag = true;
System.out.println("主线程:已将stopFlag设为true,喊线程A停手");
}
}

“你看,volatile 就像给变量装了个广播喇叭,主线程一改,所有线程都能听见‘变量更新了,赶紧去主内存拿新的’,这就是可见性的解决办法。” 王哥补充,“但别高兴太早,volatile 只能解决可见性,解决不了原子性 —— 你之前的库存超卖,还有第二个大坑。”
三、实战:给库存变量加 volatile,能解决超卖吗?(踩坑预警)
我赶紧给库存代码的stock加了 volatile,心想这下稳了,结果王哥一盆冷水浇下来:“你跑跑看,照样超卖!volatile 管不了原子性,这是另一个 JMM 核心问题。”
加了 volatile 的 “伪修复” 代码
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class StockVolatileFakeFix {
// 加了volatile的库存变量
static volatile int stock = 100;
static final int THREAD_NUM = 123;
static CountDownLatch latch = new CountDownLatch(THREAD_NUM);
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < THREAD_NUM; i++) {
pool.submit(() -> {
try {
if (stock > 0) {
Thread.sleep(10);
stock--; // 看似没问题,实际有原子性坑
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("最终库存:" + stock); // 还是会超卖,比如-3
}
}
四、第一篇总结:JMM 可见性核心要点(记牢不踩坑)
王哥拍了拍我的肩膀,给我画了个 “可见性避坑表”,贴显示器上比便利贴管用:

王哥的血泪彩蛋
“我刚工作时,写的定时任务线程停不下来,” 王哥捂脸,“就是因为停止标志没加 volatile,线程一直读缓存里的旧值,我以为是线程卡死,重启了 10 次服务器,最后发现是少了个关键字 —— 被领导骂‘连 JMM 基础都不懂’!”
下一篇预告
这篇我们搞懂了 JMM 的可见性和 volatile 的用法,但库存超卖还没彻底解决 —— 因为原子性问题还在作祟。下一篇我们扒透:
- 什么是原子性?为什么stock–会被拆成三步?
- synchronized 和 AtomicInteger 怎么解决原子性?
- 最后用 JMM 三大规则(可见性 + 原子性 + 有序性)彻底修复库存超卖代码!
关注我,下一篇带你彻底告别并发超卖,把 2 万赔偿款赚回来!如果这篇对你有用,点赞 + 分享给你那写并发代码 “全靠运气” 的同事,让他别再踩 JMM 的坑了~



被折叠的 条评论
为什么被折叠?



