别再只会用 synchronized!Java 并发编程全链路核心体系,从底层原理到生产实战全覆盖

简介: 本文深入解析Java并发编程核心知识,基于JDK17从底层原理到生产实践全面讲解。首先剖析JMM内存模型与三大特性(原子性、可见性、有序性),详解synchronized、ReentrantLock等锁机制及AQS实现原理。然后介绍JUC工具类(原子类、并发容器、线程池、同步工具)的正确使用方式。重点通过商品库存扣减案例,对比悲观锁、乐观锁、SQL原子操作三种方案解决超卖问题。最后总结常见坑点(死锁、线程池误用等)和线上问题排查方法,强调理解底层原理而非死记API的重要性,帮助开发者真正掌握并发编程精髓。

很多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条核心规则:

  1. 程序次序规则:一个线程内,按照代码顺序,前面的操作happens-before于后面的操作
  2. 锁规则:一个unlock操作happens-before于后续对同一个锁的lock操作
  3. volatile变量规则:对一个volatile变量的写操作happens-before于后续对这个变量的读操作
  4. 线程启动规则:Thread的start()方法happens-before于该线程内的所有操作
  5. 线程终止规则:线程内的所有操作happens-before于其他线程检测到该线程终止
  6. 线程中断规则:对线程interrupt()方法的调用happens-before于被中断线程的代码检测到中断事件的发生
  7. 对象终结规则:一个对象的初始化完成happens-before于它的finalize()方法的开始
  8. 传递性规则:如果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核心结构

  1. 同步状态state:volatile修饰,保证多线程间的可见性,子类通过getState()、setState()、compareAndSetState()方法安全操作state
  • 独占锁:state=0表示无锁,state>0表示有线程持有锁,可重入时state递增
  • 共享锁:state表示可用的许可数量
  1. CLH等待队列:双向链表,存储等待获取锁的线程,节点分为独占模式和共享模式。线程获取锁失败时,会被封装成节点加入队列尾部,阻塞等待;锁释放时,会唤醒队列头部的线程
  2. 条件队列: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中原子类分为四大类:

  1. 基本类型原子类:AtomicInteger、AtomicLong、AtomicBoolean
  2. 数组类型原子类:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
  3. 引用类型原子类:AtomicReference、AtomicStampedReference、AtomicMarkableReference
  4. 字段更新原子类:AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater

3.1.1 CAS核心原理与问题解决

CAS(Compare And Swap,比较并交换)是CPU级别的原子指令,需要三个参数:内存地址V、预期值A、新值B,只有当V的值等于A时,才会把V的值更新为B,否则不做任何操作,返回当前值。

CAS三大核心问题与解决方案:

  1. ABA问题:变量的值从A变成B,又变回A,CAS会认为值没有变化,实际已发生修改。解决方案:AtomicStampedReference,给变量加上版本号,每次修改版本号递增,CAS同时比较值和版本号
  2. 自旋开销大:高并发场景下,大量线程同时CAS,导致自旋重试次数过多,CPU占用过高。解决方案:分段锁、LongAdder替代AtomicLong
  3. 单变量限制: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)

  1. corePoolSize核心线程数:线程池中长期存活的线程数量,即使空闲也不会被销毁(除非设置allowCoreThreadTimeOut=true)
  • CPU密集型任务:corePoolSize = CPU核心数 + 1
  • IO密集型任务:corePoolSize = CPU核心数 * 2
  • 混合型任务:拆分任务,分别设置线程池
  1. maximumPoolSize最大线程数:线程池允许创建的最大线程数量,核心线程满、工作队列满后,才会创建新线程,直到达到最大值
  2. keepAliveTime空闲存活时间:非核心线程的空闲存活时间,超过该时间会被销毁;设置allowCoreThreadTimeOut=true时,核心线程也受该时间控制
  3. unit时间单位:keepAliveTime的时间单位
  4. workQueue工作队列:存储等待执行的任务,必须使用有界队列,生产环境禁止使用无界队列(会导致任务无限堆积,引发OOM),推荐使用ArrayBlockingQueue
  5. threadFactory线程工厂:用于创建线程,生产环境必须自定义线程工厂,设置有意义的线程名称前缀,方便线上问题排查
  6. 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 死锁问题

死锁是指两个或多个线程互相等待对方持有的锁,导致永久阻塞的现象。死锁的四个必要条件,缺一不可,破坏其中一个即可避免死锁:

  1. 互斥条件:一个资源同一时间只能被一个线程持有
  2. 持有并等待条件:线程持有至少一个资源,又请求其他线程持有的资源,同时不释放自己持有的资源
  3. 不可剥夺条件:线程持有的资源,只能自己释放,不能被其他线程强行剥夺
  4. 循环等待条件:多个线程之间形成循环等待资源的链

避坑方法

  • 固定加锁顺序,所有线程按照相同的顺序获取锁
  • 避免持有锁的同时等待其他锁,尽量减少锁的持有时间
  • 使用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 线程池常见坑

  1. 使用无界队列,导致任务无限堆积,引发OOM
  2. 最大线程数设置过大,导致大量线程创建,CPU占用100%
  3. 线程池定义在方法内部,每次调用方法都创建新的线程池,导致线程无限创建
  4. 线程池中的任务捕获异常不当,导致异常被吞,无法感知任务执行失败

5.1.3 线程安全常见坑

  1. 使用SimpleDateFormat,该类不是线程安全的,多线程场景下会出现异常,推荐使用java.time包下的DateTimeFormatter
  2. 多线程场景下修改成员变量,未做线程安全控制,导致数据不一致,尽量使用局部变量(线程私有)
  3. 迭代集合的过程中修改集合,抛出ConcurrentModificationException,推荐使用并发容器

5.1.4 volatile误用

  1. 用volatile修饰复合操作的变量(如i++),volatile只能保证可见性和有序性,不能保证原子性
  2. 双重检查锁单例未加volatile,导致指令重排引发空指针异常

5.2 线上并发问题排查工具与方法

5.2.1 JDK自带排查工具

  1. jps:查看Java进程的PID,是所有排查的第一步
  2. jstack:查看线程的堆栈信息,自动检测死锁,排查线程阻塞、死循环等问题
  3. jstat:查看JVM的运行状态,GC情况、内存使用情况
  4. jmap:查看堆内存信息,生成堆转储文件,排查OOM问题
  5. JConsole/VisualVM:可视化工具,监控JVM运行状态、线程状态、内存使用情况

5.2.2 死锁排查步骤

  1. jps -l获取Java进程的PID
  2. jstack <PID>查看线程堆栈信息,jstack会自动检测死锁,输出死锁的线程、持有的锁、等待的锁
  3. 根据堆栈信息,定位到代码中的死锁位置,按照避坑方法修复

5.2.3 线程安全问题排查步骤

  1. 复现问题,确定问题出现的场景和触发条件
  2. 检查共享变量的操作,是否保证了原子性、可见性、有序性
  3. 检查锁的使用是否正确,锁的范围是否合理,是否存在锁顺序问题
  4. 用日志、Arthas等工具,打印变量的修改过程,定位问题代码

六、总结

Java并发编程的核心,不是背会API和面试题,而是理解底层的JMM内存模型、锁的实现原理、AQS的核心逻辑。只有搞懂了底层原理,才能在实际项目中正确使用并发工具,避免踩坑,快速定位和解决线上问题。

目录
相关文章
|
1月前
|
JSON Java 数据格式
Feign 复杂对象参数传递避坑指南:从报错到优雅落地
本文深入剖析了SpringCloud Feign在复杂对象参数传递中的常见问题及解决方案。文章首先分析了GET请求传递复杂对象失败的底层原因,包括HTTP规范约束和Feign参数解析逻辑。针对GET场景,提供了四种解决方案:@SpringQueryMap(首选)、手动拆分属性+@RequestParam、MultiValueMap封装和自定义FeignEncoder,详细比较了各方案的优缺点和适用场景。对于POST场景,推荐使用@RequestBody注解传递JSON请求体。
392 5
|
2月前
|
机器学习/深度学习 自然语言处理 算法
大模型和机器学习
本文系统解析大模型与机器学习的关系,指出大模型是机器学习在“大参数、大数据、大算力”下的进化形态,二者为包含与被包含关系。文章从技术本质、能力特点、应用场景等10个维度对比分析,强调两者非替代而是互补,并展望融合发展趋势。
270 2
postman 传入不同组参数循环调用接口
postman 传入不同组参数循环调用接口
2022 0
postman 传入不同组参数循环调用接口
|
3月前
|
缓存 NoSQL Java
多级缓存架构实战指南
本文详解如何利用装饰器模式实现多级缓存架构,通过Caffeine、Redis与MySQL三级联动,兼顾高性能与数据一致性。采用SpringBoot实战,代码可落地,有效解决高并发场景下的缓存穿透、击穿、雪崩问题,提升系统稳定性与扩展性。
144 1
|
3月前
|
缓存 监控 NoSQL
吃透 JVisualVM 与 JConsole:Java 性能调优实战指南
本文详细介绍了Java性能调优工具JConsole和JVisualVM的使用方法。JConsole作为轻量级监控工具,适合快速排查线程死锁、内存异常等简单问题;JVisualVM则提供采样分析、内存快照、线程快照等高级功能,能深度诊断内存泄漏、CPU过高等复杂问题。文章通过实战案例演示了如何定位和解决线程死锁、CPU过高、内存泄漏等问题,并对比了两款工具的适用场景。核心建议:日常巡检用JConsole,深度分析用JVisualVM,同时强调生产环境使用时的安全注意事项。掌握这两款工具能有效提升Java应用性
380 4
|
3月前
|
安全 小程序 Java
微信支付全流程实战指南
本文从底层逻辑到实战代码,完整覆盖了微信支付Native/JSAPI支付、异步回调、退款、对账等核心能力。在实际项目中,需结合业务场景补充异常监控、资金告警、日志审计等能力,进一步保障支付系统的稳定性和资金安全。
377 6
|
3月前
|
网络协议 Java 数据安全/隐私保护
吃透OSI七层模型:从底层逻辑到实战落地,一文打通网络通信任督二脉
本文从“底层逻辑拆解+权威标准解读+可落地实战示例”三个维度,用通俗的语言讲透OSI七层模型的每一个细节。所有内容均参考ISO/IEC 7498-1官方标准(OSI模型的权威定义),核心论点100%有据可依;实战示例基于Java语言实现,确保可直接编译运行;同时针对易混淆技术点进行明确区分,帮你真正做到“知其然,更知其所以然”。
860 2
|
3月前
|
存储 监控 安全
吃透JUC框架:从底层原理到实战落地,解决并发编程90%的问题
本文从底层原理(AQS、CAS)出发,逐步拆解了JUC的核心组件,结合实战示例讲解了各组件的使用场景和最佳实践,最后总结了并发编程的常见问题和解决方案。掌握JUC框架,不仅能夯实并发编程基础,更能解决实际开发中的高并发、线程安全问题,提升系统的吞吐量和稳定性。
315 1
|
22天前
|
Prometheus 监控 Cloud Native
Prometheus+Grafana:一站式搞定监控告警全链路
本文详解Prometheus+Grafana监控体系:从核心原理(时序数据、4类指标、Pull采集、PromQL)到完整实战,涵盖服务器、Spring Boot应用监控搭建、告警配置与生产优化,助你构建实时、可视化、可告警的分布式系统“生命线”。
214 4
|
2月前
|
监控 Java 关系型数据库
解决 GitLab 响应超时:清理日志 + 重启服务一步到位
本文记录了一次GitLab响应超时问题的排查与解决过程。因Java项目日志堆积导致磁盘空间耗尽,引发GitLab服务卡顿甚至无法访问。通过“网络→服务→资源”的排查思路,定位到根分区使用率达98%,清理历史日志并重启GitLab后恢复正常。文中详细分享了操作步骤,并给出配置日志轮转、监控磁盘空间等避坑建议,帮助运维和开发人员快速应对类似故障,提升系统稳定性。
159 1