首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >别再只会用 volatile!JMM 三大核心全解:从根上搞定 Java 并发诡异问题

别再只会用 volatile!JMM 三大核心全解:从根上搞定 Java 并发诡异问题

作者头像
果酱带你啃java
发布2026-04-14 15:01:21
发布2026-04-14 15:01:21
330
举报

本文基于Java 17

一、为什么你必须搞懂JMM?

并发编程中90%的诡异BUG,都源于你对JMM的认知缺失:

  • 明明修改了共享变量,另一个线程却始终看不到最新值
  • 单线程测试完全正常,多线程运行就出现偶发的逻辑错误
  • 加了volatile还是有并发问题,却找不到根源
  • 双重检查锁(DCL)单例,为什么必须加volatile?

这些问题的本质,都和CPU缓存、指令重排、多线程内存交互规则直接相关,而JMM(Java内存模型)正是Java官方定义的、用来规范多线程环境下内存交互行为的唯一标准,它屏蔽了不同CPU架构、操作系统的底层差异,让Java并发程序在所有平台上都能保持一致的行为。

重要区分:JMM ≠ JVM内存结构

  • JVM内存结构(堆、栈、方法区、程序计数器等):解决的是「数据在内存中存在哪里、怎么存」的问题,是JVM运行时的内存分区规则
  • JMM:解决的是「多线程下数据什么时候可见、执行顺序如何保证」的问题,是并发内存交互的规范,二者是完全不同的两个概念,切勿混淆

二、JMM要解决的三大核心问题

并发编程的所有问题,都可以归结为三个核心特性的保障:原子性、可见性、有序性,JMM的核心目标就是为这三个特性提供统一的规范保障。

特性

问题根源

核心定义

原子性

线程切换导致的操作中断

一个操作要么全部执行完成,要么完全不执行,执行过程中不会被线程调度打断

可见性

CPU多级缓存、缓存一致性协议的延迟

一个线程修改了共享变量的值,其他线程能立即看到这个修改的最新值

有序性

编译器、CPU的指令重排优化

程序的执行顺序,和代码的编写顺序保持一致

其中,原子性由synchronized、JUC原子类保障;而可见性与有序性的底层实现,完全依赖JMM的三大核心:指令重排、内存屏障、先行发生原则,下面我们逐层拆解。

三、核心一:指令重排——性能优化的双刃剑

3.1 什么是指令重排?

为了最大化利用CPU的运算性能,编译器和CPU会在不改变单线程执行结果的前提下,对代码指令进行重新排序,这个过程就是指令重排。

举个通俗的例子:你做饭的代码顺序是「洗锅→接水→等水烧开→洗米→切菜」,重排后你会在等水烧开的间隙洗米、切菜,最终的结果和原顺序完全一致,但效率大幅提升,这就是指令重排的核心逻辑。

3.2 指令重排的分类与约束

注意:Java前端编译器javac几乎不会做指令重排,绝大多数重排发生在JIT即时编译期和CPU运行期,这是很多博客的常见错误

3.2.1 不可突破的底线:as-if-serial语义

as-if-serial语义是指令重排的核心约束:不管怎么重排,单线程环境下的程序执行结果不能被改变

为了遵守这个语义,编译器和CPU不会对存在数据依赖的指令进行重排。

数据依赖:如果两个指令操作同一个变量,且其中一个是写操作,那么这两个指令就存在数据依赖,重排会改变单线程结果,因此绝对禁止。

示例:

代码语言:javascript
复制
int a = 1;
int b = a + 1;

这两行代码存在数据依赖,b的值依赖a的赋值结果,因此绝对不会被重排。

反例:

代码语言:javascript
复制
int a = 1;
int b = 2;

这两行代码没有数据依赖,完全可能被重排,先执行b=2,再执行a=1,单线程结果不变。

3.2.2 多线程下的致命漏洞

as-if-serial语义只保证单线程的执行结果,完全不考虑多线程环境的影响,这就是并发诡异问题的核心根源。

我们通过一个可直接运行的完整示例,直观看到指令重排带来的问题:

代码语言:javascript
复制
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * 指令重排问题复现示例
 * @author ken
 * @date 2024
 */
@Slf4j
publicclass ReorderDemo {
    /** 共享变量a */
    privatestaticint a = 0;
    /** 标记变量flag */
    privatestaticboolean flag = false;
    /** 统计重排发生的次数 */
    privatestaticfinal AtomicInteger REORDER_COUNT = new AtomicInteger(0);
    /** 循环执行次数,提高重排发生概率 */
    privatestaticfinalint LOOP_COUNT = 100000;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < LOOP_COUNT; i++) {
            // 每次循环重置状态
            a = 0;
            flag = false;
            CountDownLatch latch = new CountDownLatch(2);

            // 写线程
            Thread writerThread = new Thread(() -> {
                a = 1;
                flag = true;
                latch.countDown();
            });

            // 读线程
            Thread readerThread = new Thread(() -> {
                if (flag) {
                    if (a == 0) {
                        // 进入此分支,说明发生了指令重排:flag=true先执行,a=1后执行
                        REORDER_COUNT.incrementAndGet();
                        log.error("指令重排发生!a={}, flag={}", a, flag);
                    }
                }
                latch.countDown();
            });

            writerThread.start();
            readerThread.start();
            latch.await();
        }

        log.info("循环执行结束,总次数:{},重排发生次数:{}", LOOP_COUNT, REORDER_COUNT.get());
    }
}

代码说明

  1. 代码采用JDK17
  2. 写线程先执行a=1,再执行flag=true;读线程如果看到flag=true,正常逻辑下a应该等于1
  3. 但由于指令重排,a=1flag=true没有数据依赖,可能被重排为先执行flag=true,再执行a=1
  4. 此时读线程看到flag=true,但a还没被赋值,就会出现a=0的情况,也就是重排发生了

运行结果:在JDK17的Server模式下,多次循环后一定会出现重排,日志会打印出重排发生的记录,完美复现了指令重排带来的并发问题。

3.3 控制依赖的重排陷阱

as-if-serial语义只保证数据依赖,不保证控制依赖,这是另一个常见的重排陷阱。 示例代码:

代码语言:javascript
复制
int a = 0;
boolean flag = false;

// 写线程
a = 1;
flag = true;

// 读线程
if (flag) { // 控制依赖
    int b = a + 1;
}

写线程的a=1flag=true没有数据依赖,读线程的if(flag)是控制依赖,编译器和CPU会采用猜测执行的优化:提前计算a+1的值,等flag=true的时候直接赋值给b,这就会导致重排问题,和上面的示例逻辑完全一致。

四、核心二:内存屏障——重排与可见性的底层解药

既然指令重排会带来这么多问题,那怎么禁止重排?答案就是内存屏障

4.1 什么是内存屏障?

内存屏障是CPU层面的一组特殊指令,它有两个核心作用:

  1. 禁止指令重排:内存屏障两边的指令,不能越过屏障进行重排,相当于给指令执行加了一道「不可跨越的墙」
  2. 保证内存可见性:强制刷新CPU缓存到主内存,同时让其他CPU中对应的缓存行失效,确保所有线程看到的变量值是最新的,和MESI缓存一致性协议配合实现

JMM屏蔽了不同CPU架构的底层差异,在不同平台(x86、ARM等)会自动生成对应的内存屏障指令,开发者不需要关心底层实现,只需要遵循JMM规范即可。

4.2 JMM定义的四大标准内存屏障

JSR-133规范中,定义了四种标准的内存屏障,覆盖了所有重排禁止场景:

屏障类型

核心作用

禁止的重排类型

LoadLoad

保证读操作的顺序性

禁止前面的Load读指令,和后面的Load读指令重排

StoreStore

保证写操作的顺序性

禁止前面的Store写指令,和后面的Store写指令重排

LoadStore

保证读先于写执行

禁止前面的Load读指令,和后面的Store写指令重排

StoreLoad

全能屏障,性能开销最大

禁止前面的Store写指令,和后面的Load读指令重排,同时具备其他三种屏障的所有效果

4.3 内存屏障的实际应用:volatile的底层实现

我们日常开发中用的volatile关键字,底层就是通过内存屏障实现的,这也是volatile能保证有序性和可见性的核心原因。

JMM为volatile定义了严格的内存屏障插入策略,100%符合JSR-133规范:

  1. 在每个volatile写操作的前面,插入StoreStore屏障
  2. 在每个volatile写操作的后面,插入StoreLoad屏障
  3. 在每个volatile读操作的后面,插入LoadLoad屏障
  4. 在每个volatile读操作的后面,插入LoadStore屏障

我们用这个策略,来解决上面的指令重排问题:只需要给flag变量加上volatile修饰,就能彻底禁止重排。 修改后的核心代码:

代码语言:javascript
复制
/** 标记变量flag,添加volatile修饰,禁止指令重排 */
private static volatile boolean flag = false;

原理说明

  • flag是volatile变量,写操作flag=true的前面有StoreStore屏障,禁止前面的a=1(Store写)和flag=true(volatile写)重排,保证a=1一定先于flag=true执行
  • flag=true的后面有StoreLoad屏障,保证写操作完成后,所有读操作都能看到最新值
  • 读线程对flag的读操作后面有LoadLoad和LoadStore屏障,保证先读flag,再读a,不会重排

修改后的代码,无论循环多少次,都不会再出现重排问题,完美解决了有序性和可见性问题。

4.4 其他场景的内存屏障应用

除了volatile,JMM中其他关键字的底层也依赖内存屏障:

  1. synchronizedmonitorenter(加锁)相当于volatile读,会插入LoadLoad、LoadStore屏障;monitorexit(解锁)相当于volatile写,会插入StoreStore、StoreLoad屏障,因此synchronized能保证原子性、可见性、有序性
  2. final关键字:JSR-133增强了final的语义,在对象构造函数执行结束后,会插入StoreStore屏障,保证final字段的初始化一定对其他线程可见,不会出现半初始化的对象
  3. JUC锁:ReentrantLock等锁的底层,通过AQS的volatile state变量实现,同样依赖内存屏障保证可见性和有序性

4.5 JDK9+新特性:VarHandle灵活控制内存屏障

JDK9引入了VarHandle(变量句柄),用来替代不安全的Unsafe类,提供了更安全、更灵活的内存屏障控制能力,性能比volatile更优,是目前JDK官方推荐的并发编程工具。

下面是JDK17下VarHandle的完整使用示例,实现和volatile相同的效果,同时支持更细粒度的内存屏障控制:

代码语言:javascript
复制
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * VarHandle内存屏障使用示例
 * @author ken
 * @date 2024
 */
@Slf4j
publicclass VarHandleDemo {
    privateint a = 0;
    privateboolean flag = false;
    privatestaticfinal VarHandle A_HANDLE;
    privatestaticfinal VarHandle FLAG_HANDLE;
    privatestaticfinalint LOOP_COUNT = 100000;
    privatestaticfinal AtomicInteger ERROR_COUNT = new AtomicInteger(0);

    // 静态初始化VarHandle,绑定对应的变量
    static {
        try {
            MethodHandles.Lookup lookup = MethodHandles.lookup();
            A_HANDLE = lookup.findVarHandle(VarHandleDemo.class, "a", int.class);
            FLAG_HANDLE = lookup.findVarHandle(VarHandleDemo.class, "flag", boolean.class);
        } catch (ReflectiveOperationException e) {
            thrownew ExceptionInInitializerError(e);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < LOOP_COUNT; i++) {
            VarHandleDemo demo = new VarHandleDemo();
            CountDownLatch latch = new CountDownLatch(2);

            // 写线程,使用release模式写,对应StoreStore屏障
            Thread writerThread = new Thread(() -> {
                A_HANDLE.setRelease(demo, 1);
                FLAG_HANDLE.setRelease(demo, true);
                latch.countDown();
            });

            // 读线程,使用acquire模式读,对应LoadLoad屏障
            Thread readerThread = new Thread(() -> {
                if ((boolean) FLAG_HANDLE.getAcquire(demo)) {
                    if ((int) A_HANDLE.getAcquire(demo) == 0) {
                        ERROR_COUNT.incrementAndGet();
                        log.error("并发问题发生!a={}, flag={}", demo.a, demo.flag);
                    }
                }
                latch.countDown();
            });

            writerThread.start();
            readerThread.start();
            latch.await();
        }

        log.info("循环执行结束,总次数:{},错误次数:{}", LOOP_COUNT, ERROR_COUNT.get());
    }
}

代码说明

  1. 基于JDK17
  2. setRelease方法对应StoreStore屏障,保证前面的写操作不会和后面的release写重排
  3. getAcquire方法对应LoadLoad屏障,保证前面的acquire读不会和后面的读操作重排
  4. 性能比volatile更优,支持更细粒度的内存屏障控制,是JUC包底层的核心实现工具

五、核心三:先行发生原则——判断并发安全的唯一依据

到这里你可能会问:难道我每次写并发代码,都要去关心底层的内存屏障和指令重排吗?

当然不需要。JMM给开发者提供了一套上层的、无需关心底层实现的判断规则,这就是先行发生原则(happens-before),它是判断多线程环境下,共享变量的访问是否存在竞争、操作是否线程安全的唯一依据

5.1 先行发生原则的权威定义

JLS Java SE 17 Edition 17.4.5 章节明确规定:

如果一个操作A happens-before 操作B,那么A操作的执行结果,对B操作完全可见,且A操作的执行顺序排在B操作之前。 同时,happens-before关系具有传递性:如果A happens-before B,B happens-before C,那么A happens-before C。

必须纠正的致命误区

很多人误以为「先行发生」就是「时间上先执行」,这是完全错误的!

  • 时间上先执行的操作,不一定具有先行发生关系
  • 具有先行发生关系的操作,时间上不一定先执行(重排不影响先行发生的结果保证)

举个例子:线程A在时间上先修改了一个普通变量,线程B在时间上后读取这个变量,因为没有任何先行发生规则的约束,所以A的修改对B不一定可见,哪怕时间上A先执行。

5.2 JMM内置的8大天然先行发生规则

JMM内置了8条无需任何额外同步、天然存在的先行发生规则,所有的线程安全保障,都基于这8条规则实现,必须逐条吃透,100%准确记忆:

1. 程序顺序规则

在同一个线程内,按照代码的控制流顺序,前面的操作happens-before后面的所有操作。

注意:是控制流顺序,不是代码顺序,包含分支、循环等场景;单线程内的重排不影响此规则,因为as-if-serial语义保证了执行结果和代码顺序一致。

2. 管程锁定规则

对同一个锁的unlock解锁操作,happens-before后续对这个锁的lock加锁操作。

这是synchronized和ReentrantLock的核心规则,保证了加锁前的所有修改,对加锁后的线程完全可见。

3. volatile变量规则

对一个volatile变量的写操作,happens-before后续对这个变量的读操作。

JSR-133增强后的核心规则,保证了volatile写的所有结果,对后续的volatile读完全可见,也是volatile能解决可见性和有序性问题的规范依据。

4. 线程启动规则

对一个Thread对象的start()方法调用,happens-before这个线程内的所有操作。

也就是说,主线程启动子线程前的所有修改,子线程启动后都能完全看到。

5. 线程终止规则

一个线程内的所有操作,happens-before其他线程对这个线程的终止检测操作。

比如通过Thread.join()等待线程结束、Thread.isAlive()判断线程是否终止,只要检测到线程终止,线程内的所有修改都对当前线程可见。

6. 线程中断规则

对一个线程的interrupt()中断方法调用,happens-before被中断线程检测到中断事件的操作。

比如通过isInterrupted()、interrupted()方法检测到中断,一定能看到调用interrupt()之前的所有修改。

7. 对象终结规则

一个对象的初始化完成(构造函数执行结束),happens-before这个对象的finalize()方法的开始执行。

保证了finalize()方法执行时,对象的所有字段都已经完成初始化,不会看到半初始化的对象。

8. 传递性规则

如果操作A happens-before 操作B,操作B happens-before 操作C,那么操作A happens-before 操作C。

这是先行发生原则的核心扩展能力,通过传递性,可以组合多条规则,形成完整的可见性保障。

5.3 先行发生原则实战应用

我们通过两个经典案例,彻底搞懂先行发生原则的实际使用。

案例1:用先行发生原则解释volatile解决重排问题

回到我们最开始的重排示例,给flag加上volatile修饰后,为什么能解决问题?我们用先行发生规则拆解:

  1. 程序顺序规则:写线程内的a=1 happens-before flag=true(volatile写)
  2. volatile变量规则:写线程的flag=true(volatile写) happens-before 读线程的if(flag)(volatile读)
  3. 程序顺序规则:读线程内的if(flag) happens-before System.out.println(a)
  4. 传递性规则:a=1 happens-before System.out.println(a)

因此,a=1的修改对读线程的打印操作完全可见,绝对不会出现a=0的情况,完美解决了重排问题。

案例2:经典的DCL双重检查锁单例,为什么必须加volatile?

DCL单例是面试高频题,很多人只知道要加volatile,却不知道为什么,我们用先行发生原则彻底讲透。

首先看错误的DCL单例写法(没有volatile):

代码语言:javascript
复制
package com.jam.demo;
/**
 * 错误的DCL单例示例(无volatile)
 * @author ken
 * @date 2024
 */
publicclass WrongDclSingleton {
    privatestatic WrongDclSingleton INSTANCE;

    private WrongDclSingleton() {}

    public static WrongDclSingleton getInstance() {
        if (INSTANCE == null) { // 第一次检查
            synchronized (WrongDclSingleton.class) { // 加锁
                if (INSTANCE == null) { // 第二次检查
                    INSTANCE = new WrongDclSingleton(); // 问题根源
                }
            }
        }
        return INSTANCE;
    }
}

问题根源INSTANCE = new WrongDclSingleton()这个操作,在底层会分为三个步骤:

  1. 分配对象的内存空间
  2. 初始化对象(执行构造函数)
  3. 将INSTANCE引用指向分配的内存地址

这三个步骤中,2和3没有数据依赖,完全可能被重排,先执行3,再执行2。此时如果有另一个线程来调用getInstance(),会看到INSTANCE不为null,但对象还没有完成初始化,拿到了一个半初始化的对象,使用时就会出现空指针等异常。

正确的DCL单例写法(加volatile)

代码语言:javascript
复制
package com.jam.demo;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
 * 正确的DCL单例示例(带volatile)
 * @author ken
 * @date 2024
 */
@Tag(name = "DCL单例", description = "正确的双重检查锁单例实现")
publicclass CorrectDclSingleton {
    /** 单例实例,添加volatile修饰,禁止指令重排 */
    privatestaticvolatile CorrectDclSingleton INSTANCE;

    /** 私有构造函数,防止外部实例化 */
    private CorrectDclSingleton() {}

    /**
     * 获取单例实例
     * @return 单例对象
     */
    public static CorrectDclSingleton getInstance() {
        // 第一次检查,无锁,提高性能
        if (INSTANCE == null) {
            // 加锁,保证原子性
            synchronized (CorrectDclSingleton.class) {
                // 第二次检查,防止多线程同时进入第一次检查后重复实例化
                if (INSTANCE == null) {
                    INSTANCE = new CorrectDclSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

用先行发生原则解释正确性

  1. volatile变量规则:对INSTANCE的volatile写操作(初始化赋值),happens-before后续对INSTANCE的volatile读操作(第一次null检查)
  2. 程序顺序规则:对象初始化的所有操作,happens-before对INSTANCE的volatile写操作
  3. 传递性规则:对象的初始化完成,happens-before后续对INSTANCE的读操作

因此,其他线程读取INSTANCE时,要么看到null,要么看到完全初始化完成的对象,绝对不会看到半初始化的对象,完美解决了重排问题。

案例3:可见性问题的经典复现与解决

我们再看一个经典的可见性问题示例,用先行发生原则解释为什么加volatile就能解决:

代码语言:javascript
复制
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
 * 可见性问题复现与解决示例
 * @author ken
 * @date 2024
 */
@Slf4j
publicclass VisibilityDemo {
    /** 停止标记,添加volatile修饰保证可见性 */
    privatestaticvolatileboolean stop = false;

    public static void main(String[] args) throws InterruptedException {
        Thread workerThread = new Thread(() -> {
            int i = 0;
            // 读取stop变量,volatile读保证能看到主线程的修改
            while (!stop) {
                i++;
            }
            log.info("工作线程停止,循环次数:{}", i);
        });

        workerThread.start();
        // 主线程休眠1秒,让工作线程充分运行
        Thread.sleep(1000);
        // 修改stop变量,volatile写保证对工作线程可见
        stop = true;
        log.info("主线程已设置stop为true,等待工作线程停止");
        workerThread.join();
    }
}

原理说明

  • 如果stop没有volatile修饰,主线程修改了stop的值,工作线程没有任何先行发生规则的约束,看不到这个修改,JIT会把while(!stop)优化成while(true),工作线程永远不会停止
  • 加了volatile修饰后,volatile变量规则保证:主线程对stop的写操作,happens-before工作线程对stop的读操作,因此工作线程能立即看到stop的修改,正常停止

六、JMM开发最佳实践

  1. 优先使用JUC成熟工具类:优先使用Atomic原子类、ConcurrentHashMap、ReentrantLock等JUC包下的成熟工具,这些类已经完美遵循JMM规范,封装了底层的内存屏障和同步逻辑,不要自己手写volatile和同步代码
  2. 不要滥用volatile:volatile只能保证可见性和有序性,不能保证原子性,只有在符合以下场景时才使用:
    • 状态标记变量(如上面的stop、flag)
    • 双重检查锁单例
    • 一次性的安全发布对象
  3. 共享变量必须有先行发生规则约束:多线程操作的共享变量,必须有对应的先行发生规则保障,否则一定会出现可见性、有序性问题
  4. 不要依赖平台特性:x86是强内存模型,重排发生的概率低,而ARM是弱内存模型,重排非常频繁,不要依赖特定平台的特性,必须严格遵循JMM规范,才能保证跨平台的并发正确性
  5. 不要试图通过关闭优化解决问题:不要通过关闭JIT编译、禁用指令重排等方式解决并发问题,只有遵循JMM规范的代码,才是通用、稳定、可维护的

七、总结

JMM的三大核心,本质上是一套从底层到上层的完整规范体系:

  1. 指令重排是CPU和编译器为了提升性能的优化手段,单线程下完全安全,但多线程下会带来有序性和可见性问题
  2. 内存屏障是解决重排和可见性问题的底层实现,通过禁止重排和强制刷新缓存,保证多线程下的内存一致性
  3. 先行发生原则是JMM给开发者提供的上层规范,是判断并发操作是否安全的唯一依据,开发者无需关心底层实现,只要遵循这套规则,就能写出线程安全的并发代码

并发编程的本质,就是在性能和线程安全之间找到平衡,而JMM就是这个平衡的核心标尺。只有彻底搞懂JMM的三大核心,你才能从根上理解并发编程的本质,彻底解决那些偶发的、难以复现的并发诡异问题。


附录:项目依赖pom.xml

代码语言:javascript
复制
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.jam.demo</groupId>
    <artifactId>jmm-demo</artifactId>
    <version>1.0.0</version>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <spring.version>6.1.5</spring.version>
        <lombok.version>1.18.32</lombok.version>
        <guava.version>33.1.0-jre</guava.version>
        <fastjson2.version>2.0.49</fastjson2.version>
        <mybatis-plus.version>3.5.6</mybatis-plus.version>
        <swagger.version>2.5.0</swagger.version>
    </properties>
    <dependencies>
        <!-- Spring核心工具类 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Lombok -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>${lombok.version}</version>
            <scope>provided</scope>
        </dependency>
        <!-- Guava集合工具类 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>${guava.version}</version>
        </dependency>
        <!-- Fastjson2 -->
        <dependency>
            <groupId>com.alibaba.fastjson2</groupId>
            <artifactId>fastjson2</artifactId>
            <version>${fastjson2.version}</version>
        </dependency>
        <!-- MyBatis-Plus -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>${mybatis-plus.version}</version>
        </dependency>
        <!-- Swagger3 -->
        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>${swagger.version}</version>
        </dependency>
        <!-- 日志依赖 -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <version>2.0.12</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.5.3</version>
        </dependency>
    </dependencies>
</project>
本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-03-10,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 果酱带你啃java 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、为什么你必须搞懂JMM?
  • 二、JMM要解决的三大核心问题
  • 三、核心一:指令重排——性能优化的双刃剑
    • 3.1 什么是指令重排?
    • 3.2 指令重排的分类与约束
      • 3.2.1 不可突破的底线:as-if-serial语义
      • 3.2.2 多线程下的致命漏洞
    • 3.3 控制依赖的重排陷阱
  • 四、核心二:内存屏障——重排与可见性的底层解药
    • 4.1 什么是内存屏障?
    • 4.2 JMM定义的四大标准内存屏障
    • 4.3 内存屏障的实际应用:volatile的底层实现
    • 4.4 其他场景的内存屏障应用
    • 4.5 JDK9+新特性:VarHandle灵活控制内存屏障
  • 五、核心三:先行发生原则——判断并发安全的唯一依据
    • 5.1 先行发生原则的权威定义
      • 必须纠正的致命误区
    • 5.2 JMM内置的8大天然先行发生规则
      • 1. 程序顺序规则
      • 2. 管程锁定规则
      • 3. volatile变量规则
      • 4. 线程启动规则
      • 5. 线程终止规则
      • 6. 线程中断规则
      • 7. 对象终结规则
      • 8. 传递性规则
    • 5.3 先行发生原则实战应用
      • 案例1:用先行发生原则解释volatile解决重排问题
      • 案例2:经典的DCL双重检查锁单例,为什么必须加volatile?
      • 案例3:可见性问题的经典复现与解决
  • 六、JMM开发最佳实践
  • 七、总结
    • 附录:项目依赖pom.xml
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档