很多Java开发者在面试时能背出并发编程的核心概念,却在实际项目中频繁踩坑:出现超卖、死锁、数据不一致等线上问题时无从下手;只会用synchronized加锁,却不懂底层原理导致性能瓶颈;乱用线程池引发OOM。本文基于JDK 17,从JMM内存模型底层原理,到锁机制、JUC核心工具,再到生产级实战与避坑指南,全链路讲透Java并发编程的核心知识点,兼顾理论深度与落地实用性,帮你彻底打通并发编程的任督二脉。
一、并发编程的核心基石:JMM内存模型与三大特性
1.1 为什么需要JMM内存模型
CPU的运算速度比主存快了上千倍,为了提升性能,CPU引入了多级缓存、寄存器,编译器和CPU会对指令进行重排序优化。这就导致在多线程场景下,线程对变量的修改,其他线程看不到,或者指令执行顺序和预期不一致,引发线程安全问题。
JMM(Java Memory Model,Java内存模型)是JSR-133规范定义的,用来解决多线程场景下的可见性、原子性、有序性问题,屏蔽不同硬件和操作系统的内存访问差异,实现Java程序在不同平台的内存访问一致性。
1.2 JMM核心结构
JMM规定了所有变量都存储在主内存中,每个线程有自己的工作内存,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存的变量。线程间的变量传递,必须通过主内存完成。
1.3 并发编程三大核心特性
1.3.1 原子性
一个操作要么全部执行成功,要么全部不执行,执行过程中不会被线程调度器中断。
- 基本数据类型的赋值操作(JDK17 64位JVM已保证long、double的原子性)是原子操作
- 复合操作(如i++,分为读取-修改-写入三步)不具备原子性
错误示例:多线程i++原子性问题
package com.jam.demo.atomic;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.util.concurrent.CountDownLatch;
/**
* 原子性错误示例
* @author ken
*/
@Slf4j
public class AtomicErrorDemo {
private static int count = 0;
private static final int THREAD_COUNT = 10;
private static final int INCREMENT_COUNT = 1000;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
for (int j = 0; j < INCREMENT_COUNT; j++) {
count++;
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
// 预期结果10000,实际结果大概率小于10000
log.info("最终计数结果:{}", count);
}
}
正确示例:AtomicInteger解决原子性问题
package com.jam.demo.atomic;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 原子性正确示例
* @author ken
*/
@Slf4j
public class AtomicCorrectDemo {
private static final AtomicInteger count = new AtomicInteger(0);
private static final int THREAD_COUNT = 10;
private static final int INCREMENT_COUNT = 1000;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
for (int j = 0; j < INCREMENT_COUNT; j++) {
count.incrementAndGet();
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
// 预期结果10000,实际结果始终等于10000
log.info("最终计数结果:{}", count.get());
}
}
1.3.2 可见性
当一个线程修改了共享变量的值,其他线程能立即感知到这个修改。导致可见性问题的核心原因是:线程修改了工作内存的变量,没有及时刷新到主内存;其他线程没有重新从主内存读取最新的变量值。
错误示例:可见性问题导致死循环
package com.jam.demo.visibility;
import lombok.extern.slf4j.Slf4j;
/**
* 可见性错误示例
* @author ken
*/
@Slf4j
public class VisibilityErrorDemo {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// 空循环,线程不会重新读取主内存的flag值
}
log.info("线程感知到flag变化,循环结束");
}).start();
Thread.sleep(1000);
flag = false;
log.info("主线程已修改flag为false");
// 程序永远不会结束,子线程无法感知flag的变化
}
}
正确示例:volatile解决可见性问题
package com.jam.demo.visibility;
import lombok.extern.slf4j.Slf4j;
/**
* 可见性正确示例
* @author ken
*/
@Slf4j
public class VisibilityCorrectDemo {
// volatile修饰保证可见性
private static volatile boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while (flag) {
// 空循环
}
log.info("线程感知到flag变化,循环结束");
}).start();
Thread.sleep(1000);
flag = false;
log.info("主线程已修改flag为false");
// 程序正常结束,子线程成功感知flag变化
}
}
volatile可见性的底层实现:volatile修饰的变量,写操作后会插入写屏障,强制把工作内存的最新值刷新到主内存;读操作前会插入读屏障,强制从主内存读取最新值,保证每次读取的都是最新的。
1.3.3 有序性
程序执行的顺序按照代码的先后顺序执行,编译器和CPU不会进行指令重排序。指令重排序是编译器和CPU为了提升性能,在不改变单线程程序执行结果的前提下,对指令进行的重排序优化,但在多线程场景下会导致逻辑错误。
最经典的案例是双重检查锁单例模式,未加volatile会因指令重排序导致拿到半初始化的对象。对象创建分为三步:1.分配内存空间 2.初始化对象 3.将对象引用指向分配的内存地址。指令重排序可能会把2和3颠倒,导致线程A执行了1和3,还没执行2,线程B此时判断对象不为null,直接拿到了未初始化的对象,引发空指针异常。
正确示例:volatile+双重检查锁实现单例
package com.jam.demo.singleton;
/**
* 双重检查锁单例模式
* @author ken
*/
public class SingletonDemo {
// volatile禁止指令重排序,保证有序性
private static volatile SingletonDemo instance;
private SingletonDemo() {
// 私有构造方法,防止外部实例化
}
/**
* 获取单例实例
* @return 单例对象
*/
public static SingletonDemo getInstance() {
// 第一次检查,避免不必要的加锁
if (instance == null) {
synchronized (SingletonDemo.class) {
// 第二次检查,防止多线程同时进入第一次检查后重复创建
if (instance == null) {
instance = new SingletonDemo();
}
}
}
return instance;
}
}
1.4 happens-before核心规则
happens-before是JMM的核心,用来判断多线程场景下是否存在数据竞争、是否线程安全,它不是指A操作在B操作之前执行,而是说A操作的执行结果对B操作可见,保证多线程场景下的可见性和有序性。JSR-133定义了8条核心规则:
- 程序次序规则:一个线程内,按照代码顺序,前面的操作happens-before于后面的操作
- 锁规则:一个unlock操作happens-before于后续对同一个锁的lock操作
- volatile变量规则:对一个volatile变量的写操作happens-before于后续对这个变量的读操作
- 线程启动规则:Thread的start()方法happens-before于该线程内的所有操作
- 线程终止规则:线程内的所有操作happens-before于其他线程检测到该线程终止
- 线程中断规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生
- 对象终结规则:一个对象的初始化完成happens-before于它的finalize()方法的开始
- 传递性规则:如果A happens-before B,B happens-before C,那么A happens-before C
二、并发编程的核心锁机制:从底层原理到实战选型
2.1 synchronized底层原理与JDK17优化
synchronized是Java原生的互斥锁,保证原子性、可见性、有序性,是可重入锁,底层基于对象头和监视器锁(Monitor)实现。
2.1.1 对象头与锁状态
Java对象在内存中分为三部分:对象头、实例数据、对齐填充。对象头包含两部分:
- Mark Word:标记字段,64位JVM中占8字节,存储对象的hashCode、分代年龄、锁状态等信息
- Class Pointer:类型指针,指向对象的类元数据
Mark Word根据锁状态分为四种:无锁、偏向锁、轻量级锁、重量级锁。注意:JDK15及以后,偏向锁默认关闭,需手动开启-XX:+UseBiasedLocking参数,因为高并发场景下偏向锁的撤销开销远大于收益。
2.1.2 锁升级流程
锁只能升级,不能降级(除偏向锁可撤销到无锁),完整流程如下:
- 偏向锁:单线程访问时,将线程ID记录在Mark Word中,后续访问无需CAS操作,几乎无开销,适合单线程频繁加锁场景
- 轻量级锁:多线程交替访问时,线程在栈帧中创建锁记录,用CAS将Mark Word替换为指向锁记录的指针,成功则获取锁,失败则自旋等待,不会阻塞线程,适合锁持有时间短的场景
- 重量级锁:多线程同时竞争,自适应自旋超过阈值后升级为重量级锁,底层依赖操作系统互斥量(Mutex)实现,线程会被阻塞,上下文切换开销大,适合锁持有时间长的场景
2.1.3 synchronized正确使用示例
package com.jam.demo.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
/**
* synchronized使用示例
* @author ken
*/
@Slf4j
public class SynchronizedDemo {
private static int count = 0;
private static final int THREAD_COUNT = 10;
private static final int INCREMENT_COUNT = 1000;
/**
* 同步方法,锁当前对象实例
*/
public synchronized void increment() {
count++;
}
/**
* 静态同步方法,锁当前类的Class对象
*/
public static synchronized void staticIncrement() {
count++;
}
public static void main(String[] args) throws InterruptedException {
SynchronizedDemo demo = new SynchronizedDemo();
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
for (int j = 0; j < INCREMENT_COUNT; j++) {
// 同步代码块,锁demo对象
synchronized (demo) {
count++;
}
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
log.info("最终计数结果:{}", count);
}
}
2.2 Lock体系核心实现与实战选型
Lock是JUC提供的显式锁接口,相比synchronized,提供了更灵活的锁控制:可中断、可超时、可尝试获取锁、支持公平锁与非公平锁切换。
2.2.1 ReentrantLock核心使用
ReentrantLock是可重入的独占锁,基于AQS实现,默认非公平锁,可通过构造方法传入true开启公平锁。注意:unlock()必须放在finally块中,防止异常导致锁无法释放。
package com.jam.demo.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.locks.ReentrantLock;
/**
* ReentrantLock使用示例
* @author ken
*/
@Slf4j
public class ReentrantLockDemo {
private static int count = 0;
private static final int THREAD_COUNT = 10;
private static final int INCREMENT_COUNT = 1000;
// 创建非公平锁,传入true则为公平锁
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(THREAD_COUNT);
for (int i = 0; i < THREAD_COUNT; i++) {
new Thread(() -> {
try {
for (int j = 0; j < INCREMENT_COUNT; j++) {
// 获取锁
lock.lock();
try {
count++;
} finally {
// 释放锁必须放在finally块中
lock.unlock();
}
}
} finally {
countDownLatch.countDown();
}
}).start();
}
countDownLatch.await();
log.info("最终计数结果:{}", count);
}
}
2.2.2 synchronized与ReentrantLock核心区别
| 特性 | synchronized | ReentrantLock |
| 锁实现 | JVM原生,基于对象头和监视器锁 | JDK层面实现,基于AQS |
| 灵活性 | 低,锁的获取和释放自动完成 | 高,可手动控制锁的获取和释放,支持超时、中断、尝试获取 |
| 公平性 | 仅支持非公平锁 | 支持公平锁和非公平锁 |
| 可重入性 | 支持 | 支持 |
| 条件变量 | 仅支持1个(wait/notify) | 支持多个Condition,可精准唤醒指定线程 |
| 性能 | JDK17优化后,低竞争场景与ReentrantLock持平,高竞争场景略低 | 高竞争场景性能更稳定,可控性更强 |
2.2.3 读写锁ReentrantReadWriteLock
适用于读多写少的场景,读锁是共享锁,读操作之间不互斥;写锁是排他锁,读和写、写和写之间互斥,大幅提升读多写少场景的并发性能。
核心规则:
- 写锁可降级为读锁(持有写锁的同时获取读锁,再释放写锁)
- 读锁不能升级为写锁(持有读锁时获取写锁会被永久阻塞)
- 读锁和写锁均支持可重入
package com.jam.demo.lock;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.ObjectUtils;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* 读写锁实现缓存示例
* @author ken
*/
@Slf4j
public class ReadWriteLockCacheDemo {
private static final Map<String, Object> CACHE_MAP = new HashMap<>();
private static final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 读锁
private static final ReentrantReadWriteLock.ReadLock READ_LOCK = rwLock.readLock();
// 写锁
private static final ReentrantReadWriteLock.WriteLock WRITE_LOCK = rwLock.writeLock();
/**
* 从缓存获取数据
* @param key 缓存key
* @return 缓存value
*/
public Object get(String key) {
READ_LOCK.lock();
try {
return CACHE_MAP.get(key);
} finally {
READ_LOCK.unlock();
}
}
/**
* 写入缓存数据
* @param key 缓存key
* @param value 缓存value
*/
public void put(String key, Object value) {
WRITE_LOCK.lock();
try {
CACHE_MAP.put(key, value);
} finally {
WRITE_LOCK.unlock();
}
}
/**
* 清空缓存
*/
public void clear() {
WRITE_LOCK.lock();
try {
CACHE_MAP.clear();
} finally {
WRITE_LOCK.unlock();
}
}
}
2.2.4 StampedLock邮戳锁
JDK8引入,JDK17做了性能优化,解决了ReentrantReadWriteLock的写锁饥饿问题(大量读线程导致写线程长期无法获取锁),支持三种模式:写锁、悲观读锁、乐观读。
乐观读是核心优势:不需要加锁,返回一个邮戳(stamp),读取完成后用validate()方法校验邮戳是否有效,有效则说明读取过程中无写操作,数据安全;无效则升级为悲观读锁,性能远高于传统读写锁。
package com.jam.demo.lock;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.locks.StampedLock;
/**
* StampedLock使用示例
* @author ken
*/
@Slf4j
public class StampedLockDemo {
private int x;
private int y;
private final StampedLock stampedLock = new StampedLock();
/**
* 写操作,加写锁
* @param x 新的x值
* @param y 新的y值
*/
public void write(int x, int y) {
// 获取写锁,返回邮戳
long stamp = stampedLock.writeLock();
try {
this.x = x;
this.y = y;
} finally {
// 释放写锁,传入获取锁时的邮戳
stampedLock.unlockWrite(stamp);
}
}
/**
* 读操作,乐观读模式
* @return 计算结果
*/
public int read() {
// 乐观读,返回邮戳
long stamp = stampedLock.tryOptimisticRead();
// 读取数据,无锁
int currentX = this.x;
int currentY = this.y;
// 校验邮戳是否有效,无效则升级为悲观读锁
if (!stampedLock.validate(stamp)) {
// 获取悲观读锁
stamp = stampedLock.readLock();
try {
currentX = this.x;
currentY = this.y;
} finally {
// 释放悲观读锁
stampedLock.unlockRead(stamp);
}
}
return currentX + currentY;
}
}
2.3 AQS核心原理,彻底搞懂锁的底层
AQS(AbstractQueuedSynchronizer,抽象队列同步器)是JUC锁和同步工具类的核心基础,ReentrantLock、CountDownLatch、Semaphore等所有JUC同步工具均基于AQS实现。
通俗来讲:AQS是一个并发编程的基础框架,用一个volatile修饰的int类型state变量表示同步状态,用一个双向链表(CLH队列)管理等待获取锁的线程,提供了模板方法,子类只需实现tryAcquire、tryRelease等核心方法,即可实现自定义同步器。
2.3.1 AQS核心结构
- 同步状态state:volatile修饰,保证多线程间的可见性,子类通过getState()、setState()、compareAndSetState()方法安全操作state
- 独占锁:state=0表示无锁,state>0表示有线程持有锁,可重入时state递增
- 共享锁:state表示可用的许可数量
- CLH等待队列:双向链表,存储等待获取锁的线程,节点分为独占模式和共享模式。线程获取锁失败时,会被封装成节点加入队列尾部,阻塞等待;锁释放时,会唤醒队列头部的线程
- 条件队列:Condition接口实现,用于线程的等待和唤醒,一个AQS可对应多个条件队列,实现精准唤醒
2.3.2 基于AQS实现自定义独占锁
package com.jam.demo.lock;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
/**
* 基于AQS实现自定义独占锁
* @author ken
*/
public class CustomAqsLock {
/**
* 自定义同步器,继承AQS
*/
private static class Sync extends AbstractQueuedSynchronizer {
/**
* 尝试获取锁
* @param arg 获取参数
* @return 是否获取成功
*/
@Override
protected boolean tryAcquire(int arg) {
// CAS设置state,state=0表示无锁,设置为1表示获取锁成功
if (compareAndSetState(0, 1)) {
// 设置当前线程为锁的持有者
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
/**
* 尝试释放锁
* @param arg 释放参数
* @return 是否释放成功
*/
@Override
protected boolean tryRelease(int arg) {
// 校验当前线程是否是锁的持有者
if (getExclusiveOwnerThread() != Thread.currentThread()) {
throw new IllegalMonitorStateException("当前线程不是锁的持有者");
}
// 设置state为0,释放锁
setState(0);
setExclusiveOwnerThread(null);
return true;
}
/**
* 判断是否持有锁
* @return 是否持有锁
*/
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
}
private final Sync sync = new Sync();
/**
* 获取锁,阻塞直到获取成功
*/
public void lock() {
sync.acquire(1);
}
/**
* 尝试获取锁,非阻塞,立即返回结果
* @return 是否获取成功
*/
public boolean tryLock() {
return sync.tryAcquire(1);
}
/**
* 释放锁
*/
public void unlock() {
sync.release(1);
}
/**
* 判断锁是否被持有
* @return 是否被持有
*/
public boolean isLocked() {
return sync.isHeldExclusively();
}
}
三、JUC核心工具类全解:生产环境高频使用的并发利器
3.1 原子类:无锁原子操作,解决复合操作原子性问题
原子类位于java.util.concurrent.atomic包下,底层基于CAS+volatile实现,无锁化,高并发场景下性能远高于锁。JDK17中原子类分为四大类:
- 基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean
- 数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 引用类型原子类:AtomicReference、AtomicStampedReference、AtomicMarkableReference
- 字段更新原子类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater
3.1.1 CAS核心原理与问题解决
CAS(Compare And Swap,比较并交换)是CPU级别的原子指令,需要三个参数:内存地址V、预期值A、新值B,只有当V的值等于A时,才会把V的值更新为B,否则不做任何操作,返回当前值。
CAS三大核心问题与解决方案:
- ABA问题:变量的值从A变成B,又变回A,CAS会认为值没有变化,实际已发生修改。解决方案:AtomicStampedReference,给变量加上版本号,每次修改版本号递增,CAS同时比较值和版本号
- 自旋开销大:高并发场景下,大量线程同时CAS,导致自旋重试次数过多,CPU占用过高。解决方案:分段锁、LongAdder替代AtomicLong
- 单变量限制:CAS只能对单个变量进行原子操作,多变量原子性需用锁或AtomicReference封装多个变量
AtomicStampedReference解决ABA问题示例
package com.jam.demo.atomic;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicStampedReference;
/**
* AtomicStampedReference解决ABA问题
* @author ken
*/
@Slf4j
public class AtomicStampedReferenceDemo {
// 初始化引用和版本号
private static final AtomicStampedReference<Integer> reference = new AtomicStampedReference<>(100, 1);
public static void main(String[] args) throws InterruptedException {
// 线程1:执行ABA操作
Thread thread1 = new Thread(() -> {
int stamp = reference.getStamp();
log.info("线程1初始版本号:{},初始值:{}", stamp, reference.getReference());
// 100 -> 200
reference.compareAndSet(100, 200, stamp, stamp + 1);
// 200 -> 100
reference.compareAndSet(200, 100, reference.getStamp(), reference.getStamp() + 1);
log.info("线程1完成ABA操作,最终版本号:{},最终值:{}", reference.getStamp(), reference.getReference());
});
// 线程2:尝试更新值
Thread thread2 = new Thread(() -> {
int stamp = reference.getStamp();
log.info("线程2初始版本号:{},初始值:{}", stamp, reference.getReference());
try {
// 等待线程1完成ABA操作
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断异常", e);
}
// 尝试更新,版本号不匹配,更新失败
boolean result = reference.compareAndSet(100, 300, stamp, stamp + 1);
log.info("线程2更新结果:{},当前版本号:{}", result, reference.getStamp());
});
thread1.start();
thread2.start();
thread1.join();
thread2.join();
}
}
3.1.2 JDK17增强:VarHandle变量句柄
VarHandle是JDK9引入的特性,JDK17做了优化,替代了Unsafe类,提供了更安全、更灵活的内存操作,支持各种类型的CAS、内存屏障操作,性能更高,是JDK9+推荐的原子操作方式。
package com.jam.demo.atomic;
import lombok.extern.slf4j.Slf4j;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;
/**
* VarHandle使用示例
* @author ken
*/
@Slf4j
public class VarHandleDemo {
private volatile int count = 0;
// 定义VarHandle
private static final VarHandle COUNT_HANDLE;
static {
try {
// 初始化VarHandle,绑定count字段
COUNT_HANDLE = MethodHandles.lookup().findVarHandle(VarHandleDemo.class, "count", int.class);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* 原子递增
* @return 递增后的值
*/
public int incrementAndGet() {
int prev;
int next;
do {
// 获取当前值
prev = (int) COUNT_HANDLE.getVolatile(this);
next = prev + 1;
// CAS更新,失败则自旋重试
} while (!COUNT_HANDLE.compareAndSet(this, prev, next));
return next;
}
public static void main(String[] args) throws InterruptedException {
VarHandleDemo demo = new VarHandleDemo();
int threadCount = 10;
int incrementCount = 1000;
Thread[] threads = new Thread[threadCount];
for (int i = 0; i < threadCount; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < incrementCount; j++) {
demo.incrementAndGet();
}
});
threads[i].start();
}
for (Thread thread : threads) {
thread.join();
}
log.info("最终计数结果:{}", demo.count);
}
}
3.2 并发容器:高并发场景下的线程安全容器
日常开发中,很多开发者使用Collections.synchronizedList()等同步容器,但性能极低,因为所有方法都用synchronized修饰,同一时间只能有一个线程访问。JUC提供了更高效的并发容器,针对高并发场景做了深度优化。
3.2.1 ConcurrentHashMap
高并发场景下的线程安全HashMap,替代HashMap和Hashtable。JDK17的ConcurrentHashMap底层结构为数组+链表+红黑树,核心优化点:
- 读操作无锁:用volatile保证可见性,读操作无需加锁,性能极高
- 细粒度锁:JDK8之后不再使用分段锁,改为对数组的每个桶节点加锁,锁粒度更细,并发度更高
- 多线程协助扩容:扩容时支持多线程协助,大幅提升扩容效率
核心容器对比
| 容器 | 线程安全 | 锁机制 | 性能 | 适用场景 |
| HashMap | 否 | 无锁 | 最高 | 单线程场景 |
| Hashtable | 是 | 全表synchronized锁 | 极低 | 已废弃,不推荐使用 |
| Collections.synchronizedMap | 是 | 全表synchronized锁 | 低 | 低并发场景 |
| ConcurrentHashMap | 是 | 桶级细粒度锁+CAS | 高 | 高并发读写场景 |
ConcurrentHashMap缓存实现示例
package com.jam.demo.container;
import com.alibaba.fastjson2.JSON;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
import java.util.concurrent.ConcurrentHashMap;
/**
* ConcurrentHashMap实现本地缓存
* @author ken
*/
@Slf4j
public class ConcurrentHashMapCacheDemo {
private static final ConcurrentHashMap<String, Object> LOCAL_CACHE = new ConcurrentHashMap<>();
/**
* 存入缓存
* @param key 缓存key
* @param value 缓存value
*/
public void put(String key, Object value) {
if (!StringUtils.hasText(key) || ObjectUtils.isEmpty(value)) {
log.warn("缓存key或value为空");
return;
}
LOCAL_CACHE.put(key, value);
}
/**
* 获取缓存
* @param key 缓存key
* @return 缓存value
*/
public Object get(String key) {
if (!StringUtils.hasText(key)) {
return null;
}
return LOCAL_CACHE.get(key);
}
/**
* 不存在则存入,原子操作
* @param key 缓存key
* @param value 缓存value
* @return 已存在的value或新存入的value
*/
public Object putIfAbsent(String key, Object value) {
if (!StringUtils.hasText(key) || ObjectUtils.isEmpty(value)) {
return null;
}
return LOCAL_CACHE.putIfAbsent(key, value);
}
/**
* 删除缓存
* @param key 缓存key
*/
public void remove(String key) {
if (!StringUtils.hasText(key)) {
return;
}
LOCAL_CACHE.remove(key);
}
}
3.2.2 CopyOnWriteArrayList
高并发读多写少场景下的线程安全List,替代ArrayList和Vector。核心原理是写时复制:当进行add、set、remove等写操作时,会复制一份新的数组,在新数组上完成修改后,将数组引用指向新数组;读操作无需加锁,直接读取当前数组。
优缺点与适用场景
- 优点:读操作无锁,性能极高,线程安全
- 缺点:写操作需要复制数组,内存开销大,写性能低;只能保证最终一致性,无法保证实时一致性
- 适用场景:配置列表、白名单、黑名单等读多写少、数据量不大的场景
package com.jam.demo.container;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
/**
* CopyOnWriteArrayList使用示例
* @author ken
*/
@Slf4j
public class CopyOnWriteArrayListDemo {
// 初始化CopyOnWriteArrayList
private static final CopyOnWriteArrayList<String> WHITE_LIST = new CopyOnWriteArrayList<>();
/**
* 批量添加白名单
* @param list 白名单列表
*/
public void addWhiteList(List<String> list) {
if (CollectionUtils.isEmpty(list)) {
return;
}
WHITE_LIST.addAll(list);
}
/**
* 判断是否在白名单中
* @param value 待校验的值
* @return 是否在白名单中
*/
public boolean isInWhiteList(String value) {
if (!StringUtils.hasText(value)) {
return false;
}
return WHITE_LIST.contains(value);
}
public static void main(String[] args) {
CopyOnWriteArrayListDemo demo = new CopyOnWriteArrayListDemo();
// 初始化白名单
demo.addWhiteList(Lists.newArrayList("127.0.0.1", "192.168.1.1"));
// 校验白名单
log.info("127.0.0.1是否在白名单:{}", demo.isInWhiteList("127.0.0.1"));
log.info("10.0.0.1是否在白名单:{}", demo.isInWhiteList("10.0.0.1"));
}
}
3.3 线程池:生产环境并发编程的核心,彻底搞懂原理与最佳实践
线程池是生产环境中使用最频繁的并发工具,也是最容易踩坑的。阿里巴巴Java开发手册明确禁止使用Executors创建线程池,必须通过ThreadPoolExecutor构造方法手动创建。
3.3.1 线程池的核心价值
- 降低资源消耗:重复利用已创建的线程,避免频繁创建和销毁线程的开销
- 提高响应速度:任务到达时,无需等待线程创建,直接执行
- 统一管理线程:控制线程的最大并发数,避免无限制创建线程导致OOM,支持线程监控与调优
3.3.2 ThreadPoolExecutor七大核心参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
- corePoolSize核心线程数:线程池中长期存活的线程数量,即使空闲也不会被销毁(除非设置allowCoreThreadTimeOut=true)
- CPU密集型任务:corePoolSize = CPU核心数 + 1
- IO密集型任务:corePoolSize = CPU核心数 * 2
- 混合型任务:拆分任务,分别设置线程池
- maximumPoolSize最大线程数:线程池允许创建的最大线程数量,核心线程满、工作队列满后,才会创建新线程,直到达到最大值
- keepAliveTime空闲存活时间:非核心线程的空闲存活时间,超过该时间会被销毁;设置allowCoreThreadTimeOut=true时,核心线程也受该时间控制
- unit时间单位:keepAliveTime的时间单位
- workQueue工作队列:存储等待执行的任务,必须使用有界队列,生产环境禁止使用无界队列(会导致任务无限堆积,引发OOM),推荐使用ArrayBlockingQueue
- threadFactory线程工厂:用于创建线程,生产环境必须自定义线程工厂,设置有意义的线程名称前缀,方便线上问题排查
- handler拒绝策略:核心线程满、工作队列满、最大线程数满后,新任务会被拒绝,JDK提供4种默认策略
- AbortPolicy:默认策略,直接抛出RejectedExecutionException,生产环境推荐使用,及时感知任务被拒绝
- CallerRunsPolicy:用调用者线程执行任务,降低任务提交速度,起到流量控制作用
- DiscardPolicy:直接丢弃任务,不抛出异常,不推荐使用
- DiscardOldestPolicy:丢弃队列中最旧的任务,重新提交当前任务,不推荐使用
3.3.3 线程池执行流程
3.3.4 生产环境标准线程池实现
package com.jam.demo.threadpool;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
/**
* 生产环境线程池配置
* @author ken
*/
@Slf4j
@Configuration
public class ThreadPoolConfig {
/**
* CPU核心数
*/
private static final int CPU_CORE = Runtime.getRuntime().availableProcessors();
/**
* 核心线程数
*/
private static final int CORE_POOL_SIZE = CPU_CORE * 2;
/**
* 最大线程数
*/
private static final int MAX_POOL_SIZE = CPU_CORE * 4;
/**
* 空闲存活时间
*/
private static final long KEEP_ALIVE_TIME = 60L;
/**
* 工作队列容量
*/
private static final int QUEUE_CAPACITY = 1000;
/**
* 线程名称前缀
*/
private static final String THREAD_NAME_PREFIX = "business-thread-pool-";
/**
* 业务通用线程池
* @return ThreadPoolExecutor
*/
@Bean("businessThreadPool")
public ThreadPoolExecutor businessThreadPool() {
// 自定义线程工厂,设置线程名称
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setNameFormat(THREAD_NAME_PREFIX + "%d")
.setDaemon(false)
.build();
// 创建线程池
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
threadFactory,
new ThreadPoolExecutor.AbortPolicy()
);
// 线程池监控日志
log.info("业务线程池初始化完成,核心线程数:{},最大线程数:{},队列容量:{}",
CORE_POOL_SIZE, MAX_POOL_SIZE, QUEUE_CAPACITY);
return threadPool;
}
}
3.3.5 JDK17虚拟线程
虚拟线程是JDK19引入的预览特性,JDK21正式发布,JDK17可通过--enable-preview参数开启。虚拟线程是JVM管理的轻量级线程,不依赖操作系统内核线程,创建和销毁开销极低,可支持百万级并发,特别适合IO密集型任务(网络请求、数据库操作等)。
package com.jam.demo.threadpool;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Executors;
/**
* 虚拟线程使用示例
* JDK17需添加VM参数:--enable-preview
* @author ken
*/
@Slf4j
public class VirtualThreadDemo {
public static void main(String[] args) {
// 创建虚拟线程执行器
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
// 提交10000个任务,虚拟线程可轻松支持
for (int i = 0; i < 10000; i++) {
int taskNum = i;
executor.submit(() -> {
log.info("虚拟线程执行任务:{}", taskNum);
try {
// 模拟IO操作
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("任务执行中断", e);
}
});
}
}
log.info("所有任务执行完成");
}
}
3.4 同步工具类:多线程协作的核心利器
JUC提供了丰富的同步工具类,用于多线程之间的协作,高频使用的有CountDownLatch、CyclicBarrier、Semaphore。
3.4.1 CountDownLatch闭锁
一次性同步工具,允许一个或多个线程等待其他线程完成操作后再执行,基于AQS实现,用state变量表示计数,countDown()方法将state减1,await()方法阻塞直到state变为0。
适用场景:并行任务汇总,比如多个线程并行查询数据,主线程等待所有线程查询完成后汇总结果。
package com.jam.demo.sync;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.CountDownLatch;
/**
* CountDownLatch使用示例
* @author ken
*/
@Slf4j
public class CountDownLatchDemo {
private static final int TASK_COUNT = 5;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(TASK_COUNT);
for (int i = 0; i < TASK_COUNT; i++) {
int taskNum = i;
new Thread(() -> {
try {
log.info("任务{}开始执行", taskNum);
// 模拟任务执行
Thread.sleep(1000);
log.info("任务{}执行完成", taskNum);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("任务执行中断", e);
} finally {
// 计数减1
countDownLatch.countDown();
}
}).start();
}
// 主线程阻塞,等待所有任务完成
log.info("主线程等待所有任务执行完成");
countDownLatch.await();
log.info("所有任务执行完成,主线程继续执行");
}
}
3.4.2 CyclicBarrier循环栅栏
可重复使用的同步工具,允许一组线程互相等待,直到所有线程都到达栅栏位置,然后一起执行后续操作。基于ReentrantLock和Condition实现,计数变为0后会自动重置,可重复使用。
适用场景:多线程并行计算,每个阶段都需要等待所有线程完成后再进入下一个阶段,比如压力测试,多个线程同时启动执行压测。
CountDownLatch与CyclicBarrier核心区别
| 特性 | CountDownLatch | CyclicBarrier |
| 可重用性 | 一次性,计数变为0后无法重置 | 可重复使用,计数变为0后自动重置 |
| 等待对象 | 主线程等待多个工作线程完成 | 多个工作线程互相等待,全部到达后一起执行 |
| 计数方式 | 线程调用countDown()减1,不阻塞 | 线程调用await()减1,阻塞等待 |
| 高级特性 | 无 | 支持传入Runnable任务,所有线程到达后优先执行 |
3.4.3 Semaphore信号量
用于控制同时访问特定资源的线程数量,实现流量控制,基于AQS实现,用state变量表示可用的许可数量,acquire()方法获取许可,release()方法释放许可。
适用场景:流量控制,比如数据库连接池、接口限流、秒杀场景的并发控制。
package com.jam.demo.sync;
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.Semaphore;
/**
* Semaphore实现接口限流
* @author ken
*/
@Slf4j
public class SemaphoreLimitDemo {
// 最大并发数10
private static final int MAX_CONCURRENT = 10;
private static final Semaphore semaphore = new Semaphore(MAX_CONCURRENT);
/**
* 模拟接口请求
* @param requestId 请求ID
*/
public void handleRequest(String requestId) {
try {
// 获取许可,获取不到则阻塞
semaphore.acquire();
log.info("请求{}获取许可,开始处理", requestId);
// 模拟接口处理
Thread.sleep(500);
log.info("请求{}处理完成,释放许可", requestId);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("请求处理中断", e);
} finally {
// 释放许可
semaphore.release();
}
}
public static void main(String[] args) {
SemaphoreLimitDemo demo = new SemaphoreLimitDemo();
// 模拟100个并发请求
for (int i = 0; i < 100; i++) {
String requestId = "REQ-" + i;
new Thread(() -> demo.handleRequest(requestId)).start();
}
}
}
四、生产级并发实战:商品库存扣减场景,彻底解决超卖问题
4.1 业务场景与环境准备
电商秒杀场景中,用户下单扣减商品库存,高并发场景下极易出现超卖问题(库存扣减为负数)。本文基于JDK17、SpringBoot3、MyBatis-Plus、MySQL8.0,通过多种方案解决超卖问题,所有代码均可直接编译运行。
4.1.1 Maven核心依赖
<?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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.5</version>
<relativePath/>
</parent>
<groupId>com.jam</groupId>
<artifactId>concurrency-stock-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>concurrency-stock-demo</name>
<description>并发库存扣减实战demo</description>
<properties>
<java.version>17</java.version>
<mybatis-plus.version>3.5.7</mybatis-plus.version>
<lombok.version>1.18.32</lombok.version>
<fastjson2.version>2.0.52</fastjson2.version>
<guava.version>33.2.0-jre</guava.version>
<springdoc.version>2.5.0</springdoc.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>${lombok.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>${fastjson2.version}</version>
</dependency>
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>${guava.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>${springdoc.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4.1.2 MySQL表结构
CREATE TABLE `t_product_stock` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`product_id` bigint NOT NULL COMMENT '商品ID',
`stock_num` int NOT NULL DEFAULT '0' COMMENT '库存数量',
`version` int NOT NULL DEFAULT '0' COMMENT '乐观锁版本号',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_product_id` (`product_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='商品库存表';
4.1.3 实体类
package com.jam.demo.entity;
import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
/**
* 商品库存实体类
* @author ken
*/
@Data
@TableName("t_product_stock")
@Schema(description = "商品库存实体")
public class ProductStock {
@Schema(description = "主键ID")
@TableId(type = IdType.AUTO)
private Long id;
@Schema(description = "商品ID")
private Long productId;
@Schema(description = "库存数量")
private Integer stockNum;
@Schema(description = "乐观锁版本号")
@Version
private Integer version;
@Schema(description = "创建时间")
@TableField(fill = FieldFill.INSERT)
private LocalDateTime createTime;
@Schema(description = "更新时间")
@TableField(fill = FieldFill.INSERT_UPDATE)
private LocalDateTime updateTime;
}
4.1.4 Mapper接口
package com.jam.demo.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.entity.ProductStock;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 商品库存Mapper接口
* @author ken
*/
public interface ProductStockMapper extends BaseMapper<ProductStock> {
/**
* 悲观锁扣减库存
* @param productId 商品ID
* @param num 扣减数量
* @return 影响行数
*/
@Update("UPDATE t_product_stock SET stock_num = stock_num - #{num} WHERE product_id = #{productId} AND stock_num >= #{num}")
int deductStock(@Param("productId") Long productId, @Param("num") Integer num);
}
4.2 错误实现:无锁直接扣减,导致超卖
package com.jam.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.ProductStock;
import com.jam.demo.mapper.ProductStockMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 错误库存扣减实现
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StockErrorService {
private final ProductStockMapper productStockMapper;
/**
* 错误扣减实现,无锁控制,高并发下超卖
* @param productId 商品ID
* @param num 扣减数量
* @return 扣减结果
*/
@Transactional(rollbackFor = Exception.class)
public boolean deductStock(Long productId, Integer num) {
// 1.查询库存
LambdaQueryWrapper<ProductStock> queryWrapper = new LambdaQueryWrapper<ProductStock>()
.eq(ProductStock::getProductId, productId);
ProductStock productStock = productStockMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(productStock)) {
log.error("商品库存不存在,productId:{}", productId);
return false;
}
// 2.校验库存
if (productStock.getStockNum() < num) {
log.error("商品库存不足,productId:{},当前库存:{},扣减数量:{}", productId, productStock.getStockNum(), num);
return false;
}
// 3.扣减库存
productStock.setStockNum(productStock.getStockNum() - num);
productStockMapper.updateById(productStock);
log.info("商品库存扣减成功,productId:{},扣减后库存:{}", productId, productStock.getStockNum());
return true;
}
}
问题分析:高并发场景下,多个线程同时查询到库存充足,同时执行扣减,导致库存扣减为负数,出现超卖。
4.3 正确实现1:悲观锁方案
基于MySQL行锁实现,通过for update锁定行记录,保证同一时间只有一个线程能操作库存,避免超卖,使用编程式事务控制事务边界。
package com.jam.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.ProductStock;
import com.jam.demo.mapper.ProductStockMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* 悲观锁库存扣减实现
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StockPessimisticLockService {
private final ProductStockMapper productStockMapper;
private final PlatformTransactionManager transactionManager;
/**
* 悲观锁扣减库存,编程式事务控制
* @param productId 商品ID
* @param num 扣减数量
* @return 扣减结果
*/
public boolean deductStock(Long productId, Integer num) {
// 编程式事务定义
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 1.查询库存并加悲观锁,for update锁定行记录
LambdaQueryWrapper<ProductStock> queryWrapper = new LambdaQueryWrapper<ProductStock>()
.eq(ProductStock::getProductId, productId)
.last("FOR UPDATE");
ProductStock productStock = productStockMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(productStock)) {
log.error("商品库存不存在,productId:{}", productId);
transactionManager.rollback(status);
return false;
}
// 2.校验库存
if (productStock.getStockNum() < num) {
log.error("商品库存不足,productId:{},当前库存:{},扣减数量:{}", productId, productStock.getStockNum(), num);
transactionManager.rollback(status);
return false;
}
// 3.扣减库存
productStock.setStockNum(productStock.getStockNum() - num);
productStockMapper.updateById(productStock);
// 提交事务
transactionManager.commit(status);
log.info("商品库存扣减成功,productId:{},扣减后库存:{}", productId, productStock.getStockNum());
return true;
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
log.error("商品库存扣减异常,productId:{}", productId, e);
return false;
}
}
}
4.4 正确实现2:乐观锁方案
基于版本号机制实现,MyBatis-Plus内置乐观锁插件,更新时校验版本号,版本号匹配才更新成功,否则更新失败,避免超卖,性能高于悲观锁,适合读多写少的场景。
4.4.1 MyBatis-Plus乐观锁配置
package com.jam.demo.config;
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* MyBatis-Plus配置类
* @author ken
*/
@Configuration
public class MybatisPlusConfig {
/**
* 配置MyBatis-Plus拦截器
* @return MybatisPlusInterceptor
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
// 乐观锁插件
interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
// 分页插件
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
4.4.2 乐观锁业务实现
package com.jam.demo.service;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.entity.ProductStock;
import com.jam.demo.mapper.ProductStockMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* 乐观锁库存扣减实现
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StockOptimisticLockService {
private final ProductStockMapper productStockMapper;
private final PlatformTransactionManager transactionManager;
/**
* 最大重试次数
*/
private static final int MAX_RETRY = 3;
/**
* 乐观锁扣减库存,带重试机制
* @param productId 商品ID
* @param num 扣减数量
* @return 扣减结果
*/
public boolean deductStock(Long productId, Integer num) {
int retryCount = 0;
// 自旋重试
while (retryCount < MAX_RETRY) {
// 编程式事务定义
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 1.查询库存
LambdaQueryWrapper<ProductStock> queryWrapper = new LambdaQueryWrapper<ProductStock>()
.eq(ProductStock::getProductId, productId);
ProductStock productStock = productStockMapper.selectOne(queryWrapper);
if (ObjectUtils.isEmpty(productStock)) {
log.error("商品库存不存在,productId:{}", productId);
transactionManager.rollback(status);
return false;
}
// 2.校验库存
if (productStock.getStockNum() < num) {
log.error("商品库存不足,productId:{},当前库存:{},扣减数量:{}", productId, productStock.getStockNum(), num);
transactionManager.rollback(status);
return false;
}
// 3.扣减库存,乐观锁自动校验版本号
productStock.setStockNum(productStock.getStockNum() - num);
int updateCount = productStockMapper.updateById(productStock);
// 更新成功,提交事务
if (updateCount > 0) {
transactionManager.commit(status);
log.info("商品库存扣减成功,productId:{},扣减后库存:{},重试次数:{}",
productId, productStock.getStockNum(), retryCount);
return true;
}
// 更新失败,回滚事务,重试
transactionManager.rollback(status);
retryCount++;
Thread.sleep(10);
} catch (Exception e) {
transactionManager.rollback(status);
log.error("商品库存扣减异常,productId:{}", productId, e);
return false;
}
}
log.error("商品库存扣减重试次数超过上限,productId:{}", productId);
return false;
}
}
4.5 正确实现3:SQL直接扣减,数据库层面保证原子性
直接通过SQL语句完成库存校验和扣减,利用MySQL的InnoDB引擎事务特性,保证操作的原子性,性能最高,实现最简单。
package com.jam.demo.service;
import com.jam.demo.mapper.ProductStockMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
/**
* SQL原子扣减库存实现
* @author ken
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class StockSqlAtomicService {
private final ProductStockMapper productStockMapper;
private final PlatformTransactionManager transactionManager;
/**
* SQL原子扣减库存,数据库层面保证原子性
* @param productId 商品ID
* @param num 扣减数量
* @return 扣减结果
*/
public boolean deductStock(Long productId, Integer num) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
definition.setPropagationBehavior(DefaultTransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = transactionManager.getTransaction(definition);
try {
// 直接通过SQL完成校验和扣减,原子操作
int updateCount = productStockMapper.deductStock(productId, num);
if (updateCount > 0) {
transactionManager.commit(status);
log.info("商品库存扣减成功,productId:{}", productId);
return true;
}
transactionManager.rollback(status);
log.error("商品库存扣减失败,库存不足或商品不存在,productId:{}", productId);
return false;
} catch (Exception e) {
transactionManager.rollback(status);
log.error("商品库存扣减异常,productId:{}", productId, e);
return false;
}
}
}
五、并发编程常见坑点与线上问题排查指南
5.1 高频踩坑点与避坑指南
5.1.1 死锁问题
死锁是指两个或多个线程互相等待对方持有的锁,导致永久阻塞的现象。死锁的四个必要条件,缺一不可,破坏其中一个即可避免死锁:
- 互斥条件:一个资源同一时间只能被一个线程持有
- 持有并等待条件:线程持有至少一个资源,又请求其他线程持有的资源,同时不释放自己持有的资源
- 不可剥夺条件:线程持有的资源,只能自己释放,不能被其他线程强行剥夺
- 循环等待条件:多个线程之间形成循环等待资源的链
避坑方法:
- 固定加锁顺序,所有线程按照相同的顺序获取锁
- 避免持有锁的同时等待其他锁,尽量减少锁的持有时间
- 使用tryLock()方法设置超时时间,避免无限等待
- 尽量使用细粒度的锁,减少锁的范围
死锁示例与修复
package com.jam.demo.deadlock;
import lombok.extern.slf4j.Slf4j;
/**
* 死锁示例与修复
* @author ken
*/
@Slf4j
public class DeadLockDemo {
private static final Object LOCK_A = new Object();
private static final Object LOCK_B = new Object();
/**
* 死锁示例:线程1持有LOCK_A,等待LOCK_B;线程2持有LOCK_B,等待LOCK_A
*/
public static void deadLockDemo() {
new Thread(() -> {
synchronized (LOCK_A) {
log.info("线程1获取到LOCK_A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
log.info("线程1等待获取LOCK_B");
synchronized (LOCK_B) {
log.info("线程1获取到LOCK_B");
}
}
}, "dead-lock-thread-1").start();
new Thread(() -> {
synchronized (LOCK_B) {
log.info("线程2获取到LOCK_B");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
log.info("线程2等待获取LOCK_A");
synchronized (LOCK_A) {
log.info("线程2获取到LOCK_A");
}
}
}, "dead-lock-thread-2").start();
}
/**
* 修复死锁:固定加锁顺序,所有线程先获取LOCK_A,再获取LOCK_B
*/
public static void fixDeadLockDemo() {
new Thread(() -> {
synchronized (LOCK_A) {
log.info("线程1获取到LOCK_A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
log.info("线程1等待获取LOCK_B");
synchronized (LOCK_B) {
log.info("线程1获取到LOCK_B");
}
}
}, "fix-thread-1").start();
new Thread(() -> {
synchronized (LOCK_A) {
log.info("线程2获取到LOCK_A");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("线程中断", e);
}
log.info("线程2等待获取LOCK_B");
synchronized (LOCK_B) {
log.info("线程2获取到LOCK_B");
}
}
}, "fix-thread-2").start();
}
public static void main(String[] args) {
// 执行死锁示例
deadLockDemo();
// 执行修复后的示例
// fixDeadLockDemo();
}
}
5.1.2 线程池常见坑
- 使用无界队列,导致任务无限堆积,引发OOM
- 最大线程数设置过大,导致大量线程创建,CPU占用100%
- 线程池定义在方法内部,每次调用方法都创建新的线程池,导致线程无限创建
- 线程池中的任务捕获异常不当,导致异常被吞,无法感知任务执行失败
5.1.3 线程安全常见坑
- 使用SimpleDateFormat,该类不是线程安全的,多线程场景下会出现异常,推荐使用java.time包下的DateTimeFormatter
- 多线程场景下修改成员变量,未做线程安全控制,导致数据不一致,尽量使用局部变量(线程私有)
- 迭代集合的过程中修改集合,抛出ConcurrentModificationException,推荐使用并发容器
5.1.4 volatile误用
- 用volatile修饰复合操作的变量(如i++),volatile只能保证可见性和有序性,不能保证原子性
- 双重检查锁单例未加volatile,导致指令重排引发空指针异常
5.2 线上并发问题排查工具与方法
5.2.1 JDK自带排查工具
- jps:查看Java进程的PID,是所有排查的第一步
- jstack:查看线程的堆栈信息,自动检测死锁,排查线程阻塞、死循环等问题
- jstat:查看JVM的运行状态,GC情况、内存使用情况
- jmap:查看堆内存信息,生成堆转储文件,排查OOM问题
- JConsole/VisualVM:可视化工具,监控JVM运行状态、线程状态、内存使用情况
5.2.2 死锁排查步骤
- 用
jps -l获取Java进程的PID - 用
jstack <PID>查看线程堆栈信息,jstack会自动检测死锁,输出死锁的线程、持有的锁、等待的锁 - 根据堆栈信息,定位到代码中的死锁位置,按照避坑方法修复
5.2.3 线程安全问题排查步骤
- 复现问题,确定问题出现的场景和触发条件
- 检查共享变量的操作,是否保证了原子性、可见性、有序性
- 检查锁的使用是否正确,锁的范围是否合理,是否存在锁顺序问题
- 用日志、Arthas等工具,打印变量的修改过程,定位问题代码
六、总结
Java并发编程的核心,不是背会API和面试题,而是理解底层的JMM内存模型、锁的实现原理、AQS的核心逻辑。只有搞懂了底层原理,才能在实际项目中正确使用并发工具,避免踩坑,快速定位和解决线上问题。