【020】❓ 问: Java中的内存模型以及其对并发编程的影响


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 的坑了~

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值