【021】原子性 + 有序性双杀!彻底解决库存超卖|Java 并发篇 2

原子性 + 有序性双杀!彻底解决库存超卖|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 锁优化”,带你看透偏向锁、轻量级锁、重量级锁的切换逻辑,让你的并发代码既安全又高效,比架构师的代码还丝滑!

请添加图片描述
请添加图片描述
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值