引言
在Java并发编程中,volatile是最基础也最容易被误解的关键字。很多开发者只知道它能解决多线程的可见性问题,却对它的禁止指令重排序语义一知半解,甚至在双重检查锁单例中盲目使用,最终埋下线上空指针、数据错乱的隐患。本文将从Java内存模型的底层原理出发,一步步拆解volatile的核心语义,用通俗的语言讲透内存屏障的实现机制,再结合单例模式的架构演进,手把手教你写出工业级的线程安全单例,同时梳理常见误区与最佳实践,让你彻底吃透volatile关键字。
一、并发编程的三大核心问题与Java内存模型
1.1 可见性:CPU缓存与线程工作内存的博弈
在现代计算机架构中,CPU为了提升执行效率,并不会直接与主内存交互,而是通过多层高速缓存完成数据读写。多核心场景下,不同CPU核心的缓存之间会出现数据不一致的问题,这就是硬件层面的可见性问题。
Java内存模型(JMM)是Java语言规范定义的一套内存访问规则,用于屏蔽不同硬件和操作系统的内存访问差异,解决多线程场景下的内存一致性问题。JMM规定:
- 所有共享变量都存储在主内存中
- 每个线程拥有独立的工作内存,保存了该线程使用到的共享变量的主内存副本
- 线程对变量的所有读写操作都必须在工作内存中完成,不能直接操作主内存
- 不同线程之间无法直接访问对方的工作内存,变量值的传递必须通过主内存完成
这种模型带来了天然的可见性问题:当线程A修改了工作内存中的变量副本,若未及时刷新到主内存,其他线程无法感知到变量的变化。下面的示例可以直观复现这个问题:
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StopWatch;
@Slf4j
public class VolatileVisibilityDemo {
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Thread readerThread = new Thread(() -> {
log.info("读线程启动,等待flag变为true");
while (!flag) {
}
log.info("读线程检测到flag变为true,执行结束");
}, "reader-thread");
Thread writerThread = new Thread(() -> {
log.info("写线程启动,3秒后修改flag");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("写线程被中断", e);
}
flag = true;
log.info("写线程已将flag修改为true");
}, "writer-thread");
readerThread.start();
writerThread.start();
readerThread.join();
stopWatch.stop();
log.info("程序执行总耗时:{}ms", stopWatch.getTotalTimeMillis());
}
}
上述代码中,未给flag添加volatile修饰时,读线程会永久陷入死循环。原因是JIT编译器会对代码做优化,将flag变量的值缓存在寄存器中,不会重新从主内存读取最新值,导致写线程的修改对读线程完全不可见。
1.2 原子性:复合操作的线程安全陷阱
原子性指的是一个操作是不可分割的,要么全部执行完成,要么完全不执行,执行过程中不会被其他线程中断。
Java中,对基础类型变量的单次读/写操作是原子性的,但复合操作不具备原子性,例如常见的count++自增操作,在字节码层面会被拆分为三个步骤:
- 从主内存读取count的当前值到工作内存
- 在执行引擎中对count值加1
- 将计算后的新值写回工作内存,并刷新到主内存
多线程场景下,多个线程同时执行自增操作时,会出现指令交错,导致最终结果不符合预期。下面的示例可以验证这一点:
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
@Slf4j
public class VolatileAtomicityDemo {
private static volatile int count = 0;
private static final int THREAD_COUNT = 10;
private static final int INCREMENT_COUNT = 1000;
private static final CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
for (int j = 0; j < INCREMENT_COUNT; j++) {
count++;
}
countDownLatch.countDown();
}, "increment-thread-" + i).start();
}
countDownLatch.await();
log.info("预期结果:{},实际结果:{}", THREAD_COUNT * INCREMENT_COUNT, count);
}
}
上述代码中,即使给count添加了volatile修饰,最终的执行结果依然大概率小于预期的10000。这直接证明了volatile无法保证复合操作的原子性,这是开发者最容易踩的误区之一。
1.3 有序性:指令重排序与as-if-serial语义
为了提升程序执行性能,编译器和CPU会对指令序列进行重排序,分为三类:
- 编译器优化重排序:编译器在不改变单线程程序语义的前提下,重新安排语句的执行顺序
- 指令级并行重排序:CPU将多条指令重叠执行,在不改变单线程执行结果的前提下调整指令执行顺序
- 内存系统重排序:CPU缓存和主内存的读写缓冲,导致加载和存储操作看上去可能是乱序执行
JMM通过as-if-serial语义约束重排序行为:不管怎么重排序,单线程程序的执行结果不能被改变。编译器、CPU都必须遵守as-if-serial语义,也就是说,对存在数据依赖的操作,不会进行重排序。
但as-if-serial语义仅对单线程有效,多线程场景下,指令重排序会导致线程安全问题。下面的示例可以复现重排序带来的异常:
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ReorderDemo {
private static int a = 0;
private static int b = 0;
private static boolean flag = false;
public static void main(String[] args) throws InterruptedException {
int errorCount = 0;
for (int i = 0; i < 10000; i++) {
// 重置变量
a = 0;
b = 0;
flag = false;
Thread thread1 = new Thread(() -> {
a = 1;
flag = true;
});
Thread thread2 = new Thread(() -> {
if (flag) {
b = a;
}
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
if (b == 0 && flag) {
errorCount++;
log.error("第{}次执行出现重排序,b=0,flag=true", i);
}
}
log.info("10000次执行中,重排序出现次数:{}", errorCount);
}
}
上述代码中,单线程视角下,a=1和flag=true的执行顺序不会影响结果,编译器和CPU可能会将两者重排序。当flag=true先于a=1执行时,thread2会先读取到flag为true,此时a还未被赋值,最终b=0,出现不符合预期的结果。
1.4 happens-before规则:并发编程的可见性保证
JMM通过happens-before规则向开发者提供跨线程的可见性保证,无需了解底层重排序规则,只需遵守happens-before规则,就能保证多线程场景下的可见性。
核心happens-before规则如下:
- 程序顺序规则:一个线程内,按照代码执行顺序,前面的操作happens-before于后面的任意操作
- 监视器锁规则:对一个锁的解锁操作,happens-before于后续对这个锁的加锁操作
- volatile变量规则:对一个volatile变量的写操作,happens-before于后续任意对这个volatile变量的读操作
- 线程启动规则:Thread对象的start()方法调用,happens-before于该线程内的任意操作
- 线程终止规则:线程内的所有操作,都happens-before于其他线程检测到该线程终止
- 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C
其中,volatile变量规则是理解volatile语义的核心,结合传递性规则,volatile不仅能保证自身变量的可见性,还能实现更强大的跨线程内存可见性保证。
二、volatile关键字的核心语义与底层实现
2.1 语义一:保证多线程间的变量可见性
volatile的第一个核心语义是保证共享变量的多线程可见性:
- 当一个线程修改了volatile变量的值,会立即将最新值强制刷新到主内存
- 当其他线程读取volatile变量时,会强制将工作内存中的变量副本置为无效,重新从主内存读取最新值
回到1.1中的可见性示例,给flag变量添加volatile修饰后,写线程修改flag后会立即刷新到主内存,读线程每次循环都会从主内存读取最新的flag值,3秒后会正常退出循环,不会出现死循环问题。
同时,Java语言规范明确规定:volatile修饰的long和double类型变量,即使在32位JVM中,也保证单次读/写操作的原子性,解决了32位JVM中long/double变量的非原子性读写问题。
2.2 语义二:禁止指令重排序
volatile的第二个核心语义是禁止指令重排序,通过内存屏障限制编译器和CPU的重排序行为,具体规则如下:
- 当程序执行到volatile变量的读操作时,读操作之前的所有操作必须已经执行完成,且结果对后续操作可见;读操作之后的操作不能被重排序到读操作之前
- 当程序执行到volatile变量的写操作时,写操作之前的所有操作必须已经执行完成,且结果对后续操作可见;写操作之后的操作不能被重排序到写操作之前
- 禁止两个volatile变量之间的读写操作发生重排序
结合happens-before的传递性规则,volatile的禁止重排序语义实现了更强大的内存可见性:线程A在写volatile变量之前的所有普通变量写操作,都会随着volatile变量的写操作一起刷新到主内存;线程B在读volatile变量之后,会将工作内存中的普通变量副本置为无效,重新从主内存读取最新值,也就是说,线程A写volatile之前的所有操作,对线程B读volatile之后的所有操作都是可见的。
2.3 底层实现:内存屏障与MESI缓存一致性协议
JVM通过内存屏障(Memory Barrier)指令实现volatile的可见性和禁止重排序语义。内存屏障是一组CPU指令,用于控制特定操作的执行顺序和内存可见性。
JMM定义了四类内存屏障:
| 屏障类型 | 指令示例 | 功能说明 |
| LoadLoad | Load1;LoadLoad;Load2 | 保证Load1的读取操作先于Load2及后续所有读取操作执行 |
| StoreStore | Store1;StoreStore;Store2 | 保证Store1的写入操作先于Store2及后续所有写入操作执行,刷新到主内存 |
| LoadStore | Load1;LoadStore;Store2 | 保证Load1的读取操作先于Store2及后续所有写入操作执行 |
| StoreLoad | Store1;StoreLoad;Load2 | 保证Store1的写入操作先于Load2及后续所有读取操作执行,刷新到主内存 |
为了实现volatile的完整语义,JVM在编译期会按照如下策略插入内存屏障:
- 在每个volatile写操作前,插入StoreStore屏障
- 在每个volatile写操作后,插入StoreLoad屏障
- 在每个volatile读操作后,插入LoadLoad屏障
- 在每个volatile读操作后,插入LoadStore屏障
而volatile的可见性,底层依赖于CPU的MESI缓存一致性协议:
- M(Modified):缓存行被修改,与主内存数据不一致
- E(Exclusive):缓存行独占,与主内存数据一致
- S(Shared):缓存行被多个CPU共享,与主内存数据一致
- I(Invalid):缓存行失效,不可用
当CPU修改了volatile变量所在的缓存行,会将该缓存行标记为Modified状态,并通过总线嗅探机制通知其他CPU,将对应缓存行标记为Invalid状态。其他CPU需要读取该变量时,发现缓存行已失效,会强制从主内存重新加载最新的缓存行,从而保证多线程间的变量可见性。
2.4 JSR-133对volatile内存语义的增强
在JDK1.5之前,volatile虽然能保证可见性,但无法完全禁止指令重排序,导致双重检查锁单例存在线程安全问题。JDK1.5版本通过JSR-133规范修复了这一问题,增强了volatile的内存语义,完善了happens-before规则,明确了volatile变量的写/读可以实现跨线程的内存可见性传递,彻底解决了volatile禁止重排序语义的缺陷。
三、单例模式的架构演进与volatile的核心作用
3.1 单例模式的核心设计原则
单例模式是创建型设计模式中最常用的模式之一,核心设计原则是:保证一个类在任何场景下都只有一个实例,并提供一个全局唯一的访问入口。
一个合格的工业级单例实现,需要满足以下要求:
- 线程安全:多线程场景下不会创建多个实例
- 懒加载:只有在第一次使用时才创建实例,避免资源浪费
- 高性能:获取实例的操作不需要频繁加锁,性能损耗低
- 安全防护:防止反射、序列化等方式破坏单例
3.2 饿汉式单例:类加载即初始化的实现
饿汉式单例是最简单的单例实现,在类加载的初始化阶段就完成实例的创建,基于JVM的类加载机制保证线程安全。
package com.jam.demo;
public class EagerSingleton {
private static final EagerSingleton INSTANCE = new EagerSingleton();
private EagerSingleton() {
if (INSTANCE != null) {
throw new IllegalStateException("单例类禁止重复实例化");
}
}
public static EagerSingleton getInstance() {
return INSTANCE;
}
}
优点:实现简单,类加载时完成实例初始化,无线程安全问题,获取实例的性能极高。缺点:无法实现懒加载,类加载时就会创建实例,若实例初始化耗时较长,会增加类加载的时间;若实例始终未被使用,会造成内存资源的浪费。
3.3 懒汉式单例:懒加载的演进与线程安全问题
懒汉式单例实现了懒加载,只有在第一次调用获取实例的方法时,才会创建实例。
非线程安全的懒汉式实现
package com.jam.demo;
public class LazyUnsafeSingleton {
private static LazyUnsafeSingleton instance;
private LazyUnsafeSingleton() {
}
public static LazyUnsafeSingleton getInstance() {
if (instance == null) {
instance = new LazyUnsafeSingleton();
}
return instance;
}
}
该实现仅适用于单线程场景,多线程环境下,多个线程同时判断instance为null时,会同时进入实例化逻辑,创建多个实例,破坏单例的核心原则。
同步方法的线程安全懒汉式实现
package com.jam.demo;
public class LazySyncSingleton {
private static LazySyncSingleton instance;
private LazySyncSingleton() {
}
public static synchronized LazySyncSingleton getInstance() {
if (instance == null) {
instance = new LazySyncSingleton();
}
return instance;
}
}
通过给getInstance方法添加synchronized修饰,保证同一时间只有一个线程能进入该方法,解决了多线程安全问题。但该实现的缺陷非常明显:每次获取实例都需要加锁,即使实例已经创建完成,依然会进行锁竞争,带来极大的性能损耗,不适用于高并发场景。
3.4 双重检查锁(DCL)的致命缺陷:指令重排序的坑
为了解决同步方法的性能问题,开发者提出了双重检查锁(Double Check Lock,DCL)的实现,在保证线程安全的同时,大幅提升获取实例的性能。
package com.jam.demo;
public class DclUnsafeSingleton {
private static DclUnsafeSingleton instance;
private DclUnsafeSingleton() {
}
public static DclUnsafeSingleton getInstance() {
if (instance == null) {
synchronized (DclUnsafeSingleton.class) {
if (instance == null) {
instance = new DclUnsafeSingleton();
}
}
}
return instance;
}
}
DCL的执行流程如下:
该实现通过两次null值检查,只有在实例未创建时才会加锁,实例创建完成后,后续获取实例的操作无需加锁,直接返回实例,性能大幅提升。
但这个看似完美的实现,存在一个致命的线程安全缺陷,根源就是指令重排序。
instance = new DclUnsafeSingleton()实例化对象的操作,在字节码层面会被拆分为三个步骤:
- 分配对象所需的内存空间
- 初始化对象,执行构造方法中的初始化逻辑
- 将instance引用指向分配的内存地址,此时instance不再为null
编译器和CPU为了提升性能,可能会对步骤2和步骤3进行重排序,执行顺序变为1->3->2。在单线程场景下,as-if-serial语义保证重排序不会影响执行结果,不会出现问题。
但多线程场景下,会出现如下异常执行流程:
- 线程A调用getInstance方法,判断instance为null,进入同步代码块
- 线程A执行
instance = new DclUnsafeSingleton(),先分配内存空间,然后将instance引用指向分配的内存地址,此时instance已经不为null,但对象还未完成初始化 - 线程B调用getInstance方法,第一次检查instance不为null,直接返回未完成初始化的instance实例
- 线程B使用该实例时,会访问到未初始化的成员变量,触发空指针异常,导致程序崩溃
这就是DCL实现中最隐蔽的坑,在高并发场景下,这个问题会被放大,造成线上故障。
3.5 volatile如何修复DCL单例的线程安全问题
要修复DCL单例的缺陷,核心是禁止对象初始化和instance引用赋值之间的指令重排序,而volatile的禁止重排序语义,正好可以解决这个问题。
给instance变量添加volatile修饰后,编译器会插入对应的内存屏障,禁止步骤2和步骤3的重排序,保证对象的初始化操作必须在instance引用赋值之前完成。同时,结合volatile的可见性语义,当instance实例创建完成后,其他线程能立即感知到,不会读取到未初始化的半熟对象。
DCL单例正确实现如下:
package com.jam.demo;
public class DclSingleton {
private static volatile DclSingleton instance;
private DclSingleton() {
if (instance != null) {
throw new IllegalStateException("单例类禁止重复实例化");
}
}
public static DclSingleton getInstance() {
if (instance == null) {
synchronized (DclSingleton.class) {
if (instance == null) {
instance = new DclSingleton();
}
}
}
return instance;
}
}
该实现完全满足工业级单例的所有要求:
- 线程安全:volatile禁止重排序,synchronized保证实例化的原子性,多线程场景下不会创建多个实例
- 懒加载:只有第一次调用getInstance方法时才会创建实例,不会浪费内存资源
- 高性能:实例创建完成后,后续获取实例无需加锁,仅需一次volatile读操作,性能损耗极低
- 安全防护:私有构造方法中添加了实例校验,防止反射破坏单例
3.6 其他工业级单例实现的对比与选型
静态内部类单例
静态内部类单例基于JVM的类加载机制实现,同样能保证线程安全和懒加载。
package com.jam.demo;
public class StaticInnerClassSingleton {
private StaticInnerClassSingleton() {
if (SingletonHolder.INSTANCE != null) {
throw new IllegalStateException("单例类禁止重复实例化");
}
}
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
private static class SingletonHolder {
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
}
该实现的核心原理是:静态内部类SingletonHolder只有在第一次调用getInstance方法时才会被加载和初始化,INSTANCE实例的创建在类初始化阶段完成,JVM的类初始化锁保证同一时间只有一个线程能执行类的初始化,不会出现线程安全问题。同时,该实现无需添加volatile修饰,因为JVM会保证类初始化过程中的指令重排序对其他线程不可见。
枚举单例
枚举单例是《Effective Java》中推荐的单例实现,也是目前最安全的单例实现方式。
package com.jam.demo;
public enum EnumSingleton {
INSTANCE;
public void doSomething() {
// 业务逻辑实现
}
}
枚举单例的优势非常明显:
- 实现最简单,无需手动处理线程安全和懒加载问题
- 天然防止反射破坏:JVM禁止通过反射创建枚举实例
- 天然防止序列化破坏:枚举的序列化和反序列化由JVM保证,反序列化时会返回同一个枚举实例
- 线程安全:枚举实例的创建在类初始化阶段完成,由JVM保证线程安全
不同单例实现的选型建议:
- 无需懒加载,实例初始化耗时短:优先选择饿汉式单例,实现最简单,性能最高
- 需要懒加载,对性能要求高:优先选择DCL单例(必须加volatile)或静态内部类单例
- 需要防止反射和序列化破坏,追求最高安全性:优先选择枚举单例
四、volatile在生产环境的典型应用场景
4.1 线程状态标记位
volatile最经典的应用场景是线程的启停状态标记位,通过volatile修饰的boolean变量,控制线程的执行状态,实现线程的优雅停止。
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class ThreadStopDemo {
private static volatile boolean running = true;
public static void main(String[] args) throws InterruptedException {
Thread taskThread = new Thread(() -> {
log.info("任务线程启动");
while (running) {
// 执行业务任务
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("任务线程被中断", e);
}
}
log.info("任务线程收到停止信号,优雅退出");
}, "task-thread");
taskThread.start();
Thread.sleep(3000);
running = false;
log.info("主线程已发送停止信号");
taskThread.join();
log.info("程序执行结束");
}
}
该场景完全符合volatile的使用条件:对running变量的写操作不依赖当前值,只有主线程会修改running变量,其他线程仅读取变量值,无需加锁,volatile能完美保证多线程间的可见性。
4.2 无锁编程的状态变量
在高性能的无锁编程场景中,volatile常被用于修饰状态标记变量,结合CAS操作实现无锁的线程安全控制。Java并发包中的AQS(AbstractQueuedSynchronizer)、Atomic原子类等核心组件,底层都依赖volatile修饰的状态变量。
以AQS为例,其内部通过volatile修饰的state变量控制同步状态:
private volatile int state;
当线程通过CAS操作修改state变量成功后,后续线程读取state变量时,能看到之前所有的操作结果,基于volatile的happens-before规则,实现无锁的线程安全控制。
4.3 安全发布不可变对象
在多线程场景中,安全发布对象是保证线程安全的关键。对于不可变对象,通过volatile修饰对象引用,可以实现安全的跨线程发布,保证所有线程都能看到正确初始化的不可变对象。
4.4 高性能并发框架中的应用
Java并发包中的大量高性能组件都依赖volatile实现,例如:
- CopyOnWriteArrayList:内部通过volatile修饰数组引用,保证数组修改后的可见性
- ConcurrentHashMap:内部通过volatile修饰节点的val和next变量,实现无锁的并发读操作
- ThreadPoolExecutor:内部通过volatile修饰ctl变量,控制线程池的运行状态,实现无锁的状态检测
五、volatile的常见误区与最佳实践
5.1 四大常见误区拆解
误区一:volatile能保证复合操作的原子性
这是最常见的误区,很多开发者认为给变量添加volatile修饰后,就能保证自增等复合操作的线程安全。实际上,volatile只能保证单次读/写操作的原子性,对于count++这类包含读-改-写的复合操作,volatile无法保证原子性,必须通过synchronized或原子类实现线程安全。
误区二:volatile比synchronized性能差
现代JVM对volatile做了大量优化,volatile的读操作性能和普通变量几乎没有区别,写操作的性能损耗也远低于synchronized。volatile是轻量级的同步机制,不会造成线程阻塞,在适合的场景下,使用volatile能获得比synchronized更好的性能。
误区三:只需要在写变量的地方加volatile,读的地方不需要
volatile的可见性和禁止重排序语义,需要读写两端都使用volatile修饰才能保证。如果只有写操作加了volatile,读操作没有加,JMM无法保证读线程能看到变量的最新值,也无法保证指令重排序的约束,依然会出现线程安全问题。
误区四:volatile修饰的对象引用,能保证对象内部属性的可见性
volatile修饰对象引用时,只能保证引用本身的可见性,无法保证对象内部成员变量的可见性。只有当线程A修改了volatile修饰的对象引用,线程B读取到新的引用后,线程A对对象内部属性的修改才对线程B可见。如果只是修改对象的内部属性,没有修改引用本身,volatile无法保证内部属性的可见性。
5.2 生产环境最佳实践指南
只有同时满足以下所有条件时,才适合使用volatile:
- 对变量的写操作不依赖于变量的当前值,或者保证只有单线程执行写操作
- 该变量不会与其他状态变量共同参与不变性约束
- 访问变量时,没有其他需要加锁的场景
生产环境最佳实践:
- 优先使用volatile修饰状态标记位,实现线程的启停控制,替代已被废弃的Thread.stop()方法
- 双重检查锁单例中,必须给实例引用添加volatile修饰,禁止指令重排序
- 无锁编程场景中,使用volatile修饰状态变量,结合CAS操作实现高性能的线程安全控制
- 不要使用volatile修饰计数器等需要复合操作的变量,这类场景优先使用Atomic原子类
- 不要过度使用volatile,只有在明确需要保证可见性和禁止重排序的场景下使用,避免增加代码的理解成本
六、总结
volatile是Java并发编程的基石,它的核心语义是保证多线程间的变量可见性和禁止指令重排序,底层通过内存屏障和MESI缓存一致性协议实现。理解volatile的底层原理,不仅能帮助我们写出线程安全的代码,更能帮助我们理解Java并发包中核心组件的实现逻辑。
在单例模式的架构设计中,volatile是修复DCL单例缺陷的关键,通过禁止对象初始化和引用赋值的重排序,保证多线程场景下不会获取到未初始化的半熟对象,实现高性能、线程安全的懒加载单例。
并发编程的核心,是理解多线程场景下的内存可见性、原子性和有序性问题,而volatile正是解决可见性和有序性问题的轻量级利器。掌握volatile的正确使用方法,避开常见误区,才能在高并发场景下写出稳定、高性能的Java代码。
项目依赖配置
<?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</groupId>
<artifactId>volatile-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>
<lombok.version>1.18.30</lombok.version>
<spring.version>6.1.5</spring.version>
<guava.version>32.1.3-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>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
</dependencies>
</project>