文章目录
原子性 + 有序性双杀!彻底解决库存超卖|Java 并发篇 2

零、引入
上一篇我们用 volatile 解决了 JMM 的 “可见性” 问题,但你是不是发现:库存变量加了 volatile,照样超卖?别慌,这不是你代码写错了,而是 JMM 还有两个核心坑 —— 原子性和有序性没解决。
我上次就是只修了可见性,线上库存又超卖了 3 件,领导把新的赔偿单拍我桌上:“再搞不定,这个月绩效别要了!” 幸好王哥及时出手,带我扒透了原子性和有序性,最后用三行代码就彻底解决了超卖。
今天这篇讲完,你不仅能修复库存问题,还能搞懂单例模式的空指针坑、并发代码的乱序问题,点赞 + 关注,让你的并发代码比奶茶店的取餐系统还稳!
一、JMM 第二坑:原子性 —— 一个操作被 “拆成两半”,必出乱

“超卖的核心元凶其实是原子性,” 王哥叼着冰棒,点开我写的stock–代码,“你以为这是一步操作,其实 JVM 把它拆成了‘读 - 改 - 写’三步曲:
- 读:从主内存把stock拿到工作内存(比如拿到 100);
- 改:在工作内存把stock减 1(变成 99);
- 写:把改完的stock放回主内存;
这三步中间如果被其他线程打断,比如线程 A 刚做完第一步,线程 B 就来拿 100,俩线程都改成 99,主内存就只减了 1—— 这就是超卖的根源!”
1.1 原子性问题的 “实证代码”(跑起来就懂)

为了让我彻底信服,王哥写了段极简代码:1000 个线程各给count加 1,预期结果 1000,实际结果却永远小于 1000。
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AtomicityProblem {
// 初始值0,1000个线程各加1
static int count = 0;
static final int THREAD_NUM = 1000;
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 {
count++; // 看似原子,实际拆成三步
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("预期结果:1000");
System.out.println("实际结果:" + count); // 比如985,永远小于1000
}
}
“这就是原子性被破坏的后果,” 王哥说,“原子性就是‘一个操作要么全做完,要么全不做,不能被打断’—— 像你去 ATM 取钱,‘查余额 + 扣钱’必须是原子的,不然刚查完余额,钱就被别人转走了,你再扣就成负数了。”
1.2 解决原子性:两种方案任你选(实战必用)

王哥给了我两个解决方案,一个简单粗暴,一个轻量高效,根据场景随便挑。
📢 synchronized “加锁”—— 强制线程排队
“synchronized 就像 ATM 取钱时‘锁门’,” 王哥说,“把非原子操作包在 synchronized 里,强制线程排队执行,同一时间只有一个线程能改变量,自然不会被打断。”
修复代码(synchronized 版):
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class AtomicityFixedWithSync {
static int count = 0;
static final int THREAD_NUM = 1000;
static CountDownLatch latch = new CountDownLatch(THREAD_NUM);
// 锁对象:所有线程抢同一把锁,才能排队
static final Object LOCK = new Object();
public static void main(String[] args) throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
for (int i = 0; i < THREAD_NUM; i++) {
pool.submit(() -> {
try {
synchronized (LOCK) { // 加锁,强制线程排队
count++;
}
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("预期结果:1000");
System.out.println("实际结果:" + count); // 必为1000
}
}
💯 方案 2:AtomicInteger “原子类”—— 无锁更高效
“如果只是简单的增减操作,用 synchronized 有点‘杀鸡用牛刀’,” 王哥说,“JUC 包的原子类(比如 AtomicInteger)底层用 CAS 实现,不用加锁也能保证原子性,性能更好。”
修复代码(AtomicInteger 版):
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicityFixedWithAtomic {
// 用原子类替代普通int,自带原子操作
static AtomicInteger count = new AtomicInteger(0);
static final int THREAD_NUM = 1000;
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 {
count.incrementAndGet(); // 原子操作,不会被打断
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
System.out.println("预期结果:1000");
System.out.println("实际结果:" + count); // 必为1000
}
}
“原子类的incrementAndGet()方法,把‘读 - 改 - 写’做成了一个不可打断的原子操作,” 王哥解释,“适合库存增减、计数器这种简单场景;如果是复杂逻辑(比如先查库存再扣减,还要记录日志),还是用 synchronized 或 Lock 更靠谱。”
二、JMM 第三坑:有序性 —— 代码被 “乱序执行”,初始化没好就调用

“解决了可见性和原子性,还有最后一个坑 —— 有序性,” 王哥喝了口冰阔落,“JVM 为了提高效率,会在不影响单线程结果的前提下,偷偷调换代码执行顺序 —— 就像你早上上班,本来是‘穿衣服→洗漱→吃早饭’,JVM 可能改成‘洗漱→穿衣服→吃早饭’,单线程没问题,但多线程就炸了!”
有序性最坑场景:单例模式的空指针
“最典型的就是懒汉式单例,” 王哥写了段代码,“你以为是‘先初始化对象,再赋值给 instance’,JVM 可能改成‘先赋值,再初始化’,结果线程 B 拿到的是个‘半初始化’的对象,一调用就空指针。”
有坑的单例代码:
public class SingletonProblem {
// 单例对象(没加volatile)
private static SingletonProblem instance;
// 私有构造,防止外部实例化
private SingletonProblem() {}
// 双重检查锁的单例(看似完美,实际有坑)
public static SingletonProblem getInstance() {
if (instance == null) { // 第一次检查
synchronized (SingletonProblem.class) {
if (instance == null) { // 第二次检查
// JVM可能乱序:1.分配内存 2.赋值给instance 3.初始化对象
// 乱序后变成1→2→3,线程B拿到未初始化的instance
instance = new SingletonProblem();
}
}
}
return instance;
}
// 测试多线程获取单例
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonProblem instance = SingletonProblem.getInstance();
System.out.println(Thread.currentThread().getName() + "拿到实例:" + instance);
}, "线程" + i).start();
}
}
}
“线上高并发下,这段代码可能会抛出空指针,” 王哥说,“线程 A 刚把 instance 赋值,还没初始化,线程 B 就通过instance == null的检查,拿到了一个‘半成品’对象,一调用方法就报错。”
解决有序性:volatile 禁止重排序
“volatile 除了保证可见性,还有个隐藏技能 —— 禁止 JVM 重排序,” 王哥说,“加了 volatile,JVM 就不敢乱换代码顺序了,必须按‘分配内存→初始化对象→赋值给 instance’的顺序执行。”
public class SingletonFixed {
// 加volatile,禁止重排序
private static volatile SingletonFixed instance;
private SingletonFixed() {}
public static SingletonFixed getInstance() {
if (instance == null) {
synchronized (SingletonFixed.class) {
if (instance == null) {
instance = new SingletonFixed(); // 不会重排序
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
SingletonFixed instance = SingletonFixed.getInstance();
System.out.println(Thread.currentThread().getName() + "拿到实例:" + instance);
}, "线程" + i).start();
}
}
}
“这下不管多少线程抢,拿到的都是完整初始化的对象,再也不会空指针了。”
三、终极实战:彻底修复库存超卖(JMM 三大问题全解决)

“现在把可见性、原子性、有序性的解决方案合起来,修复你之前的库存代码,” 王哥手把手教我改代码,“用 AtomicInteger 保证原子性和可见性,再结合 CAS 操作,完美解决超卖。”
package cn.tcmeta.threads;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
/**
* @author: laoren
* @description: 彻底修复库存超卖(JMM 三大问题全解决)可见性、原子性、有序性
* @version: 1.0.0
*/
public class StockFinalFixedSample {
// 用AtomicInteger:自带可见性和原子性
static AtomicInteger stock = new AtomicInteger(100);
// 123个线程抢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 {
// 循环尝试扣减库存,直到成功或库存为0
while (true) {
// 1. 拿到当前最新库存(可见性:AtomicInteger保证)
int currentStock = stock.get();
// 2. 库存不足,抢单失败
if (currentStock <= 0) {
System.out.println(Thread.currentThread().getName() + ":库存不足,抢单失败");
break;
}
// 3. CAS原子操作:比较并交换(原子性保证)
// 意思是:如果当前库存还是currentStock,就改成currentStock-1
// 如果被其他线程改了,就返回false,重新尝试
boolean success = stock.compareAndSet(currentStock, currentStock - 1);
if (success) {
System.out.println(Thread.currentThread().getName() + ":抢单成功,剩余库存:" + stock.get());
break;
}
}
} finally {
latch.countDown();
}
});
}
// 等待所有线程执行完
latch.await();
pool.shutdown();
System.out.println("=====================");
System.out.println("最终库存:" + stock.get()); // 必为0,无超卖
}
}

跑起来后,123 个线程抢 100 件库存,最终库存刚好为 0,再也没有超卖 —— 这就是理解 JMM 三大规则后的威力!
代码核心亮点(王哥解读):
- AtomicInteger:自带可见性(变量改完同步主内存)和原子性(CAS 操作),不用加 volatile 和 synchronized;
- CAS 循环:compareAndSet方法会检查库存是否被其他线程修改,如果被改了就重新尝试,避免超卖;
- 线程池控制:用固定线程池避免线程过多,防止线上服务器 CPU 飙高。
四、 🚀第二篇总结:JMM 三大问题全解(记牢少赔 N 万)【必背】

王哥把 JMM 的核心要点总结成 “傻瓜式口诀”,我抄在便利贴贴显示器上,再也没踩过并发坑:
JMM 三大问题 + 解决方案汇总表

实战避坑指南(王哥的血泪经验)
- 核心变量优先用原子类:库存、金额、计数器等,直接用 AtomicInteger/AtomicLong,比自己加锁省心;
- 单例模式必加 volatile:双重检查锁的单例,instance 必须加 volatile,不然高并发下空指针;
- 复杂逻辑用锁:如果扣库存时还要查用户权限、记录日志,用 synchronized 或 Lock,别硬扛 CAS;
- 线上必加超时 / 限流:高并发场景下,加个抢单超时(比如 3 秒没抢到就退出),避免线程一直循环。
最后说句实在的
Java 内存模型(JMM)不是 “玄学”,而是并发编程的 “交通规则”—— 你之前踩的坑,本质是没按规则操作内存。记住:单线程看逻辑,多线程看 JMM,只要解决了可见性、原子性、有序性这三个问题,90% 的并发 bug 都能避免。
今天这两篇博客的代码,你复制过去就能直接用到秒杀、抢购等场景,再也不用为超卖赔钱。如果对你有用,点赞 + 分享给你那写并发代码 “全靠运气” 的同事,让他别再踩坑了!
关注我,下次咱们扒一扒 “JVM 锁优化”,带你看透偏向锁、轻量级锁、重量级锁的切换逻辑,让你的并发代码既安全又高效,比架构师的代码还丝滑!



171万+

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



