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

简介: 本文深入解析Java内存模型(JMM)的核心机制,揭示并发编程中90%的诡异BUG根源。JMM通过三大核心机制解决并发问题:1)指令重排是性能优化的双刃剑,多线程下会破坏有序性;2)内存屏障通过禁止重排和强制刷新缓存保证内存一致性;3)先行发生原则提供上层规范,包括8大规则确保线程安全。文章通过DCL单例、可见性问题等典型案例,详细演示volatile、synchronized等关键字的底层实现原理,并给出JMM开发最佳实践:优先使用JUC工具类、正确使用volatile、严格遵循先行发生规则。
❝

本文基于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不会对存在数据依赖的指令进行重排。

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

int a = 1;
int b = a + 1;

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

int a = 1;
int b = 2;

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

3.2.2 多线程下的致命漏洞

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

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

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
public class ReorderDemo {
   /** 共享变量a */
   private static int a = 0;
   /** 标记变量flag */
   private static boolean flag = false;
   /** 统计重排发生的次数 */
   private static final AtomicInteger REORDER_COUNT = new AtomicInteger(0);
   /** 循环执行次数,提高重排发生概率 */
   private static final int 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=1和flag=true没有数据依赖,可能被重排为先执行flag=true,再执行a=1
  4. 此时读线程看到flag=true,但a还没被赋值,就会出现a=0的情况,也就是重排发生了

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

3.3 控制依赖的重排陷阱

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

int a = 0;
boolean flag = false;

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

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

写线程的a=1和flag=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修饰,就能彻底禁止重排。 修改后的核心代码:

/** 标记变量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. synchronized:monitorenter(加锁)相当于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相同的效果,同时支持更细粒度的内存屏障控制:

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
public class VarHandleDemo {
   private int a = 0;
   private boolean flag = false;
   private static final VarHandle A_HANDLE;
   private static final VarHandle FLAG_HANDLE;
   private static final int LOOP_COUNT = 100000;
   private static final 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) {
           throw new 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):

package com.jam.demo;
/**
* 错误的DCL单例示例(无volatile)
* @author ken
* @date 2024
*/

public class WrongDclSingleton {
   private static 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):

package com.jam.demo;
import io.swagger.v3.oas.annotations.tags.Tag;
/**
* 正确的DCL单例示例(带volatile)
* @author ken
* @date 2024
*/

@Tag(name = "DCL单例", description = "正确的双重检查锁单例实现")
public class CorrectDclSingleton {
   /** 单例实例,添加volatile修饰,禁止指令重排 */
   private static volatile 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就能解决:

package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 可见性问题复现与解决示例
* @author ken
* @date 2024
*/

@Slf4j
public class VisibilityDemo {
   /** 停止标记,添加volatile修饰保证可见性 */
   private static volatile boolean 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)
  • 双重检查锁单例
  • 一次性的安全发布对象
  1. 共享变量必须有先行发生规则约束:多线程操作的共享变量,必须有对应的先行发生规则保障,否则一定会出现可见性、有序性问题
  2. 不要依赖平台特性:x86是强内存模型,重排发生的概率低,而ARM是弱内存模型,重排非常频繁,不要依赖特定平台的特性,必须严格遵循JMM规范,才能保证跨平台的并发正确性
  3. 不要试图通过关闭优化解决问题:不要通过关闭JIT编译、禁用指令重排等方式解决并发问题,只有遵循JMM规范的代码,才是通用、稳定、可维护的

七、总结

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

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

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


附录:项目依赖pom.xml

<?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>

目录
相关文章
|
6月前
|
缓存 安全 Java
从入门到入魂:Lock 体系与 AQS 队列同步器源码级深度剖析
本文深度解析Java并发核心——Lock体系与AQS(AbstractQueuedSynchronizer)。涵盖Lock接口契约、与synchronized的本质差异、ReentrantLock/ReadWriteLock/StampedLock实现原理;透彻剖析AQS的state同步状态、CLH队列、Node节点、模板方法及acquire/release源码;辅以生产者消费者、自定义共享锁等实战示例,助你打通并发编程任督二脉。
666 2
|
6月前
|
SQL 缓存 安全
别再只会用 synchronized!Java 并发编程全链路核心体系,从底层原理到生产实战全覆盖
本文深入解析Java并发编程核心知识,基于JDK17从底层原理到生产实践全面讲解。首先剖析JMM内存模型与三大特性(原子性、可见性、有序性),详解synchronized、ReentrantLock等锁机制及AQS实现原理。然后介绍JUC工具类(原子类、并发容器、线程池、同步工具)的正确使用方式。重点通过商品库存扣减案例,对比悲观锁、乐观锁、SQL原子操作三种方案解决超卖问题。最后总结常见坑点(死锁、线程池误用等)和线上问题排查方法,强调理解底层原理而非死记API的重要性,帮助开发者真正掌握并发编程精髓。
408 3
|
3月前
|
存储 算法 Java
【JVM虚拟机】垃圾回收GC:垃圾收集器:G1:Region分区、Mixed GC、回收流程、适用场景(高频)(附《思维导图》+《面试高频考点清单》)
G1是JDK 9起默认的低延迟垃圾收集器,将堆划分为2048个可动态分配角色的Region,通过Mixed GC优先回收垃圾最多的区域,结合Remembered Set与SATB算法,在大堆(≥4GB)场景下实现可预测停顿(如≤200ms)与高吞吐平衡。
|
3月前
|
存储 缓存 算法
【JVM虚拟机】垃圾回收GC:垃圾判定算法:引用计数法、可达性分析算法(附《思维导图》+《面试高频考点清单》)
JVM垃圾判定是GC第一步,核心是识别“死亡”对象。主流采用**可达性分析算法**(以GC Roots为起点,不可达即回收),彻底解决引用计数法致命的**循环引用问题**。全文系统梳理原理、GC Roots类型、四种引用及面试高频考点。
|
6月前
|
缓存 Java 数据库
【Spring Boot】Spring Boot 全体系知识结构化拆解(附 Spring Boot 高频面试八股文精简版)
Spring Boot 是 Pivotal 基于 Spring 的“约定大于配置”快速开发框架,简化初始搭建与开发,无缝整合 Spring 全生态,内嵌容器、自动配置、起步依赖开箱即用,是 Java 企业级应用与微服务架构的核心基石。
1890 8
|
3月前
|
存储 算法 Java
【JVM虚拟机】垃圾回收GC:垃圾收集器:ZGC/Shenandoah:核心原理、低延迟特性、JDK21分代ZGC优化(2026超高频)(附《思维导图》+《面试高频考点清单》)
本文系统梳理ZGC与Shenandoah两大低延迟GC器的核心原理(着色指针/ Brooks指针、读屏障)、分代优化(JDK21+)、性能对比及调优实践,覆盖2026年面试超高频考点,助力秒答原理、设计与选型问题。
|
4月前
|
SQL 存储 关系型数据库
【MySQL】《MySQL索引 与 慢SQL优化 面试问答清单》(附《思维导图》)
本文是面向MySQL开发者与DBA的索引与慢SQL优化实战指南,涵盖B+树原理、聚簇/覆盖索引、EXPLAIN深度解读、四大阶段优化(发现→分析→索引→重构)及电商订单/多表联查真实案例,含可打印速查表,助你将慢查询从3秒优化至0.005秒。
【MySQL】《MySQL索引 与 慢SQL优化 面试问答清单》(附《思维导图》)
|
4月前
|
算法 关系型数据库 MySQL
【MySQL】《MySQL海量数据处理:面试核心考点问答清单》(附:《一页纸速记版》+《20道模拟面试口述题》)
《MySQL海量数据处理:面试核心考点问答清单》聚焦单库瓶颈、读写分离、分库分表、分片策略、分布式事务等八大主题,涵盖演进路线、原理对比、避坑指南与Sharding-JDBC实战,助力候选人系统掌握高并发场景下的架构设计与面试应答要点。
|
6月前
|
Java 数据库 数据格式
【Spring注解】Spring生态常见注解——面试高频考点总结
本文系统梳理Spring生态高频面试注解考点,直击本质——注解只是表象,核心考察IOC容器、Bean生命周期、MVC流程、AOP代理、自动配置与事务机制等底层原理。涵盖@Component/@Configuration、@Autowired/@Resource、@RestController、@Transactional、@SpringBootApplication、@RequestBody等七大类注解的辨析、原理、坑点 及 TOP10 必背题。
833 12
|
6月前
|
人工智能 Cloud Native 安全
AWS Bedrock托管Claude 4.6的工程实践与合规思考
近期AWS Bedrock集成Claude 4.6引发热议。该架构以VPC内数据隔离、云原生无缝集成及Firecracker微虚拟机硬隔离为核心,兼顾合规(SOC2/GDPR)、安全与工程效率。国内企业出海需关注主体资质、模型白名单申请及跨境网络优化。

热门文章

最新文章