别再只会用 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=1flag=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=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修饰,就能彻底禁止重排。 修改后的核心代码:

/** 标记变量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相同的效果,同时支持更细粒度的内存屏障控制:

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>

目录
相关文章
|
22天前
|
SQL 运维 监控
MySQL高可用生产落地全解:主从同步、MGR集群、读写分离从原理到实战
本文系统讲解MySQL高可用三大核心:主从复制(含GTID、增强半同步实战)、MGR原生集群(单主模式部署、自动选主、脑裂防护)及读写分离(应用层/ProxySQL方案)。涵盖RTO/RPO指标、故障根因分析、全场景最佳实践与容灾预案,助你构建稳定、高性能、可扩展的生产级高可用体系。
157 3
|
29天前
|
安全 应用服务中间件 nginx
Docker 命令大全:从入门到生产,全场景核心指令全覆盖
本文系统讲解Docker核心原理与实战命令,涵盖Namespace、CGroup、UnionFS三大底层机制,详解镜像构建/拉取/安全扫描、容器全生命周期管理、数据卷与网络配置、Compose编排及生产最佳实践,内容全面、示例丰富、贴合v27.0.3最新版。
776 2
|
24天前
|
Arthas 运维 监控
线上 JVM 故障秒解:Arthas 高阶用法与全链路定位实战指南
本文介绍阿里巴巴开源的Java诊断工具Arthas在线上JVM故障排查中的核心应用。针对CPU飙高、FullGC频繁、接口超时等常见问题,Arthas提供无需重启服务的热修复能力,包括方法热替换(trace/watch/tt命令)、线程问题定位(thread命令)、内存分析(heapdump)等核心功能。文章通过真实案例演示全链路排查流程,并给出安全使用建议,帮助开发者快速定位和解决线上问题,实现从被动救火到主动定位的转变。Arthas的字节码增强技术可实时监控JVM状态,是提升线上问题排查效率的利器。
294 1
|
24天前
|
存储 缓存 监控
90% 的 JVM 元空间 OOM,都栽在类加载与类卸载上!调优 + 排查全攻略
本文深入剖析JVM元空间OOM、频繁Full GC等生产问题,聚焦类加载机制、元空间内存模型与类卸载三要素,详解自定义类加载器泄漏、线程上下文类加载器未恢复等5大高频场景,提供参数调优、日志观测、MAT排查及修复实战方案。
510 3
|
24天前
|
缓存 安全 Java
从入门到入魂:Lock 体系与 AQS 队列同步器源码级深度剖析
本文深度解析Java并发核心——Lock体系与AQS(AbstractQueuedSynchronizer)。涵盖Lock接口契约、与synchronized的本质差异、ReentrantLock/ReadWriteLock/StampedLock实现原理;透彻剖析AQS的state同步状态、CLH队列、Node节点、模板方法及acquire/release源码;辅以生产者消费者、自定义共享锁等实战示例,助你打通并发编程任督二脉。
254 2
|
负载均衡 安全 Java
深入了解Spring Cloud Gateway:构建高效微服务网关
Spring Cloud Gateway是一个强大的微服务网关,它在现代分布式架构中扮演着至关重要的角色。本文将深入介绍Spring Cloud Gateway的核心概念、功能和用途,以帮助您更好地理解和利用这一工具来构建高效的微服务应用。
|
1月前
|
SQL 缓存 安全
别再只会用 synchronized!Java 并发编程全链路核心体系,从底层原理到生产实战全覆盖
本文深入解析Java并发编程核心知识,基于JDK17从底层原理到生产实践全面讲解。首先剖析JMM内存模型与三大特性(原子性、可见性、有序性),详解synchronized、ReentrantLock等锁机制及AQS实现原理。然后介绍JUC工具类(原子类、并发容器、线程池、同步工具)的正确使用方式。重点通过商品库存扣减案例,对比悲观锁、乐观锁、SQL原子操作三种方案解决超卖问题。最后总结常见坑点(死锁、线程池误用等)和线上问题排查方法,强调理解底层原理而非死记API的重要性,帮助开发者真正掌握并发编程精髓。
162 4
|
1月前
|
API
开源剪映小助手(capcut-mate)v3.0.22发布
CapCut Mate API 是开源免费的剪映草稿自动化工具,基于 FastAPI,支持独立部署。为大模型提供开箱即用的视频编辑 Skills,实现剪映核心功能全流程自动化,轻松生成专业视频。兼容 Coze/n8n,支持云渲染。
|
消息中间件 算法 Java
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!
1285 1
弥补延时消息的不足,RocketMQ 基于时间轮算法实现了定时消息!
|
4月前
|
存储 缓存 监控
从GC日志小白到分析大神:GCEasy实战全攻略
GCEasy是Java GC日志分析利器,支持多种垃圾收集器,通过可视化报表与智能诊断,帮助开发者快速定位内存泄漏、GC频繁等问题。本文结合实战案例,详解其原理、使用方法及性能优化策略,提升系统稳定性与并发能力。
700 1

热门文章

最新文章