文档说明

整合全部基础、进阶、底层源码、冷门死角、面试考点、线上实战、JDK 新特性,全网最全无缺漏


第一部分 线程基础核心

1.1 基础概念(通俗易懂)

  1. 进程:操作系统资源分配的最小单位,独立内存、相互隔离。
  2. 线程:CPU调度最小单位,共享进程资源,开销极小。
  3. 并发:CPU时间片快速切换,同一时间段交替执行,宏观同时、微观串行。
  4. 并行:多CPU核心,同一时刻真正同时执行。
  5. 同步:线程排队执行,阻塞等待,数据安全。
  6. 异步:线程互不阻塞,后台执行,提高吞吐量。
  7. Java线程调度模型:抢占式调度,优先级仅为建议,不保证绝对执行顺序。
  8. 时间片轮转:CPU给线程分配时间片,时间结束强制切换线程。

1.2 线程六大生命周期(必考)

NEW新建 → RUNNABLE就绪运行 → BLOCKED阻塞锁 → WAITING无限等待 → TIMED_WAITING限时等待 → TERMINATED终止

  • NEW:new Thread,未调用start。
  • RUNNABLE:包含就绪+运行,等待CPU时间片。
  • BLOCKED:争抢synchronized失败,阻塞在锁池。
  • WAITING:无限等待,必须手动唤醒(wait/join)。
  • TIMED_WAITING:限时等待,时间到自动唤醒(sleep)。
  • TERMINATED:线程执行完毕。

1.2.1 线程六大生命周期思维导图(补全必考)

plain
线程六大生命周期
├─ ① NEW 新建状态
│  ├─ 触发:new Thread()
│  ├─ 特点:未启动、无执行、无CPU占用
│  └─ 切换:仅能调用start() → RUNNABLE
├─ ② RUNNABLE 就绪/运行状态
│  ├─ 就绪:抢到锁、等待CPU时间片
│  ├─ 运行:获取CPU、正在执行run方法
│  ├─ 触发:start()结束、阻塞解除、时间片轮转
│  └─ 流出:阻塞/等待/代码执行完毕
├─ ③ BLOCKED 阻塞锁状态
│  ├─ 触发:争抢synchronized内置锁失败
│  ├─ 存放位置:锁池
│  ├─ 特点:不占用CPU、不释放已持有锁
│  └─ 切换:获取锁 → RUNNABLE
├─ ④ WAITING 无限等待状态
│  ├─ 触发:wait()、join()、Lock条件等待
│  ├─ 存放位置:等待池
│  ├─ 特点:永久阻塞、主动释放锁
│  └─ 唤醒:notify()/notifyAll() → BLOCKED
├─ ⑤ TIMED_WAITING 限时等待
│  ├─ 触发:sleep()、wait(time)、join(time)
│  ├─ 特点:限时阻塞、时间到自动唤醒
│  ├─ sleep:不释放锁
│  └─ wait:释放锁
└─ ⑥ TERMINATED 终止状态
   ├─ 触发:代码执行完毕、异常终止
   ├─ 特点:线程彻底死亡、不可重启
   └─ 禁忌:禁止二次调用start()

1.3 线程基础属性

  1. 线程优先级:1~10,默认5;优先级分为最小(1)、普通(5)、最高(10);仅操作系统调度建议,Java不保证优先级生效;底层依赖操作系统时间片抢占,不能用来控制业务执行顺序。
  2. 守护线程(后台线程):后台服务线程;JVM只要剩下守护线程,直接退出;优先级极低;不能执行业务、不能关闭资源、不建议手动IO操作;典型例子:GC线程、JVM监控线程;生命周期跟随JVM。
  3. 用户线程:默认创建全部为用户线程;JVM必须等待所有用户线程执行完毕才会正常退出;专门执行业务代码、允许资源读写、事务操作。
  4. 线程组ThreadGroup:批量管理线程,统一中断、统一查看状态、统一捕获异常;树形结构、支持父子线程组;实际开发极少手动使用,JVM底层默认使用;可以批量中断防止线程泄露。
  5. 线程唯一标识(Id):线程全局唯一id,不可重复、不可修改;自增生成,JVM内部维护;常用于日志链路追踪、线程排查。
  6. 线程名称(Name):默认命名Thread-X;生产环境必须自定义线程名称,方便线上jstack排查故障;线程池务必自定义线程工厂命名。
  7. 线程是否存活(isAlive):NEW、TERMINATED判定为死亡;RUNNABLE/BLOCKED/WAITING/TIMED_WAITING判定为存活。
  8. 全局异常处理器UncaughtExceptionHandler:线程出现未捕获异常不会终止JVM,只会单独终止当前线程;默认异常打印简陋、线上无日志;生产必须自定义全局异常处理器,记录堆栈、报错位置、线程信息;杜绝线程静默死亡。
  9. 线程上下文类加载器:每个线程自带类加载器;父线程传递给子线程;Spring、Tomcat热加载、动态代理底层依赖;防止类加载泄漏。
  10. 线程禁止修改属性:线程进入TERMINATED终止状态后,禁止修改名称、优先级、是否守护线程;修改直接抛出非法状态异常。

1.4 四种线程创建方式(优劣对比)

  1. 继承Thread类:无法继承其他类,无返回值,不推荐。
  2. 实现Runnable接口:无返回值、无异常,解耦,常用。
  3. Callable+FutureTask:有返回值、可抛异常、支持泛型。
  4. 线程池创建:生产唯一推荐、复用线程、减少开销。

1.4.1 四种创建方式【完整可运行代码+详解】

① 继承 Thread 类

java
// 1、继承Thread
class MyThread extends Thread{
    @Override
    public void run() {
        System.out.println("线程执行:" + Thread.currentThread().getName());
    }
}
// 使用
public class Test{
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        thread.start(); // 开启线程
    }
}

② 实现 Runnable 接口

java
// 2、实现Runnable(最常用、无返回值)
class MyRunnable implements Runnable{
    @Override
    public void run() {
        System.out.println("Runnable 线程执行");
    }
}
// 使用
new Thread(new MyRunnable()).start();

③ Callable + FutureTask(有返回值、可抛异常)

java
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

// 3、Callable 带返回值
class MyCallable implements Callable<Integer>{
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable 执行计算");
        return 666;
    }
}
// 使用
public static void main(String[] args) throws Exception{
    FutureTask<Integer> task = new FutureTask<>(new MyCallable());
    new Thread(task).start();
    Integer res = task.get(); // 阻塞获取返回值
    System.out.println("结果:" + res);
}

④ 线程池方式(生产唯一推荐)

java
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

// 4、线程池创建线程
public static void main(String[] args) {
    // 创建固定线程池
    ExecutorService pool = Executors.newFixedThreadPool(3);
    for (int i = 0; i < 5; i++) {
        pool.execute(() -> {
            System.out.println("线程池执行任务:" + Thread.currentThread().getName());
        });
    }
    pool.shutdown(); // 关闭线程池
}

1.4.2 四种线程创建方式 优劣对比表(面试必背)

创建方式

返回值

异常处理

优缺点

使用场景

继承Thread

不可抛出检查异常

缺点:单继承限制、耦合高;优点:写法简单

简单测试、临时代码

实现Runnable

不可抛出检查异常

优点:避免单继承、解耦、复用;无返回值

通用异步任务、无需返回结果

Callable

有泛型返回值

可抛出异常

优点:有返回、可捕获异常;缺点:get()阻塞

需要计算结果、异步获取返回值

线程池

可控

统一异常处理

优点:线程复用、可控并发、性能高;缺点:上手复杂

生产环境、所有正式项目

1.4.3 核心面试总结

  • 为什么不推荐继承Thread? Java单继承机制,继承Thread后无法继承其他类,耦合严重。
  • Runnable和Callable区别? Runnable无返回、不能抛检查异常;Callable有返回、可抛异常。
  • 生产为什么必须用线程池? 频繁创建销毁线程开销极大、无法管控线程数量、容易OOM。

1.5 线程常用API(最详细区别)

方法

是否释放锁

是否释放CPU

唤醒条件

sleep()

不释放

释放

时间到自动唤醒

wait()

释放

释放

notify手动唤醒

yield()

不释放

释放

直接重新竞争CPU

join()

释放

释放

子线程执行完毕

  • start():只能调用一次,创建新线程。
  • run():普通方法调用,不开启线程。
  • interrupt():修改中断标记,不会直接终止线程。
  • 废弃方法:stop()、destroy(),暴力终止,数据不安全。

1.5.1 全部API深度精讲 + 面试坑点(独家补全)

① start() 启动线程

  • 作用:向JVM提交线程,进入就绪状态,由操作系统调度执行。
  • 底层:调用native本地方法,创建操作系统真实线程。
  • 硬性禁忌:一个线程只能调用一次start(),重复调用抛出 IllegalThreadStateException
  • 易错点:start()不是立刻执行,只是进入就绪队列,取决于CPU调度。

② run() 线程业务方法

  • 作用:承载线程业务逻辑。
  • 区别:直接调用run()就是普通同步方法,不创建新线程;只有start()才会开辟子线程。
  • 底层:JVM回调run(),用户不可手动触发线程调度。

③ sleep(long time) 线程休眠

  • 归属:Thread静态方法,在哪行代码调用、休眠当前线程。
  • 锁行为不释放锁、不释放资源
  • CPU:释放CPU执行权。
  • 异常:阻塞状态下被中断,抛出InterruptedException、清空中断标记。
  • 面试坑:sleep(0) 触发一次CPU时间片重新竞争。

④ wait() / wait(time) 线程等待

  • 归属:Object成员方法,所有对象都有。
  • 前提:必须在synchronized同步代码块中执行,否则抛异常。
  • 锁行为彻底释放锁,其他线程可抢占锁。
  • 存放位置:进入等待池。
  • 唤醒方式:无参必须手动唤醒;有参超时自动唤醒。

⑤ notify() / notifyAll() 唤醒线程

  • notify():随机唤醒等待池中一条线程(jdk无顺序、不公平)。
  • notifyAll():唤醒全部等待线程,进入锁池竞争锁。
  • 强制要求:必须持有当前对象锁才能唤醒。
  • 易错点:唤醒后不会立刻执行,需要重新争抢锁。

⑥ yield() 线程礼让

  • 作用:主动让出当前CPU时间片。
  • 锁行为:不释放锁。
  • 特点:礼让不保证成功,CPU可能再次选中当前线程。
  • 使用场景:低优先级线程给高优先级线程让步。

⑦ join() 线程插队

  • 作用:主线程阻塞,等待子线程执行完毕。
  • 锁行为:底层封装wait,释放锁
  • 底层原理:循环判断线程是否存活,存活则持续wait。
  • 使用场景:多线程任务合并、依赖执行结果。

⑧ interrupt() 线程中断(高频面试)

  • 本质:仅仅修改中断标记位,不会直接杀死线程。
  • 阻塞清除标记:sleep、wait、join 阻塞时被中断,抛出异常并且清空中断标记
  • 非阻塞保留标记:正常运行线程,标记永久保留,业务手动判断结束。
  • 配套方法:isInterrupted() 判断标记;interrupted() 判断并清空标记(静态方法)。

⑨ 废弃方法(禁止使用)

  • stop():暴力杀死线程,锁直接释放、数据错乱、事务断裂。
  • destroy():JDK未实现,源码空方法。
  • suspend()/resume():挂起线程,死锁风险极高,永久废弃。

1.5.2 四大阻塞方法 终极对比总结表(背诵版)

核心方法

所属类

释放锁

释放CPU

唤醒条件

使用位置

sleep()

Thread

❌ 不释放

✅ 释放

时间到自动唤醒

任意位置

wait()

Object

✅ 释放

✅ 释放

notify手动唤醒

必须同步代码块

yield()

Thread

❌ 不释放

✅ 释放

立刻重新竞争

任意位置

join()

Thread

✅ 释放

✅ 释放

子线程执行完毕

任意位置

1.5.3 高频面试简答题(标准答案)

  • 为什么wait必须放在同步代码块?:防止线程丢失唤醒、避免死锁,JVM语法强制校验。
  • sleep和wait最大区别?:sleep不释放锁;wait释放锁;sleep是线程方法;wait是对象方法。
  • interrupt能不能终止运行线程?:不能,仅修改标记,需要业务代码主动判断。
  • notify和notifyAll区别?:notify随机唤醒一条;notifyAll唤醒全部,解决线程饿死。
  • yield能否保证礼让?:不能,Java线程调度是抢占式,礼让后仍可再次抢到CPU。

1.6 线程正确停止方式(面试必考+代码实现)

面试核心结论:Java线程没有强制立刻终止的语法,全部为优雅终止;暴力方法全部废弃禁止使用。

1.6.1 线程四大停止方式(优劣汇总)

  1. 方式一:自定义布尔变量标记停止(简单业务、无阻塞):自定义flag变量,循环判断标记,手动退出。
  1. 方式二:interrupt() + 判断中断状态(官方推荐、通用):修改中断标记,配合判断停止线程,支持阻塞线程唤醒。
  1. 方式三:捕获InterruptedException响应中断(阻塞线程专用):sleep/wait/join阻塞时,捕获异常退出。
  1. 方式四:废弃禁用方法:stop()、destroy()、suspend(),暴力终止,生产绝对禁止。

1.6.2 方式一:自定义布尔标记(代码示例)

java
/**
 * 适用场景:无阻塞、纯循环业务、简单定时任务
 * 缺点:遇到sleep/wait阻塞无法及时停止
 */
public class StopFlagDemo {
    // 自定义 volatile 标记(保证可见性)
    private static volatile boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            while (flag) {
                System.out.println("线程正常运行...");
            }
            System.out.println("线程优雅停止");
        }).start();

        // 3秒后停止线程
        Thread.sleep(3000);
        flag = false;
    }
}

1.6.3 方式二:interrupt() 非阻塞线程停止(官方推荐)

java
/**
 * 适用场景:运行中线程、无阻塞、通用业务
 * 原理:仅修改中断标记,不会杀死线程,业务手动判断
 */
public class InterruptRunningDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                System.out.println("线程持续运行,未被中断");
            }
            System.out.println("线程检测到中断标记,优雅退出");
        });
        thread.start();

        Thread.sleep(3000);
        // 修改中断标记 = 发出停止信号
        thread.interrupt();
    }
}

1.6.4 方式三:interrupt() 停止阻塞线程(高频面试代码)

java
/**
 * 重点:sleep/wait/join 阻塞被中断 → 抛出异常 + 清空中断标记
 * 必须捕获异常完成退出
 */
public class InterruptSleepDemo {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            try {
                System.out.println("线程进入休眠,阻塞5秒");
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                // 阻塞状态被中断,自动清除标记
                System.out.println("线程被中断,捕获异常,优雅结束");
                // 可做资源释放、事务回滚
            }
        });
        thread.start();

        // 1秒后中断阻塞线程
        try {Thread.sleep(1000);} catch (Exception e) {}
        thread.interrupt();
    }
}

1.6.5 三大停止方式优缺点对比表(背诵版)

停止方式

优点

缺点

适用场景

自定义布尔标记

简单易懂、代码简洁

阻塞状态无法响应停止

无阻塞循环任务

interrupt()运行线程

安全优雅、无资源破坏

需要手动判断标记

通用绝大多数业务

interrupt()阻塞线程

可唤醒阻塞线程

中断标记会被清空

含sleep/wait阻塞任务

1.6.6 面试必考5句标准答案(必背)

  • 为什么不用stop()? 暴力杀死线程,不释放锁、数据错乱、事务断裂、资源无法回收。
  • interrupt()会不会直接终止线程? 不会,只是修改中断标记位,需要业务代码主动感知。
  • 阻塞线程中断有什么特点? 抛出异常并且自动清空中断标记。
  • 非阻塞线程中断有什么特点? 标记永久保留,不会自动清除。
  • 生产最优停止方案? interrupt()+异常捕获+资源手动释放,保证线程安全。

1.7 虚假唤醒(90%人写错|面试高频坑点)

1.7.1 什么是虚假唤醒?

定义:线程在没有被notify()/notifyAll()唤醒的情况下,无故、被动从WAITING状态苏醒

官方说明:JDK源码注释明确标注,JVM允许wait线程无理由唤醒,属于操作系统底层机制,并非Bug。

1.7.2 虚假唤醒产生原因

  • 操作系统CPU调度、内核随机性唤醒;
  • 多线程竞争下,锁池、等待池线程混乱流转;
  • JVM底层为了提高并发吞吐量,主动批量唤醒休眠线程。

1.7.3 致命错误:if 判断(千万不要写)

错误原理:if 只判断一次条件,虚假唤醒后不会二次校验,直接向下执行,造成数据错乱、越界、空指针

java
// ❌ 错误写法:if 判断,虚假唤醒直接BUG
synchronized (obj){
    if(flag == false){
        obj.wait(); // 一旦虚假唤醒,直接跳出if,不判断条件
    }
    // 无校验直接执行业务 → 数据错乱
}

1.7.4 正确写法:while 循环判断(强制背诵)

核心原理:唤醒后循环二次校验条件,不满足条件继续休眠,杜绝虚假唤醒

java
// ✅ 标准写法:while循环、JDK官方强制规范
synchronized (obj){
    while(flag == false){
        obj.wait(); // 虚假唤醒后,再次判断条件,不满足继续等待
    }
    // 条件合法,安全执行业务
}

1.7.5 完整版生产者消费者代码(防虚假唤醒)

java
/**
 * 经典面试题:生产者消费者模型
 * 重点:必须while、禁止if,解决虚假唤醒
 */
public class FalseWakeUpDemo {
    // 仓库标记:是否有商品
    private static boolean hasGoods = false;

    public static void main(String[] args) {
        // 消费者
        new Thread(() -> {
            synchronized (FalseWakeUpDemo.class){
                // while循环反复校验
                while (!hasGoods){
                    try {
                        FalseWakeUpDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("消费者:成功消费商品");
                hasGoods = false;
                FalseWakeUpDemo.class.notifyAll();
            }
        },"消费者").start();

        // 生产者
        new Thread(() -> {
            synchronized (FalseWakeUpDemo.class){
                while (hasGoods){
                    try {
                        FalseWakeUpDemo.class.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("生产者:成功生产商品");
                hasGoods = true;
                FalseWakeUpDemo.class.notifyAll();
            }
        },"生产者").start();
    }
}

1.7.6 面试必考5句标准答案(必背)

  1. 什么是虚假唤醒? 线程未被notify唤醒,无故自动苏醒,属于操作系统底层机制。
  2. 为什么不能用if? if只判断一次,虚假唤醒后无条件执行,导致业务异常。
  3. 为什么要用while? 循环二次校验条件,不满足继续等待,从根源杜绝虚假唤醒。
  4. JDK官方要求? 所有wait()必须包裹在while循环内,是强制编码规范。
  5. 虚假唤醒能否避免? 不能根除,只能通过while循环规避风险。

1.8 线程通信方式(最全6种|面试必考)

核心概念:多线程并行执行,彼此隔离;为了完成协同工作、数据传递、任务调度,需要线程之间进行通信。Java一共6种正规线程通信方式

  1. 内置锁通信:wait、notify、notifyAll。
  2. 管道流通信:Piped输入输出流,线程之间传输数据。
  3. 共享变量通信:volatile标记变量。

1.8.1 六种通信方式总览(背诵清单)

  1. 共享变量通信:基础方式,配合volatile保证可见性。
  2. 内置锁等待唤醒:wait() / notify() / notifyAll()(synchronized)。
  3. 显式锁精准唤醒:Lock + Condition(await/signal)。
  4. 管道流通信:PipedInputStream / PipedOutputStream。
  5. 并发工具类通信:CountDownLatch、CyclicBarrier、Semaphore。
  6. 中间媒介通信:并发容器BlockingQueue阻塞队列。

1.8.2 方式一:共享变量通信(最简单)

原理:多个线程共享同一成员变量,使用volatile保证可见性,实现信号传递。

缺点:无法精准阻塞、只能简单标记、不能精准唤醒。

java
public class VolatileComm {
    // 共享标记变量
    private static volatile boolean flag = false;
    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag){}
            System.out.println("子线程感知标记变化,执行完毕");
        }).start();
        // 主线程修改变量,通知子线程
        Thread.sleep(2000);
        flag = true;
    }
}

1.8.3 方式二:内置锁 wait/notify(经典生产者消费者)

特点:Object自带方法、必须在synchronized中、唤醒随机、存在虚假唤醒。

痛点:只能全部唤醒/随机唤醒,无法分组,粒度粗糙。

java
// 前文虚假唤醒代码一致,必须while循环判断
synchronized (obj){
    while (!flag){obj.wait();}
    obj.notifyAll();
}

1.8.4 方式三:Lock+Condition 精准通信(推荐)

底层:AQS条件队列,实现分组阻塞、精准唤醒。

优势:避免无效竞争、性能高、不会虚假唤醒、生产常用。

java
public class ConditionComm {
    private static final ReentrantLock LOCK = new ReentrantLock();
    // 区分生产者、消费者条件队列
    private static final Condition PRODUCE = LOCK.newCondition();
    private static final Condition CONSUME = LOCK.newCondition();
    private static boolean hasGoods = false;

    // 生产者
    public static void produce(){
        LOCK.lock();
        try {
            while (hasGoods){PRODUCE.await();}
            System.out.println("生产商品");
            hasGoods = true;
            CONSUME.signal(); // 精准唤醒消费者
        } catch (Exception e) {e.printStackTrace();}
        finally {LOCK.unlock();}
    }
}

1.8.5 方式四:管道流Piped(字节流通信)

唯一专门用于线程数据传输,不依赖共享变量,底层操作系统管道。

适用:线程之间传递字符串、字节数据,极少业务使用。

java
// 管道输出流(写)、管道输入流(读)
PipedOutputStream out = new PipedOutputStream();
PipedInputStream in = new PipedInputStream(out);

1.8.6 方式五:阻塞队列BlockingQueue(生产最常用)

原理:put()阻塞、take()阻塞,自带锁+唤醒机制,无需手动加锁。

优点:解耦、安全、简洁、并发容器封装好。

java
// 阻塞队列实现生产者消费者
BlockingQueue<String> queue = new ArrayBlockingQueue<>(10);
// 生产者阻塞放入
queue.put("商品");
// 消费者阻塞取出
String take = queue.take();

1.8.7 方式六:JUC工具类通信(流程控制)

  • CountDownLatch:等待多线程完成,一次性通信。
  • CyclicBarrier:线程互相等待,集齐再出发。
  • Semaphore:信号量通信,控制限流互斥。

1.8.8 六种通信方式 终极对比表(背诵)

通信方式

是否手动加锁

唤醒精度

优缺点

使用场景

共享变量volatile

无唤醒

简单、只能做标记

一写多读、停止标记

wait/notify

是(synchronized)

随机唤醒

底层老旧、存在虚假唤醒

初级面试手写生产者消费者

Lock+Condition

是(Lock)

精准唤醒

高性能、无虚假唤醒

复杂线程协同、分组等待

管道流Piped

IO阻塞

专门传数据、极少用

简单字节传输

阻塞队列

内置锁

自动唤醒

极简、生产首选

消息队列、异步解耦

JUC工具类

内置锁

批量唤醒

流程控制极强

并发限流、批量等待

1.8.9 面试必考5句标准答案(必背)

  • 线程通信本质? 让独立线程之间数据共享、任务协同、执行互相控制。
  • 为什么弃用notify? 随机唤醒,产生线程饿死,生产必须notifyAll或Condition。
  • Condition优势? 精准唤醒、避免无效竞争、解决虚假唤醒、灵活性高。
  • 阻塞队列原理? 封装Lock+Condition,屏蔽底层锁操作,极简通信。
  • 生产最优通信方式? 优先阻塞队列,复杂协同使用Lock+Condition。


第二部分 Java 内存模型 JMM(并发底层基石)

2.1 JMM作用

JMM全称:Java Memory Model(Java内存模型),是Java官方制定的一套内存访问规范、抽象逻辑模型,不是硬件内存、不是堆内存,是一种语法规则。


一、底层诞生原因(面试必问)


1、硬件层面:现代CPU多级缓存、缓存不一致、CPU乱序执行;
2、编译器层面:JIT编译器为优化性能,会对代码指令重排;
3、系统层面:不同操作系统内存架构差异极大;
4、语言层面:Java需要跨平台,必须统一并发内存规范。


二、核心作用(标准答案背诵)


1、屏蔽硬件内存差异:统一Windows、Linux、Mac等不同平台的内存访问逻辑,实现Java一次编写、到处运行;
2、约束多线程内存交互:规定线程、工作内存、主内存三者之间读写交互规则;
3、解决并发三大致命问题:通过内存屏障、指令规则保证原子性、可见性、有序性
4、提供高层并发语法:给volatile、synchronized、Lock、CAS提供底层内存规则支撑。


三、人话通俗解释


没有JMM:CPU乱执行、缓存数据不一致、多线程代码毫无逻辑、并发完全失控;
有了JMM:Java给你封装好底层硬件差异,程序员无需关心CPU缓存、总线、指令排序,只需要用关键字就能保证并发安全。


四、JMM三大核心组成


1、内存交互指令(8大指令);

2、内存屏障;

3、Happens-Before先行发生规则。

2.2 八大内存交互指令(必考|JMM底层执行规范)

核心概念:JMM定义了8种内存交互指令,严格规定主内存 ↔ 工作内存之间变量读写交互方式,所有多线程变量操作,底层都必须遵循这8条指令,不可违背。

2.2.1 八大指令逐条详解(背诵版)

  1. lock(锁定):作用于主内存变量,把变量标记为线程独占状态,禁止其他线程修改;常用于锁操作、原子操作。
  2. unlock(解锁):作用于主内存变量,释放锁定的变量,其他线程可争抢锁定该变量;必须搭配lock使用。
  3. read(读取):从主内存读取变量值,传输到工作内存,为load指令做准备。
  4. load(载入):将read读取到的数据,存入当前线程的工作内存副本中。
  5. use(使用):把工作内存中的变量值,传递给线程执行引擎,用于代码运算、逻辑处理。
  6. assign(赋值):线程运算完毕后,将最新结果赋值给工作内存中的变量副本。
  7. store(存储):将工作内存中修改后的变量,传输到主内存,为write指令做准备。
  8. write(写入):把store传输的数据,写入并更新主内存的共享变量。

2.2.2 变量完整读写流程(面试画图题)

读取流程(主内存→线程):主内存 → read读取 → load载入 → use使用(线程执行)

写入流程(线程→主内存):assign赋值 → store传出 → write写入 → 主内存更新

锁操作流程:lock锁定主内存变量 → 独占操作 → unlock解除锁定

2.2.3 八大强制语法规则(90%人记不全)

  1. read和load必须成对出现:不能单独读取、不载入;保证主内存数据成功进入工作内存。
  2. store和write必须成对出现:不能单独传输、不写入;保证工作内存数据刷回主内存。
  3. 变量不允许无原因丢弃:read后必须load、store后必须write。
  4. 不允许无赋值直接刷主内存:工作内存必须经过assign修改,才能执行store。
  5. use/assign必须有序:线程内变量使用、赋值遵循代码顺序。
  6. lock可重复加锁:加锁次数必须等于解锁次数(可重入锁底层规则)。
  7. lock锁定后,其他线程禁止操作该主内存变量。
  8. unlock必须解锁当前线程已锁定的变量,禁止解锁别人的锁。

2.2.4 通俗易懂人话总结

  • read+load:把数据从硬盘(主内存)拿到缓存(工作内存)。
  • use+assign:CPU运算修改缓存里的数据。
  • store+write:把修改后的缓存数据刷回硬盘。
  • lock+unlock:给变量上锁、解锁,保证排他性。

2.2.5 面试高频简答题(必背)

  • 八大指令作用? 规范主内存与工作内存的数据交互规则,是JMM最底层执行指令。
  • 为什么read和load必须成对? 防止数据丢失、保证主内存数据完整载入工作内存。
  • volatile如何依托指令实现可见性? 强制修改后立刻store+write刷回主内存,其他线程read+load刷新最新值。
  • synchronized依托哪两个指令? lock加锁、unlock解锁,实现原子性和排他性。

2.3 并发三大特性(面试必考|并发核心基石)

核心概述:多线程并发编程存在三大安全问题:原子性丢失、可见性失效、有序性错乱。JMM所有规则、锁、关键字(volatile/synchronized/Lock)本质都是为了解决这三大特性问题。

  1. 原子性:操作不可分割,中途不会被打断。
  2. 可见性:一个线程修改变量,其他线程立刻感知。
  3. 有序性:禁止编译器、CPU乱序执行代码。

2.3.1 原子性(Atomicity)

定义:一组操作不可分割、不可中断、要么全部执行成功,要么全部执行失败,线程切换不能打断执行过程。

① 易错认知

  • 基本数据类型赋值:int a = 10 是原子操作;
  • 自增运算 i++ 不是原子操作(读取-修改-赋值三步指令);
  • 复合运算、多步运算全部非原子。

② 代码演示:原子性丢失(经典BUG)

java
/**
 * 多线程自增,原子性丢失
 * 预期结果:10000,实际结果永远小于10000
 */
public class AtomicBugDemo {
    private static int count = 0;
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) count++;
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5000; i++) count++;
        });
        t1.start();t2.start();
        t1.join();t2.join();
        // 结果永远小于10000
        System.out.println("最终计数:" + count);
    }
}

③ 原子性解决方案

  1. synchronized:底层lock加锁,保证操作不可中断;
  1. Lock显式锁:手动独占锁,保证代码块原子性;
  1. CAS原子类:无锁自旋,底层硬件指令保证原子性。

④ 面试标准答案

原子性是指操作不可被分割,多线程下不会被线程调度打断;volatile不保证原子性,自增、复合运算必须加锁或原子类。

2.3.2 可见性(Visibility)

定义:一个线程修改共享变量值,其他线程能够立刻感知到最新修改值,不会一直读取工作内存旧数据。

① 可见性丢失原因

  • CPU多级缓存优化,每个线程拥有独立工作内存;
  • 线程修改数据优先存在工作内存,不会立刻刷回主内存;
  • 其他线程不会主动刷新主内存数据,导致永久读取旧值。

② 代码演示:可见性丢失

java
/**
 * 没有volatile修饰,主线程修改标记,子线程永远感知不到
 * 子线程死循环,程序无法结束
 */
public class VisibilityBugDemo {
    private static boolean flag = true;
    public static void main(String[] args) {
        new Thread(() -> {
            while (flag){
                // 无IO打印,JIT编译器优化,永久缓存flag旧值
            }
            System.out.println("子线程结束");
        }).start();
        // 修改标记
        flag = false;
    }
}

③ 可见性解决方案

  1. volatile:强制修改立即刷回主内存,读取强制拉取最新主内存数据;
  1. synchronized:解锁前刷新数据,加锁后读取最新数据;
  1. Lock锁:同内置锁,保证内存可见性。

④ 面试标准答案

可见性是解决多线程缓存不一致问题;底层依靠MESI缓存一致性协议+内存屏障;volatile专门解决可见性

2.3.3 有序性(Ordering)

定义:代码执行顺序按照编写代码顺序执行,禁止编译器、CPU为了优化性能打乱指令执行顺序。

① 指令重排三类(必考)

  1. 编译器重排:JIT编译优化,调整代码顺序;
  1. CPU指令重排:CPU乱序执行,提高吞吐;
  1. 内存系统重排:缓存读写延迟造成顺序错乱。

② 重排危害(经典DCL漏洞)

创建对象三步:开辟内存 → 初始化对象 → 引用指向内存;指令重排后会变成:开辟内存 → 引用指向内存 → 初始化对象,出现半初始化对象,线程获取残缺对象,程序崩溃。

③ 有序性解决方案

  1. volatile:添加内存屏障,禁止上下指令重排;
  1. synchronized:同步代码块内代码串行,禁止重排;
  1. 内存屏障:四类屏障强制限制指令顺序。

④ 面试标准答案

有序性防止指令重排,单线程不会重排,多线程共享变量会出现重排漏洞;volatile依靠内存屏障禁止重排,解决DCL单例漏洞。

2.3.4 三大特性 终极对比总结表(必背)

特性

作用

volatile

synchronized/Lock

CAS原子类

原子性

保证操作不可中断

❌ 不支持

✅ 支持

✅ 支持

可见性

保证数据实时同步

✅ 支持

✅ 支持

✅ 支持

有序性

禁止指令乱序执行

✅ 支持

✅ 支持

❌ 不保证

2.3.5 高频5句面试真题(满分背诵)

  • 并发三大特性是什么? 原子性、可见性、有序性。
  • volatile能保证哪些特性? 可见性 + 有序性,不保证原子性
  • i++为什么线程不安全? 分三步执行,无原子性,多线程覆盖丢失数据。
  • 指令重排什么时候发生? 单线程不会出错,多线程共享变量才会出现安全漏洞。
  • synchronized三大特性? 全部保证:原子性、可见性、有序性。

2.4 指令重排(三类|底层必考|并发乱序根源)

核心定义:编译器、CPU、内存系统为了提升执行性能、优化吞吐、减少空闲损耗,在不改变单线程语义的前提下,打乱代码原有编写顺序,重新排序指令执行流程,该现象称为指令重排。

重要前提单线程下指令重排绝对安全(JMM遵守as-if-serial语义);只有多线程共享变量场景下,重排才会产生致命BUG。

2.4.1 三类指令重排逐条详解

① 编译器优化重排(编译阶段)

  • 触发时机:Java源码编译为Class字节码、JIT即时编译阶段。
  • 执行主体:Javac编译器、JIT编译器(HotSpot)。
  • 优化目的:剔除无效代码、合并重复指令、调整代码顺序,减少CPU指令执行次数,提升编译后执行效率。
  • 重排规则:不改变单线程代码执行结果,无数据依赖的代码随意调换顺序。
  • 通俗举例:无关赋值代码,编译器自动调换执行顺序,无业务影响。

java
// 原始代码
int a = 1;
int b = 2;
// 编译器重排后(合法、无影响)
int b = 2;
int a = 1;

② CPU指令重排(运行阶段)

  • 触发时机:CPU执行机器指令阶段。
  • 执行主体:CPU硬件执行单元。
  • 优化目的:现代CPU具备多级流水线、超标量执行能力,打乱指令顺序,让空闲CPU执行单元不闲置,最大化压榨CPU算力。
  • 重排规则:无硬件数据依赖的指令,CPU乱序执行。
  • 特点:硬件层面重排,JVM无法直接干预,只能通过内存屏障禁止。

③ 内存系统重排(缓存阶段)

  • 触发时机:主内存与CPU多级缓存交互阶段。
  • 执行主体:CPU缓存、内存总线。
  • 优化目的:弥补CPU运算速度与内存读写速度的巨大差距,缓存异步刷写、延迟写入。
  • 产生原因:线程工作内存修改的数据,不会立即同步刷入主内存,缓存读写延迟,造成逻辑顺序错乱。
  • 隐蔽性:最难排查、最容易被忽略的重排,线上偶发BUG大多源于此。

2.4.2 核心两大重排约束规则(面试必背)

① as-if-serial 语义(单线程保护伞)

定义:无论如何重排,单线程内所有代码执行结果,必须和代码书写顺序执行结果完全一致

约束范围:所有编译器、CPU、内存重排都必须遵守该规则。

人话总结:单线程随便重排,不会出BUG;多线程共享变量,该规则失效。

② 数据依赖性(重排禁止红线)

如果两条指令存在读写依赖、写读依赖、写写依赖,绝对禁止重排;只有无依赖的无关指令,才允许重排优化。

java
// 存在数据依赖,禁止重排
int a = 1;
int b = a + 1; // 依赖a的值,顺序不可颠倒

2.4.3 指令重排致命危害(多线程场景)

单线程无危害,多线程无同步措施、共享变量读写场景下,重排会导致逻辑错乱、数据异常、半初始化对象。

经典案例:DCL单例重排漏洞(复盘)

正常创建对象三步指令:1、开辟堆内存空间 → 2、初始化对象成员变量 → 3、引用变量指向堆内存。

指令重排后错乱指令:1、开辟堆内存空间 → 3、引用变量指向堆内存 → 2、初始化对象。

危害结果:其他线程获取到未初始化完成的半初始化对象,调用属性出现空指针、数据错乱。

2.4.4 禁止指令重排四大方案

  1. volatile关键字(最常用):插入内存屏障,禁止上下指令重排,专门解决多线程重排问题。
  2. synchronized内置锁:同步代码块内代码串行执行,关闭重排优化。
  3. Lock显式锁:底层AQS保证代码串行,禁止指令乱序。
  4. 内存屏障:底层硬件指令,强制固定指令执行顺序。

2.4.5 面试必考标准答案(精简背诵)

  • 指令重排是什么? 编译器、CPU、内存为优化性能,在不违背单线程语义下打乱指令顺序。
  • 三类重排优先级? 编译器重排 → CPU指令重排 → 内存系统重排。
  • 什么时候会出BUG? 仅多线程共享变量、无同步措施时产生漏洞,单线程永远安全。
  • 如何禁止重排? volatile内存屏障、加锁、串行化执行。
  • as-if-serial作用? 保证单线程重排结果不变,是Java重排核心规则。

2.5 四大内存屏障

LoadLoad、StoreStore、LoadStore、StoreLoad(禁止相邻指令重排)。

2.5.1 内存屏障核心概念

内存屏障(Memory Barrier):也叫内存栅栏,是CPU底层硬件指令,用于禁止相邻指令发生重排序、强制刷新内存数据,保障多线程下可见性与有序性。内存屏障不会占用CPU运算时间,仅做指令排序拦截,是volatile关键字底层实现的核心依赖。Java将硬件屏障封装为四大类型,精准控制读写指令顺序。

2.5.2 四大内存屏障逐条详解(背诵版)

(1).LoadLoad 屏障(读-读屏障)

格式:Load1 <LoadLoad> Load2。

作用:禁止两次读取指令重排,保证屏障前的读操作优先执行完毕,再执行屏障后的读操作。

底层规则:禁止CPU将后面的读指令插队到前面读指令之前。

通俗举例:先读取A变量、再读取B变量,屏障保证A读完再读B,不会颠倒顺序。

(2).StoreStore 屏障(写-写屏障)

格式:Store1 <StoreStore> Store2。

作用:禁止两次写入指令重排,保证屏障前的写操作刷入主内存后,再执行后续写操作。

底层规则:屏蔽缓存延迟写入,防止多写指令乱序刷盘。

通俗举例:先修改A、再修改B,屏障保证A写入主内存后,再写入B。

(3).LoadStore 屏障(读-写屏障)格式

Load1 <LoadStore> Store2。

作用:禁止前面读取指令、后面写入指令发生重排,保证读操作完全结束后,再执行写操作。

底层规则:阻断读指令后置、写指令前置的乱序行为。

通俗举例:先读取A数据,再修改B数据,屏障禁止颠倒执行顺序。

(4).StoreLoad 屏障(写-读屏障|最重屏障)

格式:Store1 <StoreLoad> Load2。

作用:禁止前面写入、后面读取指令重排;唯一全能屏障,同时刷新写缓存、刷新读缓存。

底层规则:写操作强制刷入主内存,后续读操作强制从主内存拉取最新数据。

特点:四大屏障中开销最大、功能最强,volatile写操作底层默认插入该屏障。

通俗举例:修改完A数据,必须刷入主内存,后续读取B数据直接从主内存读取最新值。

2.5.3 volatile内存屏障插入规则(必考)

  • volatile写操作:指令末尾插入StoreLoad屏障,强制修改数据立刻刷入主内存,对其他线程可见。
  • volatile读操作:指令开头插入LoadLoad+LoadStore屏障,禁止前置指令重排,强制读取主内存最新数据。
  • 无volatile普通变量:无任何内存屏障,允许CPU、编译器自由重排。

2.5.4 面试必考标准答案(精简5句)

  1. 四大屏障分类? LoadLoad(读读)、StoreStore(写写)、LoadStore(读写)、StoreLoad(写读)。
  2. 最强屏障? StoreLoad,开销最大、兼顾读写刷新,volatile写依赖它。
  3. 屏障核心作用? 禁止指令重排、强制内存同步,保障有序性+可见性。
  4. volatile底层屏障? 读加Load系列屏障,写加StoreLoad屏障。
  5. 使用场景? 无锁并发、volatile、CAS底层均依赖内存屏障。

2.6 Happens-Before 八条规则(必背)

  1. 程序顺序规则:单线程代码有序。
  2. volatile规则:写先行于读。
  3. 锁规则:解锁先行于加锁。
  4. 线程启动规则:start()先行于内部代码。
  5. Join规则:子线程结束先行于主线程后续代码。
  6. 中断规则:中断代码先行于异常捕获。
  7. 对象终结规则:初始化先行于销毁。
  8. 传递性:A先行B、B先行C,则A先行C。

2.7 MESI缓存一致性协议

CPU硬件层保证缓存数据一致性,是volatile可见性底层原理。

2.8 伪共享(高并发性能大坑|面试冷门高频)

原理:多个变量存放在同一个缓存行,一个修改导致全部失效。

解决:@Contended 填充缓存行、变量隔离。

2.8.1 核心基础:CPU缓存行(Cache Line)

缓存行定义:CPU缓存的最小存储单位,主流操作系统默认大小为64字节。CPU从内存读取数据时,不会单独读取单个变量,而是一次性读取相邻整块64字节的数据存入缓存行。

底层规则:缓存行是CPU缓存交互的最小粒度,无论变量大小,只要占用同一缓存行,就会绑定缓存状态,互相影响。

2.8.2 什么是伪共享?

官方定义:多个相互独立、无业务关联的共享变量,存储在同一个CPU缓存行中;当其中一个变量被修改,会导致整个缓存行失效,其他无关联变量强制刷新缓存,触发不必要的缓存一致性同步,造成严重性能损耗。

人话通俗解释:多个互不干扰的变量挤在同一个缓存格子里,改其中一个,整个格子全部作废,其他变量必须重新从主内存加载,频繁触发MESI缓存同步,拖慢并发执行速度。

2.8.3 伪共享产生核心原因

  • CPU缓存行固定64字节,多个小变量会被压缩存入同一缓存行;
  • 多线程分别修改同一缓存行内不同变量;
  • MESI缓存一致性协议触发缓存行失效,频繁刷写主内存;
  • 无锁并发场景下,缓存同步开销远超代码执行开销。

2.8.4 危害(高并发致命坑)

  1. 性能断崖式下跌:无业务竞争的变量产生虚假竞争,大量CPU资源浪费在缓存同步;
  2. 并发吞吐量降低:多线程并行执行变串行缓存刷新,丧失CPU并行优势;
  3. 线上偶发性能卡顿:低并发无感知,高并发流量下性能雪崩,排查难度极高;
  4. 无报错无异常:代码逻辑完全正确,仅底层缓存层面性能损耗。

2.8.5 代码演示:伪共享性能对比

① 错误写法:产生伪共享(变量挤在同一缓存行)

java
/**
 * 伪共享BUG演示
 * 两个独立变量,共存于一个缓存行,互相干扰
 * 执行速度极慢,缓存频繁失效
 */
class FalseSharingDemo{
    // 两个long变量,各8字节,共16字节,挤入同一个64字节缓存行
    public volatile long a = 0;
    public volatile long b = 0;
}
public class FalseSharingTest {
    public static void main(String[] args) throws InterruptedException {
        FalseSharingDemo demo = new FalseSharingDemo();
        // 线程1循环修改a
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 100000000L; i++) demo.a = i;
        });
        // 线程2循环修改b
        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 100000000L; i++) demo.b = i;
        });
        long start = System.currentTimeMillis();
        t1.start();t2.start();
        t1.join();t2.join();
        // 耗时极高:约800ms+
        System.out.println("伪共享耗时:" + (System.currentTimeMillis() - start));
    }
}

② 优化写法:缓存行填充(杜绝伪共享)

java
/**
 * 缓存行填充优化:添加占位变量,独占缓存行
 * 一个long=8字节,填充7个long占位,合计64字节,占满单个缓存行
 */
class SolveFalseSharingDemo{
    public volatile long a = 0;
    // 填充占位变量,占满缓存行剩余空间
    public long p1,p2,p3,p4,p5,p6,p7;
    public volatile long b = 0;
}
// 测试代码一致,优化后耗时直接减半,约300ms以内

2.8.6 主流解决方案(生产通用)

方式一:手动缓存行填充(JDK7之前):添加无用占位变量,凑满64字节,强制变量隔离不同缓存行;代码冗余、可读性差,目前基本淘汰。

方式二:@Contended注解(官方推荐):JDK8+提供的注解,自动进行缓存行填充,让被修饰变量独占一个缓存行,彻底杜绝伪共享;底层JVM编译优化,无需手动写占位变量。

方式三:变量隔离:将高频修改的独立变量拆分不同实体类,物理隔离缓存存储位置。

2.8.7 @Contended注解使用规范

java
// 注解修饰类/字段,自动缓存行填充
@sun.misc.Contended
class ContendedDemo{
    public volatile long value;
}

注意事项:IDEA默认生效;生产环境JVM需添加启动参数开启注解优化:-XX:-RestrictContended

2.8.8 典型应用场景(源码落地)

  • LongAdder分段计数器:JDK8分段锁累加器,每个分段变量独立缓存行,规避伪共享,超高并发性能碾压AtomicLong;
  • Disruptor高性能队列:开源无锁队列,大量使用缓存行填充,适配百万级TPS;
  • 线程池、并发容器:JUC底层高频变量,全部做缓存隔离优化。

2.8.9 面试必考5句标准答案(必背)

  1. 伪共享是什么? 多个无关变量共存同一缓存行,修改一个导致整行缓存失效,产生虚假竞争。
  2. 缓存行大小? 默认64字节,是CPU缓存最小读写单位。
  3. 产生条件? 多线程修改同一缓存行内不同独立变量。
  4. 最优解决方案? JDK8+使用@Contended注解,自动缓存行填充。
  5. 典型应用? LongAdder分段计数底层规避伪共享,高并发性能极强。

第三部分 Volatile关键字

简洁:

  1. 状态标记位。
  2. DCL双重检查锁单例。
  3. 一写多读场景。

3.1 三大作用(底层原理+通俗详解+面试必背)

3.1.1 保证可见性(核心作用)

底层原理:强制刷新CPU缓存,volatile修饰的变量,写操作后立刻刷入主内存,读操作强制从主内存加载最新数据,不走线程本地工作内存缓存。

通俗解释:一个线程改了变量,其他线程立刻能感知到最新值,不会读取旧缓存数据。

底层依托:MESI缓存一致性协议 + 内存屏障。

适用场景:一写多读的并发场景,杜绝可见性丢失BUG。

3.1.2 禁止指令重排(解决底层漏洞)

底层原理:变量读写前后插入内存屏障,禁止编译器、CPU、内存系统三类指令重排,固定代码执行顺序。

通俗解释:防止代码执行顺序被打乱,避免出现半初始化、空指针等诡异BUG。

经典落地:DCL双重检查锁单例模式,依靠volatile禁止对象创建指令重排。

屏障规则:volatile读加Load系列屏障,volatile写加StoreLoad全能屏障。

3.1.3 不保证原子性(高频面试坑点)

底层原因:volatile仅保证内存有序同步,无法锁住CPU指令,复合操作(自增、赋值计算、多变量联动)会被线程打断。

易错代码count++ 自增操作,读取-修改-赋值三步指令,volatile无法保证三步不可分割。

解决方案:配合synchronized锁、Lock显式锁、CAS原子类实现原子性。

面试话术:volatile是轻量级同步机制,牺牲原子性换取性能,专注解决可见性与有序性。

3.2 使用场景(补全生产场景+落地代码)

volatile在生产中严格遵循一写多读核心原则,禁止多写场景,以下为3类高频落地场景,附带完整可运行代码。

3.2.1 场景一:状态标记位(服务启停、任务开关)

适用场景:后台定时任务、服务热部署、线程启停管控、网关熔断开关;单线程修改标记,多线程监听标记状态。

核心优势:volatile保证标记实时可见,无锁、性能极高。

java
/**
 * 业务场景:后台定时日志采集任务,动态启停
 * 一写多读:主线程修改开关,多条任务线程监听开关
 */
public class VolatileFlagDemo {
    // volatile保证标记可见性
    private static volatile boolean collectSwitch = true;

    public static void main(String[] args) throws InterruptedException {
        // 开启3条日志采集线程
        for (int i = 1; i <= 3; i++) {
            new Thread(() -> {
                while (collectSwitch) {
                    // 模拟日志采集业务
                    System.out.println(Thread.currentThread().getName() + ":正在采集业务日志");
                    try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}
                }
                System.out.println(Thread.currentThread().getName() + ":采集任务停止");
            }, "采集线程" + i).start();
        }

        // 5秒后关闭采集开关(单线程修改)
        Thread.sleep(5000);
        collectSwitch = false;
        System.out.println("主线程:手动关闭日志采集开关");
    }
}

3.2.2 场景二:DCL双重检查锁单例模式(生产最常用)

适用场景:工具类、配置类、连接池等全局唯一对象;延迟加载、节省内存、兼顾线程安全。

volatile作用:禁止对象创建指令重排,杜绝半初始化对象逃逸,解决DCL漏洞。

java
/**
 * 业务场景:全局配置单例类
 * 标准DCL写法,必须加volatile
 */
public class ConfigSingleton {
    // volatile:禁止指令重排,防止半初始化对象
    private static volatile ConfigSingleton instance;

    // 私有构造,禁止外部new
    private ConfigSingleton(){}

    // 双重检查锁获取单例
    public static ConfigSingleton getInstance(){
        // 第一次判断:无锁拦截,避免频繁加锁
        if (instance == null) {
            synchronized (ConfigSingleton.class) {
                // 第二次判断:防止多线程穿透锁
                if (instance == null) {
                    instance = new ConfigSingleton();
                }
            }
        }
        return instance;
    }

    // 模拟配置业务方法
    public void printConfig(){
        System.out.println("加载全局业务配置");
    }

    // 测试
    public static void main(String[] args) {
        ConfigSingleton.getInstance().printConfig();
    }
}

3.2.3 场景三:一写多读数据监听(配置热更新)

适用场景:Nacos/Apollo配置热更新、动态参数配置、限流阈值动态修改;后台线程监听配置,业务线程读取配置。

核心逻辑:单独线程修改配置,大量业务线程无锁读取,保证配置实时同步。

java
/**
 * 业务场景:动态限流阈值配置
 * 后台线程修改阈值,接口请求线程读取阈值
 */
public class VolatileConfigDemo {
    // 动态限流阈值
    private static volatile int limitNum = 10;

    // 模拟业务请求线程(多读)
    static class RequestTask implements Runnable{
        @Override
        public void run() {
            while (true) {
                System.out.println(Thread.currentThread().getName() + ":当前限流阈值=" + limitNum);
                try {Thread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        // 开启5条业务请求线程
        for (int i = 1; i <= 5; i++) {
            new Thread(new RequestTask(), "请求线程" + i).start();
        }

        // 8秒后后台修改限流阈值(单写)
        Thread.sleep(8000);
        limitNum = 50;
        System.out.println("后台配置线程:修改限流阈值为50");
    }
}

3.2.4 禁止使用场景(避坑总结)

  1. 禁止多写场景:多个线程同时修改volatile变量,无法保证原子性,数据覆盖错乱;
  2. 禁止复合运算:i++、i=i+1、多变量联动计算,必须加锁或CAS;
  3. 禁止做事务变量:事务一致性要求原子性,volatile无法保障。

3.3 冷门易错点(95%程序员踩坑|深度补全)

简洁:

  1. 不能修饰构造方法。
  2. 防止构造方法逃逸。
  3. volatile数组:仅引用可见,内部元素不可见。
  4. DCL必须加volatile:防止指令重排导致半初始化对象。

详细:

  1. 修饰权限限制:volatile不能修饰构造方法、接口方法、局部变量;仅能修饰类成员变量、静态成员变量,修饰局部变量直接编译报错。
  2. 防止构造方法逃逸:对象引用被volatile修饰时,禁止在构造方法中向外发布当前对象引用,避免未初始化完成的对象被其他线程获取,造成引用逃逸BUG。
  3. volatile数组特殊性:volatile仅修饰数组引用地址,保证数组引用可见性;数组内部元素不具备可见性、有序性,多线程修改数组元素仍存在线程安全问题。
  4. DCL单例强制加volatile底层原因:new对象分为开辟空间、初始化、引用赋值三步,无volatile会发生指令重排,出现引用赋值优先于初始化,导致其他线程获取半初始化空对象。
  5. volatile不阻塞线程:volatile属于无锁机制,不会造成线程阻塞、无上下文切换,开销远小于synchronized,是最轻量级同步关键字。
  6. volatile不能修饰常量:被final修饰的常量不可修改,搭配volatile无任何意义,编译器会优化剔除修饰符,代码无报错但冗余。
  7. volatile变量禁止编译优化:普通变量会被JIT编译器缓存、指令重排优化,volatile变量禁用缓存优化,每次强制从主内存读取,禁止代码合并、剔除。
  8. 线程切换可见性延迟:volatile可见性并非绝对实时,依赖CPU缓存刷写时机,高并发下存在纳秒级微弱延迟,不会影响业务,无法做到毫秒级绝对同步。
  9. 不支持复合操作易错点:除i++外,i = i + 1、i += 2、判断赋值联动等复合操作,均无法保证原子性,底层多条CPU指令极易被线程打断。
  10. volatile与final区别:final保证变量不可修改、编译期常量;volatile保证变量内存可见、禁止重排,二者作用完全不同,不可混用替代。
  11. 锁释放优先级:volatile无法替代锁,多线程写场景下,即使加volatile,仍需加锁保证原子性,volatile仅做辅助内存同步。
  12. 空值可见性:volatile修饰引用变量,置为null时,同样会刷新主内存,其他线程可实时感知null状态,无缓存残留。

3.4 局限性

无法解决复合运算、自增、多线程修改等非原子操作。


第四部分 Synchronized 内置锁

4.1 使用方式

  1. 修饰普通实例方法:对象锁

  2. 修饰静态方法:类锁

  3. 修饰同步代码块:自定义对象锁(生产最常用、粒度最小)

4.1.1 三种使用方式+完整实现代码

① 修饰普通实例方法(对象锁)

锁对象:当前实例对象 this;

锁范围:同一个对象多条线程访问同步方法互斥阻塞;不同对象互不干涉、不阻塞。

适用场景:非静态成员变量、对象级别资源竞争。

/**
 * 1、实例方法锁(锁住当前对象 this)
 * 多线程同一对象访问互斥,不同对象不互斥
 */
public class SyncInstanceDemo {
    // 修饰普通实例方法
    public synchronized void printLog(){
        System.out.println(Thread.currentThread().getName() + ":正在执行实例方法");
        try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
    }

    public static void main(String[] args) {
        SyncInstanceDemo demo = new SyncInstanceDemo();
        // 同一对象,锁互斥、串行执行
        new Thread(demo::printLog,"线程A").start();
        new Thread(demo::printLog,"线程B").start();
    }
}

② 修饰静态方法(类锁)

锁对象:当前类的Class字节码对象,全局唯一;

锁范围所有实例全部互斥,不管new多少对象,共用同一把类锁。

适用场景:静态共享资源、全局唯一变量。

/**
 * 2、静态方法锁(锁住当前类.class)
 * 无论创建多少个对象,全部互斥
 */
public class SyncStaticDemo {
    // 修饰静态方法
    public static synchronized void staticMethod(){
        System.out.println(Thread.currentThread().getName() + ":执行静态类锁方法");
        try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
    }

    public static void main(String[] args) {
        // 创建两个不同对象,依旧互斥(类锁全局唯一)
        new Thread(SyncStaticDemo::staticMethod,"线程1").start();
        new Thread(SyncStaticDemo::staticMethod,"线程2").start();
    }
}

③ 修饰同步代码块(自定义锁|生产最优)

锁对象:自定义任意引用类型对象;

锁范围:锁住同一把自定义锁对象线程互斥;锁对象不同则不互斥。

适用场景:精准控制临界区、缩小锁范围、高并发优化。

优势:锁粒度最小、灵活控制、性能最高、不锁住整个方法。

/**
 * 3、同步代码块(自定义锁对象)
 * 精准控制锁范围,缩小临界区,生产推荐
 */
public class SyncBlockDemo {
    // 自定义锁对象(必须是引用类型,不能是基本类型、不能为null)
    private final Object lock = new Object();

    public void businessMethod(){
        // 只锁住核心并发代码,无关代码不锁
        System.out.println(Thread.currentThread().getName() + ":执行非并发普通逻辑");
        // 同步代码块
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + ":进入临界区,执行并发安全逻辑");
            try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
        }
    }

    public static void main(String[] args) {
        SyncBlockDemo demo = new SyncBlockDemo();
        new Thread(demo::businessMethod,"业务线程1").start();
        new Thread(demo::businessMethod,"业务线程2").start();
    }
}

4.1.2 加锁底层原理(面试必背)

  1. 实例方法锁:隐式锁定 this,字节码无 monitorenter/monitorexit,标记 ACC_SYNCHRONIZED。

  2. 静态方法锁:隐式锁定 类.class,全局唯一 Class 锁。

  3. 代码块锁:字节码生成 monitorenter、monitorexit 指令,手动进出锁。

4.1.3 使用方式易错点

  1. 锁对象禁止为:null、基本类型、常量池字符串(无效锁、死锁隐患);

  2. 实例锁 & 类锁互不阻塞,混用造成线程安全漏洞;

  3. 锁范围越大性能越差,优先使用同步代码块缩小临界区。

4.1.4 极易踩坑:锁失效四大场景

  1. 锁对象地址发生改变:锁对象不能为可变引用(Integer、String、Boolean),引用一改直接锁失效。
  2. 实例锁 & 静态锁混用:两把不同锁,并行执行,并发安全彻底失效。
  3. 同一类多把不同锁:锁对象不同,互不阻塞,达不到互斥效果。
  4. 加锁代码无共享变量:锁错代码位置,锁住无关代码,临界区裸露造成线程不安全。

4.1.5 面试5句标准答案(满分背诵)

  • 区别? 实例锁锁this、静态锁锁Class、代码块锁自定义对象。
  • 粒度? 静态锁 > 实例锁 > 同步代码块。
  • 底层? 代码块靠monitor指令,方法锁靠标记位。
  • 互斥? 实例锁和静态锁互不阻塞。
  • 生产? 优先同步代码块,缩小锁范围提高并发。

4.2 核心特性

  1. 隐式加锁、自动释放锁

  2. 可重入锁,同一线程可多次获取

  3. 线程发生异常自动释放锁

  4. 不可中断、不可超时获取

4.3 对象头与 MarkWord

存储锁状态、线程 ID、GC 分代年龄、偏向线程信息,是锁升级载体

4.4 JDK1.6 锁升级全过程

完整升级流向:无锁 ➔ 偏向锁 ➔ 轻量级锁(自适应自旋) ➔ 重量级锁;JDK1.6对synchronized做大量优化,锁升级单向不可逆,只能升级、不能降级,目的是在不同竞争场景下平衡性能,尽可能减少重量级锁开销。

  1. 偏向锁:单线程无竞争,无开销

  2. 轻量级锁:多线程交替执行,自适应自旋

  3. 重量级锁:激烈竞争,阻塞排队

4.4.1 MarkWord 存储结构(锁升级载体)

Java对象头MarkWord占用8字节,运行时动态存储:锁标记位、偏向线程ID、GC年龄、时间戳;锁状态全部依靠修改MarkWord二进制标识实现切换

锁状态

标识位(2bit)

偏向位(1bit)

特征

无锁

01

0

无偏向、无竞争

偏向锁

01

1

偏向单一线程

轻量级锁

00

自旋竞争、无阻塞

重量级锁

10

阻塞队列、内核态

4.4.2 四级锁逐阶段详解(面试必背)

① 无锁状态(初始状态)
  • 场景:对象创建出来,无任何线程争抢。

  • MarkWord:哈希码、GC分代年龄、偏向位0。

  • 特点:无任何锁开销,纯普通对象。

② 偏向锁(单线程最优、无竞争)
  • 触发条件:JVM默认延迟4秒开启,只有单线程反复加锁、无其他线程竞争。

  • 底层原理:第一次加锁记录当前线程ID到MarkWord,后续该线程再次加锁,不做任何CAS、不切换锁,零开销直接获取锁。

  • 优点:消除频繁加锁CAS开销,单线程性能极致。

  • 撤销触发条件:出现第二条线程竞争、调用hashCode、批量重偏向阈值触发。

③ 轻量级锁(交替竞争、无阻塞)
  • 触发条件:偏向锁撤销,多线程交替执行、无同时争抢

  • 底层原理:线程在栈帧创建锁记录LockRecord,通过CAS替换MarkWord指针;失败进行自适应自旋

  • 自适应自旋:JDK1.6优化,不再固定次数自旋;根据过往竞争情况动态调整自旋次数,避免盲目空转消耗CPU。

  • 优点:用户态自旋、不阻塞、不切换内核态、性能高。

  • 升级条件:自旋次数过多、同一时刻多线程并发争抢,直接膨胀重量级锁。

④ 重量级锁(激烈竞争、阻塞排队)
  • 触发条件:并发量大、竞争激烈、自旋失败、CPU空转严重。

  • 底层原理:JVM向操作系统申请互斥量Mutex,未抢到锁的线程进入阻塞队列,放弃CPU时间片,挂起等待。

  • 缺点:涉及用户态→内核态切换、线程阻塞、上下文切换、开销极大。

  • 唤醒机制:持有锁线程释放后,JVM唤醒队列首位线程抢占锁。

4.4.3 锁升级触发流程图解(背诵版)

  1. 对象新建 → 无锁(01);

  2. 单线程反复加锁 → 偏向锁(01 偏向位1);

  3. 出现第二条线程竞争 → 撤销偏向锁 → 升级轻量级锁(00);

  4. 并发激烈、自旋失败 → 膨胀重量级锁(10);

  5. 升级规则只能升级、不能降级,不可逆

4.4.4 偏向锁批量机制(冷门面试)

  1. 批量重偏向:同一类大量对象发生偏向锁竞争,阈值20,JVM判定竞争频繁,将后续对象直接偏向新线程。

  2. 批量撤销:阈值40,竞争极度频繁,判定偏向锁无意义,直接关闭该类所有对象偏向锁,永久使用轻量级锁。

4.4.5 锁升级高频面试必背(8句满分)

  1. JDK1.6之后新增锁优化:偏向锁、轻量级锁、自适应自旋;

  2. 锁升级顺序:无锁→偏向锁→轻量级锁→重量级锁;

  3. 单向不可逆,永远不会降级;

  4. 偏向锁适合单线程,轻量级适合交替执行,重量级适合激烈竞争;

  5. 偏向锁延迟加载,默认启动4秒后开启;

  6. 轻量级锁依靠CAS+自适应自旋,不阻塞;

  7. 重量级锁依赖操作系统Mutex互斥量,线程阻塞;

  8. 偏向锁一旦调用hashCode直接撤销偏向标记。

4.5 JVM 四大锁优化机制(JDK1.6 官方优化|面试必考)

JDK1.6 之后 JVM 对 synchronized 进行大量底层优化,除锁升级以外,还包含锁粗化、锁消除、偏向锁优化、自适应自旋四大优化,目的:减少加锁开销、减少CAS竞争、减少阻塞、最大化提升内置锁性能。

简洁:

  1. 锁粗化:合并连续加锁

  2. 锁消除:逃逸分析判定无共享直接消除锁

  3. 偏向锁撤销、批量重偏向、批量撤销

  4. JVM 参数关闭偏向锁:-XX:-UseBiasedLocking

4.5.1 锁粗化(Lock Coarsening)

原理:JVM 检测到连续多次对同一把锁频繁加锁、解锁,会自动合并锁范围,将多次加锁合并为一次,避免频繁进出锁消耗性能。

触发场景:循环内频繁加锁、连续重复加锁解锁。

优化前(低效)

// 循环内频繁加解锁,严重浪费资源
for(int i = 0; i < 100; i++){
    synchronized (lock){
        // 简单逻辑
    }
}

JVM 优化后(自动粗化)

// 把锁提到循环外,只加锁一次
synchronized (lock){
    for(int i = 0; i < 100; i++){
        // 简单逻辑
    }
}

注意事项:开发禁止故意在循环内加锁,即便JVM优化,也会加重编译开销。

4.5.2 锁消除(Lock Elimination)

原理:JVM 通过逃逸分析,判断锁对象只会在当前线程内部使用、不会逃逸到多线程,判定无并发竞争,直接消除锁,锁代码失效。

核心底层:逃逸分析(JDK1.6默认开启),判断对象是否逃逸当前线程。

代码示例(锁会被自动消除)

public void testLockClear(){
    // 局部对象,永远不会逃逸,不存在多线程竞争
    Object lock = new Object();
    synchronized (lock){
        System.out.println("锁会被JVM直接消除");
    }
}

面试考点:方法内部new锁对象,大概率被锁消除;不要在方法内定义锁。

4.5.3 偏向锁优化(批量重偏向+批量撤销)

该优化专门解决大量对象频繁偏向竞争问题,是JDK1.6专为高并发对象创建场景优化。

  1. 批量重偏向(阈值 20):一类对象超过20次偏向竞争,JVM判定线程切换频繁,将后续新建对象直接偏向新竞争线程,减少撤销开销。

  2. 批量撤销(阈值 40):一类对象竞争超过40次,判定偏向锁无意义,直接永久关闭该类所有对象偏向锁,默认使用轻量级锁。

JVM参数:-XX:BiasedLockingStartupDelay=4000 默认4秒延迟开启偏向锁。

4.5.4 自适应自旋锁(Adaptive Spinning)

原理:JDK1.6 摒弃固定自旋次数,JVM根据前一次锁竞争情况,动态调整自旋次数。

  1. 上次自旋成功抢到锁 ➜ 本次自旋次数增加;

  2. 上次自旋失败阻塞 ➜ 本次减少自旋次数,甚至不自旋;

优点:避免盲目自旋空转浪费CPU,兼顾低竞争、高竞争场景性能。

4.5.5 锁优化关闭参数(面试冷门)

  1. 关闭偏向锁:-XX:-UseBiasedLocking

  2. 关闭逃逸分析:-XX:-DoEscapeAnalysis(关闭后不会锁消除)

  3. 修改批量偏向阈值:-XX:BiasedLockingBulkRebiasThreshold=20

4.5.6 锁优化面试必背(7句满分话术)

  1. JDK1.6四大锁优化:锁粗化、锁消除、偏向锁批量优化、自适应自旋;

  2. 锁粗化:合并频繁加锁,减少锁切换开销;

  3. 锁消除:逃逸分析判定无竞争,直接抹除锁;

  4. 批量重偏向:阈值20,偏向新线程;

  5. 批量撤销:阈值40,彻底关闭偏向锁;

  6. 自适应自旋:动态调整自旋次数,不固定循环;

  7. 所有优化目的:尽量不要进入重量级锁、不要进入内核态。

4.6 易错点

  1. 锁对象不能为 null:synchronized 锁对象为 null,直接抛出空指针异常;锁对象引用不可中途修改。

  2. 实例锁与静态类锁互不互斥:一把锁this、一把锁class,两种锁完全不冲突,并行执行,引发线程安全漏洞。

  3. 禁止使用字符串字面量做锁:例如synchronized("lock"),字符串常量池复用,不同业务、不同类共用同一把锁,莫名阻塞、死锁隐患极大。

  4. 禁止包装类缓存对象做锁:Integer、Long缓存-128~127,多个地方使用同一个数字锁,锁对象重合导致诡异阻塞。

  5. 锁对象不可动态修改引用:加锁期间如果修改锁对象引用,线程切换为不同锁对象,锁彻底失效,数据错乱。

  6. 同步方法不能修饰构造方法:语法报错,构造方法天生线程安全,不需要加锁。

  7. 子类不会继承父类同步锁:父类同步方法,子类重写后默认不带锁,必须手动重新加锁。

  8. 代码异常自动释放锁(重大坑点):同步代码报错,JVM强制释放锁,极易造成数据半更新、事务残缺、脏数据。

  9. 同步块内sleep不释放锁:sleep只让出CPU,不会释放Monitor,其他线程全部阻塞,极易造成线程卡顿积压。

  10. 循环内频繁加锁性能极差:哪怕JVM锁粗化优化,也不要在循环内部写锁,加重编译开销、降低吞吐量。

  11. 方法内部新建锁对象必定锁失效:每次进入方法new不同锁,线程互不争抢,锁完全无效。

  12. 偏向锁调用hashCode直接失效:偏向锁状态下,一旦调用hashCode,强制撤销偏向锁,直接升级轻量级锁。

  13. 偏向锁存在4秒延迟开启:程序刚启动前4秒,全部为轻量级锁,并非偏向锁。

  14. synchronized不可中断:线程阻塞等待锁时,interrupt无法打断阻塞,这是和Lock最大区别。

  15. 空同步代码块依旧生成指令:空锁代码无业务意义,但仍生成monitor指令,浪费CPU开销。

  16. 锁粗化不会跨方法优化:仅优化同一方法内连续加锁,跨方法频繁加锁不会合并。

  17. 逃逸分析开启才会锁消除:关闭JVM逃逸分析,局部对象锁不会消除,白白浪费性能。

  18. 重量级锁切换内核态开销巨大:一旦膨胀为重量级锁,用户态切内核态,上下文切换严重卡顿。

  19. 锁只能锁住堆内存对象:常量、静态常量、元空间对象不适合做业务锁,容易全局锁粘连。

  20. synchronized不能穿透线程逃逸:锁内new对象逃逸到外部,依旧存在并发安全问题。

4.6.1 synchronized 锁失效十大场景(面试必考)

  1. 锁对象不一致:时而锁this、时而锁class,混用锁导致失效。

  2. 锁对象被重新赋值:中途修改锁引用,多线程锁不同对象。

  3. 方法内部new临时锁:每次锁都是新对象,锁完全无效。

  4. 字符串常量池锁复用:不同业务共用同一字面量字符串锁。

  5. 加锁代码逻辑没有共享变量:锁加了但无意义,不保护临界资源。

  6. volatile搭配synchronized顺序错乱:无法提升原子性,纯属多余。

  7. 子类重写丢失同步修饰:子类方法不加锁,打破父类线程安全。

  8. 锁范围过大、无关代码加锁:阻塞正常业务,吞吐量暴跌。

  9. 自旋时间过长CPU飙高:高并发下轻量级锁自旋空转,CPU占用100%。

  10. 异常吞锁:try-catch包住同步代码,异常自动释放锁引发脏数据。

4.6.2 synchronized 终极背诵总结(一页纸)

  1. 底层:对象头MarkWord + Monitor监视器锁;

  2. 升级:无锁→偏向→轻量级→重量级,单向不可逆;

  3. 优化:锁粗化、锁消除、自适应自旋、批量偏向;

  4. 特性:可重入、自动解锁、异常释放、不可中断;

  5. 用法:优先同步代码块、缩小临界区、不要锁常量、不要锁null;

  6. 坑点:混用锁、修改锁引用、字面量锁、循环内加锁、异常丢数据。


第五部分 Lock 显式锁体系

5.1 ReentrantLock 可重入锁(重点必考)

ReentrantLock 是 JDK 显式锁核心实现,基于 AQS 底层实现,手动加锁、手动解锁、灵活性远超 synchronized,是企业生产中最常用的显式锁。

  1. 手动 lock () 加锁、unlock () 释放锁

  2. 支持公平锁、非公平锁

  3. 灵活性远超 synchronized

5.1.1 核心特性

  1. 显式加解锁:手动 lock () 加锁、unlock () 释放锁,可控性极强

  2. 可重入特性:同一线程可反复多次加锁,不会自己阻塞自己

  3. 锁模式可选:支持公平锁、非公平锁(默认非公平)

  4. 可中断阻塞:线程阻塞等待锁时可被中断

  5. 超时防死锁:支持限时获取锁,避免永久死锁

  6. 精准条件唤醒:配合 Condition 实现分组唤醒

5.1.2 完整使用代码(基础标准写法|生产模板)

import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock 标准生产写法
 * 必须:try-finally、必须手动 unlock、防止异常死锁
 */
public class ReentrantLockDemo {
    // 默认非公平锁;传入true为公平锁
    private static final ReentrantLock lock = new ReentrantLock(false);

    public static void businessTask(){
        // 手动加锁
        lock.lock();
        try {
            // 临界区:并发安全代码
            System.out.println(Thread.currentThread().getName() + " 获取锁,执行业务逻辑");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            // 【强制规范】finally 释放锁,防止异常死锁
            lock.unlock();
            System.out.println(Thread.currentThread().getName() + " 释放锁");
        }
    }

    public static void main(String[] args) {
        // 开启三条线程竞争锁
        new Thread(ReentrantLockDemo::businessTask,"线程A").start();
        new Thread(ReentrantLockDemo::businessTask,"线程B").start();
        new Thread(ReentrantLockDemo::businessTask,"线程C").start();
    }
}

5.1.3 可重入原理(面试必考底层)

ReentrantLock 依靠 AQS 的 state 状态变量实现可重入:

  1. 线程第一次加锁:state = 0 ➜ CAS 修改为 1,记录当前独占线程;

  2. 同一线程再次加锁:判断当前持有锁线程为本线程,state + 1

  3. 重复加锁 N 次,state 累加为 N;

  4. 解锁一次 state - 1,必须加锁次数 = 解锁次数,state 归零才算真正释放锁。

5.1.4 公平锁 & 非公平锁(源码级区别)

① 非公平锁(默认)
  1. 特点:新线程直接抢占锁,不排队、插队执行

  2. 优点:吞吐量高、CPU 切换少、性能极强

  3. 缺点:老线程可能长期饥饿

  4. 底层:上来直接 CAS 抢锁,不管队列是否有等待线程

② 公平锁
  1. 特点:严格按照队列顺序排队,先进先出,禁止插队

  2. 优点:线程公平、无饥饿现象

  3. 缺点:大量上下文切换、吞吐量低、性能差

  4. 底层:先判断队列是否有前驱节点,有则入队排队

5.1.5 四种加锁方式对比(进阶API)

加锁方法

特性

生产用途

lock()

阻塞加锁、不可中断

普通业务加锁

lockInterruptibly()

可中断阻塞

需要终止卡死线程

tryLock()

无阻塞尝试、立即返回

快速判断锁占用

tryLock(time)

限时等待、超时放弃

防死锁、超时降级

5.1.6 ReentrantLock 高频易错点(冷门坑)

  1. 必须手动解锁:忘记 unlock 永久死锁、线程卡死,必须写在 finally。

  2. 加锁解锁次数必须一致:重入加锁几次,必须解锁几次,state 不归零锁不释放。

  3. 不可在 if 内解锁:防止异常跳过解锁,必须固定 finally。

  4. 非公平锁性能远高于公平锁:生产默认非公平,除非业务强制公平。

  5. 锁内不要写耗时IO:占用锁时间过长,大量线程阻塞积压。

  6. 不能空解锁:未加锁直接 unlock,直接抛出异常。

5.1.7 面试满分总结(背诵)

  1. ReentrantLock 基于 AQS,依靠 state 实现可重入;

  2. 默认非公平锁,吞吐量高,适合绝大多数业务;

  3. 四大加锁方式:阻塞、可中断、尝试、限时;

  4. 必须 finally 解锁,加解锁次数一致;

  5. 比 synchronized 灵活:可中断、可超时、可公平、多条件唤醒。

5.2 进阶获取锁方式

  1. lockInterruptibly ():可中断锁,阻塞可被打断

  2. tryLock ():无阻塞尝试获取

  3. tryLock (time):超时限时获取,防死锁

5.3 Condition 条件队列

实现精准唤醒,替代 notifyAll,区分不同业务线程唤醒,解决内置锁唤醒粗暴、无法精准控制的痛点,是 Lock 体系专属的等待/通知组件

5.3.1 Condition 核心介绍

Condition 是 JDK1.5 随 Lock 推出的条件等待队列,绑定显式锁 ReentrantLock,替代 synchronized 的 wait()、notify()、notifyAll()。

核心最大优势:精准唤醒,可以将线程分组,指定唤醒某一组线程,不会盲目全部唤醒,减少无效竞争,极大提升并发性能。

5.3.2 核心常用 API

  1. await():阻塞等待、释放锁,等效于 wait()

  2. signal():唤醒单个等待线程,等效于 notify()

  3. signalAll():唤醒全部等待线程,等效于 notifyAll()

  4. awaitNanos():限时等待、支持超时自动唤醒

  5. awaitUninterruptibly():等待过程不可被中断

5.3.3 底层原理(面试必考)

  1. 每一个 Condition 内部维护一条独立单向条件等待队列

  2. 调用 await():当前线程释放锁,封装为节点加入条件队列;

  3. 调用 signal():将条件队列头部节点转移到 AQS 同步队列;

  4. 等待节点抢到锁后,从 await() 处继续执行。

5.3.4 Condition VS 内置锁等待队列(区别)

对比项

synchronized(wait/notify)

Condition(await/signal)

队列数量

单个等待队列

可创建多个独立条件队列

唤醒方式

随机唤醒、全部唤醒,无法精准控制

分组精准唤醒,指定线程唤醒

锁类型

内置隐式锁

显式 Lock 锁专属

超时等待

支持有限

多种超时、不可中断等待

5.3.5 生产实战代码(生产者消费者|经典模板)


import java.util.ArrayDeque; import java.util.Queue; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; /** * Condition 精准唤醒:生产者消费者模型 * 分开两个条件队列:生产者队列、消费者队列 * 不会虚假唤醒、不会盲目唤醒 */ public class ConditionDemo { // 仓库队列 private static final Queue<String> QUEUE = new ArrayDeque<>(5); // 显式锁 private static final ReentrantLock LOCK = new ReentrantLock(); // 生产者条件队列:仓库满就等待 private static final Condition PRODUCER_COND = LOCK.newCondition(); // 消费者条件队列:仓库空就等待 private static final Condition CONSUMER_COND = LOCK.newCondition(); // 生产者 public static void produce() { LOCK.lock(); try { // 防止虚假唤醒,必须while循环 while (QUEUE.size() >= 5) { // 仓库满,生产者阻塞等待 PRODUCER_COND.await(); } QUEUE.add("商品"); System.out.println(Thread.currentThread().getName() + " 生产商品,库存:" + QUEUE.size()); // 精准唤醒消费者 CONSUMER_COND.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { LOCK.unlock(); } } // 消费者 public static void consume() { LOCK.lock(); try { while (QUEUE.isEmpty()) { // 仓库空,消费者阻塞等待 CONSUMER_COND.await(); } QUEUE.poll(); System.out.println(Thread.currentThread().getName() + " 消费商品,库存:" + QUEUE.size()); // 精准唤醒生产者 PRODUCER_COND.signal(); } catch (InterruptedException e) { e.printStackTrace(); } finally { LOCK.unlock(); } } public static void main(String[] args) { // 生产者线程 new Thread(() -> {for (int i = 0; i < 10; i++) produce();}, "生产者").start(); // 消费者线程 new Thread(() -> {for (int i = 0; i < 10; i++) consume();}, "消费者").start(); } }

5.3.6 高频易错点(面试踩坑)

  1. await() 必须在循环中判断:防止多线程下虚假唤醒,和 wait 使用规范一致。

  2. Condition 必须绑定 Lock:未加锁直接调用 await/signal 直接抛出异常。

  3. 读锁不能创建 Condition:ReadLock.newCondition() 直接报错,仅写锁可用。

  4. 禁止混用条件队列:生产者、消费者必须分开队列,防止唤醒错乱。

  5. await 自动释放锁:和 wait 一样,阻塞时主动释放当前 Lock 锁。

5.3.7 面试满分总结(背诵5句)

  1. Condition 是显式锁专属条件队列,替代 wait/notify;

  2. 支持多队列分组,实现精准唤醒,性能优于内置锁;

  3. 底层维护单向条件队列,节点转移至AQS同步队列完成唤醒;

  4. await必须while循环、必须加锁、finally解锁;

  5. 生产多用于生产者消费者、线程精准阻塞唤醒场景。

5.4 读写锁 ReentrantReadWriteLock(读多写少专用)

ReentrantReadWriteLock 是 JDK 高性能读写分离锁,分为读锁(共享锁)写锁(排他锁),专门优化读多写少业务场景,大幅提升并发吞吐量,例如:缓存、配置类、静态数据。

  1. 读共享、写独占

  2. 支持写锁降级为读锁,禁止读锁升级写锁

5.4.1 四大锁互斥规则(面试必考)

  1. 读与读:共享不互斥:多个线程同时加读锁,完全并行,无阻塞。

  2. 读与写:互斥阻塞:有读锁,写锁阻塞;有写锁,读锁阻塞。

  3. 写与写:互斥阻塞:写锁独占,同一时刻只能一个线程修改。

  4. 写锁降级读锁:允许(唯一降级规则)。

5.4.2 核心特性

  1. 读写分离:拆分读锁、写锁,粒度更细,并发度更高。

  2. 可重入:读锁、写锁均支持同一线程重复加锁。

  3. 锁降级机制:写锁可以降级为读锁,禁止读锁升级写锁

  4. 支持公平/非公平:默认非公平,吞吐量更高。

  5. 锁饥饿问题:非公平模式下,读多写少容易造成写锁长期饥饿。

5.4.3 完整生产代码(缓存模拟)


import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.locks.ReentrantReadWriteLock; /** * 读写锁实战:本地缓存工具类 * 读多写少、读共享、写独占 */ public class ReadWriteLockDemo { // 模拟缓存容器 private static final Map<String,Object> CACHE_MAP = new ConcurrentHashMap<>(); // 创建读写锁 private static final ReentrantReadWriteLock RW_LOCK = new ReentrantReadWriteLock(); // 拆分读锁、写锁 private static final ReentrantReadWriteLock.ReadLock READ_LOCK = RW_LOCK.readLock(); private static final ReentrantReadWriteLock.WriteLock WRITE_LOCK = RW_LOCK.writeLock(); // 写数据(独占锁) public static void put(String key,Object value){ WRITE_LOCK.lock(); try { System.out.println(Thread.currentThread().getName() + " 写入缓存"); CACHE_MAP.put(key,value); } finally { WRITE_LOCK.unlock(); } } // 读数据(共享锁) public static Object get(String key){ READ_LOCK.lock(); try { System.out.println(Thread.currentThread().getName() + " 读取缓存"); return CACHE_MAP.get(key); } finally { READ_LOCK.unlock(); } } public static void main(String[] args) { // 多线程读 for (int i = 0; i < 5; i++) { new Thread(() -> get("name"),"读线程"+i).start(); } // 单线程写 new Thread(() -> put("name","Java并发文档"),"写线程").start(); } }

5.4.4 锁降级(面试高频难点)

定义:持有写锁的线程,获取读锁后,释放写锁,由写锁变为读锁。

规则硬性要求

  1. 允许:写锁 ➜ 读锁(降级)

  2. 禁止:读锁 ➜ 写锁(升级):会死锁

锁降级标准代码模板

// 锁降级流程:写锁 → 读锁 → 释放写锁
WRITE_LOCK.lock();
try {
    // 1、修改数据
    data = 666;
    // 2、提前加读锁(降级)
    READ_LOCK.lock();
} finally {
    // 3、先释放写锁,完成降级
    WRITE_LOCK.unlock();
}
// 降级后:当前线程持有读锁,其他线程可读
try {
    System.out.println("降级后读取数据:"+data);
} finally {
    // 最后释放读锁
    READ_LOCK.unlock();
}

5.4.5 AQS底层计数原理(冷门面试)

ReentrantReadWriteLock 将 AQS 的 state 32位拆分使用:

  1. 高16位:读锁计数(共享锁、线程个数)

  2. 低16位:写锁计数(独占锁、重入次数)

5.4.6 优缺点总结

优点

  1. 读操作并行,极大提高读多写少场景吞吐量;

  2. 保证写操作原子性、独占性,数据安全;

  3. 细粒度锁,性能优于普通独占锁。

缺点

  1. 非公平模式下写锁容易饥饿;

  2. 不支持锁升级,升级直接死锁;

  3. 读写交替频繁场景性能反而变差。

5.4.7 高频易错点(生产踩坑)

  1. 禁止读锁升级写锁:自身持有读锁,无法获取写锁,永久死锁。

  2. 写锁必须降级:数据修改后需要立刻可见,降级保证数据一致性。

  3. 读锁不支持条件队列:readLock.newCondition() 直接报错。

  4. 写锁一定要最后释放:降级顺序不能颠倒。

  5. 大量读锁积压会饿死写锁:高并发读场景建议使用 StampedLock。

5.5 StampedLock(JDK8)

StampedLock 是 JDK8 新增的**改进型读写锁**,优化 ReentrantReadWriteLock 写锁饥饿问题,新增乐观读模式,无锁开销、超高并发性能,专门适配超高读多写少场景。底层同样基于AQS,引入**戳记Stamp**版本机制,区分锁状态,是JUC高性能锁代表。

5.5.1 三大锁模式(核心必考)

  1. 乐观读(Optimistic Reading):无锁、不加锁、无阻塞、无CAS,仅记录戳记,极致高性能,允许并发写。

  2. 悲观读(Read Lock):等效普通读写锁读锁,共享锁、阻塞写线程,线程安全。

  3. 写锁(Write Lock):独占锁,排他阻塞所有读写线程。

5.5.2 核心特性

  1. 解决写锁饥饿:打破读锁大量积压卡死写锁问题,读写排队公平性优于ReentrantReadWriteLock。

  2. 乐观读无开销:不加锁、不阻塞,适合超高并发只读场景。

  3. 不可重入:致命特点,StampedLock不支持可重入,重复加锁直接死锁。

  4. 不支持条件队列Condition:无法精准唤醒,没有await/signal机制。

  5. 戳记Stamp校验:每次加锁返回long类型戳记,校验戳记判断数据是否被修改。

5.5.3 锁状态与戳记规则

StampedLock 通过long类型stamp戳记标记锁状态,底层二进制划分标识:

  1. 乐观读:戳记为偶数,无锁标记;

  2. 悲观读:戳记高位标识读锁占用;

  3. 写锁:戳记高位标识写锁占用;

  4. 每次解锁、加锁都会刷新戳记,用于校验数据是否被篡改。

5.5.4 完整实战代码(乐观读标准模板|生产唯一写法)

import java.util.concurrent.locks.StampedLock;

/**
 * StampedLock 标准生产模板
 * 乐观读 + 悲观读 + 写锁 完整演示
 * 专门解决读多写少、超高并发、写锁饥饿问题
 */
public class StampedLockDemo {
    // 创建戳记锁
    private static final StampedLock STAMPED_LOCK = new StampedLock();
    // 共享变量
    private static int data = 0;

    // 1、乐观读(核心重点)
    public static void optimisticRead() {
        // 获取乐观戳记(偶数、无锁)
        long stamp = STAMPED_LOCK.tryOptimisticRead();
        // 读取共享数据
        int temp = data;
        // 校验戳记:判断读取期间是否被写线程修改
        if (!STAMPED_LOCK.validate(stamp)) {
            // 戳记失效,数据被篡改,升级为悲观读
            stamp = STAMPED_LOCK.readLock();
            try {
                temp = data;
            } finally {
                // 释放悲观读锁
                STAMPED_LOCK.unlockRead(stamp);
            }
        }
        System.out.println(Thread.currentThread().getName() + " 读取数据:" + temp);
    }

    // 2、悲观读锁
    public static void pessimisticRead() {
        long stamp = STAMPED_LOCK.readLock();
        try {
            System.out.println(Thread.currentThread().getName() + " 悲观读数据:" + data);
        } finally {
            STAMPED_LOCK.unlockRead(stamp);
        }
    }

    // 3、写锁
    public static void writeData() {
        long stamp = STAMPED_LOCK.writeLock();
        try {
            data++;
            System.out.println(Thread.currentThread().getName() + " 修改数据,当前值:" + data);
        } finally {
            STAMPED_LOCK.unlockWrite(stamp);
        }
    }

    public static void main(String[] args) {
        // 大量读线程
        for (int i = 0; i < 8; i++) {
            new Thread(StampedLockDemo::optimisticRead, "读线程"+i).start();
        }
        // 少量写线程
        new Thread(StampedLockDemo::writeData, "写线程").start();
    }
}

5.5.5 乐观读执行流程(面试必背)

  1. 调用 tryOptimisticRead() 获取偶数戳记,不上锁;

  2. 无阻塞直接读取共享数据;

  3. 调用 validate(stamp) 校验戳记是否变更;

  4. 戳记未变:数据安全,读取完成;

  5. 戳记改变:读取期间被写线程修改,升级悲观读,重新读取。

5.5.6 StampedLock VS ReentrantReadWriteLock

对比项

ReentrantReadWriteLock

StampedLock

可重入

✅ 支持

❌ 不支持

乐观读

❌ 无

✅ 支持、无锁开销

写锁饥饿

容易饥饿

解决饥饿问题

Condition条件队列

✅ 支持

❌ 不支持

适用场景

普通读多写少

超高并发读多写少

5.5.7 高频易错坑点(生产严禁踩坑)

  1. 不可重入(最大坑):同一线程重复加锁直接死锁,绝对不能嵌套加锁。

  2. 乐观读必须校验戳记:不写validate校验,数据脏读、线程不安全。

  3. 不支持中断锁:无法响应中断,阻塞线程不能被interrupt终止。

  4. 禁止混用解锁:读锁戳记不能用写锁解锁,直接抛异常。

  5. 不支持Condition:不能做精准线程唤醒,不适合生产者消费者场景。

  6. 乐观读不能加耗时操作:读取逻辑必须简短,防止长时间持有数据快照。

5.5.8 面试满分总结(背诵6句)

  1. StampedLock是JDK8高性能读写锁,新增乐观读模式;

  2. 依靠long戳记标识锁状态,偶数乐观、奇数悲观;

  3. 三种模式:乐观读、悲观读、写锁;

  4. 解决ReentrantReadWriteLock写锁饥饿问题;

  5. 致命缺点:不可重入、不支持中断、无Condition;

  6. 超高并发读多写少优先使用,普通业务用普通读写锁。

5.6 两大锁核心对比

特性

synchronized

ReentrantLock

锁类型

隐式

显式

可中断

不支持

支持

公平锁

仅非公平

可选

超时获取

支持

条件唤醒

单一

多 Condition 精准唤醒


第六部分 原子类全家桶(无锁并发)

6.1 基础原子类型(AtomicInteger/AtomicLong/AtomicBoolean)

基础原子类是JDK1.5引入的无锁并发工具,底层基于CAS自旋机制,不依赖操作系统互斥锁,在并发场景下保证基础数据类型操作原子性。性能远超synchronized、ReentrantLock,适合简单数值计数、状态标记场景。核心包含三大类:AtomicInteger、AtomicLong、AtomicBoolean

  1. AtomicReference

  2. AtomicStampedReference:解决 ABA 问题(版本号)

  3. AtomicMarkableReference:标记位解决 ABA

6.1.1 核心底层原理(CAS必考)

  1. CAS机制:Compare And Swap,比较并交换,无锁乐观锁思想;包含三个参数:内存值V、旧预期值E、新修改值N。

  2. 执行逻辑:判断内存原值V是否等于预期值E,相等则无锁修改为新值N;不相等则本次修改失败,重新获取原值,循环自旋重试。

  3. Unsafe类:底层依靠sun.misc.Unsafe直接操作内存偏移地址,实现原生CAS指令,规避Java语法层限制。

  4. volatile修饰:内部共享变量被volatile修饰,保证多线程数据可见性,配合CAS实现无锁原子操作。

6.1.2 通用核心API(三类原子类通用)

常用API

功能说明

get()

获取当前最新值

set()

强制修改为指定值,非原子修改

getAndIncrement()

先获取值,再自增(i++)

incrementAndGet()

先自增,再获取值(++i)

compareAndSet()

CAS核心方法,预期值匹配则修改,返回布尔结果

lazySet()

延迟刷新内存,弱化可见性,性能更高,特殊场景使用

6.1.3 完整实战代码(AtomicInteger标准模板)

import java.util.concurrent.atomic.AtomicInteger;

/**
 * 基础原子类:AtomicInteger 并发计数演示
 * 对比普通变量、原子变量线程安全差异
 */
public class AtomicIntegerDemo {
    // 普通变量:多线程计数不安全
    private static int commonNum = 0;
    // 原子整型:默认初始值0,线程安全
    private static final AtomicInteger ATOMIC_NUM = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        // 开启10个线程,每个线程累加1000次
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    commonNum++;
                    ATOMIC_NUM.getAndIncrement();
                }
            }).start();
        }
        // 休眠等待所有线程执行完毕
        Thread.sleep(2000);
        // 普通变量:结果永远小于10000,存在线程安全问题
        System.out.println("普通变量计数:" + commonNum);
        // 原子变量:结果严格等于10000,原子性保证
        System.out.println("原子变量计数:" + ATOMIC_NUM.get());
    }
}

6.1.4 三类基础原子类细分使用场景

  1. AtomicInteger(整型原子类)适用场景:接口请求计数、自增ID、简单整型统计、并发标记;

  2. 底层存储:int基础数据类型,占用内存小,性能最优。

  3. AtomicLong(长整型原子类)适用场景:大数据量统计、流水号、日志ID、海量并发计数;

  4. 底层存储:long类型,支持超大数值,适配高量级统计。

  5. AtomicBoolean(布尔原子类)适用场景:线程启停标记、开关状态、一次性初始化判断;

  6. 核心优势:无锁修改状态,常用于控制服务启停、防止重复初始化。

6.1.5 CAS优缺点(面试必背)

优点

  1. 无锁开销:全程用户态执行,不切换内核态,无线程阻塞、无上下文切换;

  2. 并发性能高:低竞争场景下,性能碾压synchronized重量级锁;

  3. 使用简单:API简洁,无需手动加锁解锁,代码简洁不易出错。

缺点

  1. 自旋空转消耗CPU:高并发竞争激烈时,CAS持续重试,CPU占用率飙升;

  2. 只能保证单个变量原子性:无法实现多变量复合原子操作;

  3. 存在ABA问题:数值先改后改还原,CAS无法识别中间修改过程。

6.1.6 高频易错坑点(生产避坑)

  1. 自增运算符不具备原子性:普通i++是读取、修改、写入三步操作,多线程必然错乱,必须使用原子类;

  2. set()非原子一致性修改:set直接覆盖值,不做CAS校验,并发场景慎用;

  3. 高并发优先使用LongAdder:海量并发计数场景,AtomicLong自旋严重,性能不如分段累加LongAdder;

  4. 不能解决复合操作:例如判断+赋值组合操作,单纯原子类无法保证原子性,需加锁;

  5. AtomicBoolean禁止频繁切换:高频状态切换会导致CAS重试,浪费CPU资源。

6.1.7 面试满分总结(背诵6句)

  1. 基础原子类包含AtomicInteger、AtomicLong、AtomicBoolean;

  2. 底层基于CAS+Unsafe+volatile实现,无锁保证原子性;

  3. 原理为比较并交换,原值匹配则修改,失败自旋重试;

  4. 低竞争性能极强,高竞争CPU空转严重;

  5. 存在ABA漏洞,仅适用于单一变量简单计数;

  6. 生产简单统计用原子类,海量计数优先LongAdder。

6.2 引用原子类(解决对象引用CAS、ABA问题)

引用原子类专门针对引用类型对象实现无锁CAS操作,基础原子类只能操作基本数据类型,而引用原子类可以自定义实体对象、修改对象引用、保证对象替换的原子性。核心包含三大类:AtomicReference、AtomicStampedReference、AtomicMarkableReference,其中后两类专门根治CAS经典ABA问题。

6.2.1 三类引用原子类总览

引用原子类

核心特性

适用场景

AtomicReference

普通引用CAS,无版本标记,存在ABA问题

简单对象替换、无中间篡改场景

AtomicStampedReference

对象+数字版本号,精准判断ABA

严格防篡改、需要记录修改次数

AtomicMarkableReference

对象+布尔标记,仅判断是否被修改过

只需判断篡改状态、无需记录次数

6.2.2 AtomicReference 普通引用原子类

① 核心介绍

AtomicReference 是最基础的引用原子类,底层封装任意引用类型,支持对象引用的CAS无锁替换,原理和AtomicInteger一致,仅修改数据类型为通用泛型。致命缺陷:存在ABA问题,无法识别对象中间修改行为。

② 常用核心API

  1. get():获取当前引用对象

  2. set(V newValue):直接覆盖修改引用

  3. compareAndSet(V expect, V update):CAS比对引用,相等则替换

  4. getAndSet(V newValue):先获取旧引用,再设置新引用

③ 实战代码(自定义实体CAS替换)

import java.util.concurrent.atomic.AtomicReference;

/**
 * AtomicReference 引用原子类演示
 * 实现自定义实体类无锁CAS替换
 */
public class AtomicReferenceDemo {
    // 自定义用户实体
    static class User {
        private String name;
        private Integer age;
        // 构造、get/set省略
        public User(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
        @Override
        public String toString() {
            return "User{name='" + name + "', age=" + age + "}";
        }
    }

    public static void main(String[] args) {
        // 初始化原子引用,绑定初始用户对象
        AtomicReference<User> atomicUser = new AtomicReference<>(new User("张三", 18));
        // CAS替换:预期是张三,替换为李四
        boolean isSuccess = atomicUser.compareAndSet(new User("张三", 18), new User("李四", 20));
        // 注意:new对象地址不同,此处替换失败
        System.out.println("CAS是否替换成功:" + isSuccess);
        System.out.println("当前对象:" + atomicUser.get());
    }
}

④ 易错坑点

  1. CAS比对的是内存地址:不是属性值,两个属性相同的new对象地址不同,替换失败;

  2. 无版本标记,线程来回修改引用,无法识别ABA篡改;

  3. 适合静态唯一引用对象,不适合频繁修改的业务对象。

6.2.3 AtomicStampedReference(版本号防ABA|重点必考)

① ABA问题完整复盘

ABA问题:线程A读取内存值为A,线程B先将A修改为B、再改回A;线程A判定原值未变,执行CAS修改,无法识别中间篡改过程,造成业务逻辑漏洞。基础CAS、AtomicReference均存在该问题。

② 底层防篡改原理

内部封装二元组:引用对象 + int版本号,每次修改引用,版本号自增+1;CAS比对时,同时校验引用地址+版本号,哪怕对象还原、版本号不同,直接判定篡改,拒绝修改。

③ 核心独有API

  1. compareAndSet(V expectRef, V newRef, int expectStamp, int newStamp):双重校验(引用+版本)

  2. getStamp():获取当前版本号

  3. get(V[] pair):一次性获取引用和版本号

④ 代码实战(彻底解决ABA问题)

import java.util.concurrent.atomic.AtomicStampedReference;

/**
 * AtomicStampedReference 解决ABA问题演示
 * 核心:对象引用 + 数字版本号双重校验
 */
public class AtomicStampedDemo {
    public static void main(String[] args) throws InterruptedException {
        // 初始值A,初始版本号1
        AtomicStampedReference<Integer> stampedRef = new AtomicStampedReference<>(100, 1);

        // 线程1:模拟业务修改
        new Thread(() -> {
            // 第一次修改:100 -> 200,版本+1
            stampedRef.compareAndSet(100, 200, stampedRef.getStamp(), stampedRef.getStamp() + 1);
            // 第二次修改:200 -> 100,版本再+1
            stampedRef.compareAndSet(200, 100, stampedRef.getStamp(), stampedRef.getStamp() + 1);
            System.out.println("线程1篡改完成,当前值:" + stampedRef.getReference() + ",当前版本:" + stampedRef.getStamp());
        }).start();

        // 主线程休眠,等待线程1完成ABA篡改
        Thread.sleep(1000);

        // 主线程尝试CAS修改:预期值100、预期版本1(初始版本)
        boolean result = stampedRef.compareAndSet(100, 999, 1, 2);
        System.out.println("主线程CAS修改结果:" + result);
        System.out.println("最终数值:" + stampedRef.getReference());
    }
}

⑤ 执行结果说明

主线程修改结果为false,修改失败;虽然数值还原为100,但是版本号已经变为3,和预期版本1不匹配,精准拦截ABA篡改,彻底解决漏洞。

6.2.4 AtomicMarkableReference(标记位防ABA)

① 核心定位

属于轻量化防ABA引用原子类,底层二元组:引用对象 + boolean标记位;无需记录修改次数,只判定对象是否被修改过,节省内存、性能更高。

② 适用场景

只需判断数据是否发生篡改、不需要统计修改次数;例如:资源是否被占用、链路是否被篡改、状态是否变更。

③ 简易代码示例

import java.util.concurrent.atomic.AtomicMarkableReference;

/**
 * AtomicMarkableReference 布尔标记防篡改
 */
public class AtomicMarkableDemo {
    public static void main(String[] args) {
        // 初始对象、初始标记false(未修改)
        AtomicMarkableReference<String> markRef = new AtomicMarkableReference<>("原始数据", false);
        // 修改数据,同时修改标记为true
        markRef.compareAndSet("原始数据", "修改后数据", false, true);
        // 获取当前标记
        System.out.println("是否被修改过:" + markRef.isMarked());
    }
}

6.2.5 三者面试硬核区别(必背)

  1. AtomicReference:纯引用CAS,无标记,存在ABA,适合静态不变对象;

  2. AtomicStampedReference:引用+int版本号,记录修改次数,精准防ABA,生产最常用;

  3. AtomicMarkableReference:引用+boolean标记,仅判断是否修改,轻量化、省内存。

6.2.6 高频易错坑点(生产避坑)

  1. 版本号必须手动自增:AtomicStampedReference不会自动叠加版本,需要手动+1,新手极易写错;

  2. 标记位不可重复使用:AtomicMarkable只有true/false,反复篡改无法识别次数;

  3. 引用比对依赖地址:所有引用原子类CAS比对均为内存地址,不是属性值;

  4. 禁止空引用CAS:null引用进行比对,直接抛出空指针异常。

6.2.7 面试满分总结(背诵5句)

  1. 引用原子类包含三种:普通引用、版本引用、标记引用;

  2. AtomicReference无标记,原生存在ABA问题;

  3. AtomicStampedReference依靠版本号,精准拦截ABA篡改;

  4. AtomicMarkableReference用布尔标记,轻量化判定修改;

  5. 业务防篡改优先Stamped,简单状态判定优先Markable。

6.3 字段更新原子类

字段更新原子类包含AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,属于轻量化原子工具类。无需把整个类封装为原子对象,仅对普通类中的**单个成员字段**实现无锁CAS原子修改,节省内存、无需创建额外原子对象,适合大量实体类字段并发更新场景,是生产中优化内存开销的关键工具。

6.3.1 三大字段原子类分类

  1. AtomicIntegerFieldUpdater:专门修改实体类int类型成员字段

  2. AtomicLongFieldUpdater:专门修改实体类long类型成员字段

  3. AtomicReferenceFieldUpdater:专门修改实体类任意引用类型成员字段

6.3.2 核心使用硬性约束(必考易错)

字段更新原子类对被修改字段有严格语法要求,不满足直接报错:

  1. 修饰符必须为volatile:保证字段可见性,底层CAS依赖volatile禁止指令重排、保证内存可见

  2. 不能是private私有修饰:必须为public/protected/包访问权限,底层反射获取字段

  3. 不能是static静态字段:仅支持实例对象成员字段,不支持静态属性

  4. 不能是final修饰:final字段不可修改,无法进行CAS赋值

6.3.3 底层实现原理

  1. 底层依旧基于Unsafe类,通过反射获取字段内存偏移地址;

  2. 采用CAS无锁机制,直接修改堆内存中实体对象的字段值;

  3. 不创建额外包装对象,直接操作原实体类,内存开销极低;

  4. 仅能修改单个字段,不支持多字段复合原子操作。

6.3.4 实战代码模板(AtomicIntegerFieldUpdater)

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;

/**
 * 字段更新原子类演示
 * 无需封装原子对象,直接修改普通实体类volatile字段
 */
public class IntegerFieldUpdaterDemo {
    // 自定义普通实体类
    static class User {
        // 硬性要求:必须volatile、非private、非static、非final
        public volatile int age;
        public String name;

        public User(String name, int age) {
            this.name = name;
            this.age = age;
        }
    }

    // 创建字段更新器:绑定User类、age字段
    private static final AtomicIntegerFieldUpdater<User> AGE_UPDATER =
            AtomicIntegerFieldUpdater.newUpdater(User.class, "age");

    public static void main(String[] args) {
        User user = new User("张三", 18);
        // CAS修改字段:预期值18,修改为20
        boolean isSuccess = AGE_UPDATER.compareAndSet(user, 18, 20);
        System.out.println("字段修改是否成功:" + isSuccess);
        System.out.println("修改后年龄:" + user.age);

        // 自增修改
        AGE_UPDATER.getAndIncrement(user);
        System.out.println("自增后年龄:" + user.age);
    }
}

6.3.5 引用字段更新示例(AtomicReferenceFieldUpdater)

import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;

/**
 * 引用类型字段更新原子类
 * 修改实体类自定义引用对象字段
 */
public class ReferenceFieldUpdaterDemo {
    static class Student {
        // 引用类型字段,必须volatile
        public volatile String status;
    }

    // 绑定Student类、status引用字段
    private static final AtomicReferenceFieldUpdater<Student, String> STATUS_UPDATER =
            AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "status");

    public static void main(String[] args) {
        Student student = new Student();
        student.status = "未入学";
        // CAS修改引用状态
        STATUS_UPDATER.compareAndSet(student, "未入学", "已入学");
        System.out.println("学生状态:" + student.status);
    }
}

6.3.6 生产适用场景

  1. 海量实体对象并发更新:批量对象单个字段修改,节省原子包装对象内存;

  2. 状态标记字段更新:实体类启停、状态流转、开关标记修改;

  3. 数据库乐观锁版本号:实体version版本号无锁自增;

  4. 自定义并发工具:手写简易自旋锁、计数器工具类。

6.3.7 高频易错坑点

  1. 字段修饰符违规:缺少volatile、private修饰直接初始化失败;

  2. 字段名写错:字符串传参字段名错误,运行期反射报错;

  3. 不支持复合操作:仅能单字段CAS修改,判断+赋值组合无法保证原子性;

  4. 不可修改数组字段:仅支持基础类型、普通引用类型,不支持数组;

  5. 反射存在性能损耗:初始化更新器依赖反射,建议全局常量定义更新器,不要重复创建。

6.3.8 面试满分总结(背诵5句)

  1. 字段更新原子类包含整型、长整型、引用型三类;

  2. 专门修改普通实体类字段,无需创建原子包装对象;

  3. 字段必须满足volatile、非私有、非静态、非final;

  4. 底层Unsafe+CAS实现,内存开销极低、适合海量对象;

  5. 多用于实体状态更新、乐观锁版本号自增场景。

6.4 数组原子类

AtomicIntegerArray

6.5 高并发分段累加(LongAdder/DoubleAdder)

AtomicLong 在超高并发下大量线程 CAS 自旋空转,CPU 占用飙升、性能严重卡顿。JDK1.8 推出 LongAdder、DoubleAdder 分段累加工具类,采用分散热点思想,将单个竞争变量拆分多段分散竞争,是海量并发计数生产首选,性能碾压 AtomicLong。

简洁:LongAdder、DoubleAdder,分散热点,超高并发优于 AtomicLong

6.5.1 核心核心架构

  1. 基础变量:base 基础数值(低并发直接累加);

  2. 分段数组:Cell[] 哈希分段数组,每个Cell独立计数;

  3. 累加规则:低并发修改base,高并发线程哈希映射到不同Cell,分散竞争;

  4. 最终总值:总值 = base + 所有Cell数组元素累加和。

6.5.2 为什么性能远超AtomicLong?(面试必考)

  1. AtomicLong:仅有一个value变量,所有线程争抢同一内存地址,高并发CAS失败大量自旋,CPU空转严重;

  2. LongAdder:采用分段锁思想,将竞争压力分散到多个Cell单元格,不同线程修改不同Cell,极少发生竞争,无需频繁自旋;

  3. 线程通过哈希算法绑定Cell,冲突时扩容数组,进一步降低竞争概率。

6.5.3 完整实战代码(对比AtomicLong)

import java.util.concurrent.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

/**
 * 高并发计数对比:AtomicLong VS LongAdder
 * 模拟海量线程累加,直观性能差距
 */
public class LongAdderDemo {
    // 普通原子长整型
    private static final AtomicLong ATOMIC_LONG = new AtomicLong(0);
    // 分段累加计数器
    private static final LongAdder LONG_ADDER = new LongAdder();

    public static void main(String[] args) throws InterruptedException {
        // 线程数
        int threadNum = 50;
        // 单线程累加次数
        int loopNum = 100000;

        // 测试AtomicLong耗时
        long start1 = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopNum; j++) {
                    ATOMIC_LONG.incrementAndGet();
                }
            }).start();
        }

        // 测试LongAdder耗时
        long start2 = System.currentTimeMillis();
        for (int i = 0; i < threadNum; i++) {
            new Thread(() -> {
                for (int j = 0; j < loopNum; j++) {
                    LONG_ADDER.increment();
                }
            }).start();
        }

        // 等待线程执行完毕
        Thread.sleep(3000);
        System.out.println("AtomicLong最终值:" + ATOMIC_LONG.get() + ",耗时:" + (System.currentTimeMillis() - start1) + "ms");
        System.out.println("LongAdder最终值:" + LONG_ADDER.sum() + ",耗时:" + (System.currentTimeMillis() - start2) + "ms");
    }
}

6.5.4 LongAdder 优缺点分析

✅ 优点

  1. 超高并发性能极强:分散竞争,规避大量CAS自旋,CPU占用低;

  2. 自适应扩容:Cell数组冲突自动扩容,进一步降低竞争概率;

  3. 使用简单:API简洁,无需手动加锁,天然线程安全。

❌ 缺点

  1. 无实时一致性:sum()汇总数值存在延迟,非强一致性;

  2. 内存占用更高:维护Cell数组,相比AtomicLong占用额外内存;

  3. 不支持复杂CAS逻辑:仅适合单纯累加、累减,无自定义CAS修改。

6.5.5 DoubleAdder 补充说明

  1. 专门针对double浮点类型分段累加,底层原理、架构、使用方式和LongAdder完全一致;

  2. 解决AtomicDouble高并发自旋卡顿问题,适用于浮点型海量统计;

  3. 浮点运算存在精度丢失,不适合金融高精度计算场景。

6.5.6 生产严格使用规范(避坑)

  1. 低并发简单计数:优先使用 AtomicLong,内存占用更低;

  2. 高并发海量计数:接口请求统计、日志计数、流量监控,强制使用 LongAdder;

  3. 需要实时精准取值:禁止使用LongAdder,汇总存在数据延迟;

  4. 需要自定义CAS修改:禁止使用分段累加类,改用AtomicLong;

  5. sum()方法频繁调用会合并Cell数组,消耗性能,尽量减少汇总次数。

6.5.7 面试满分总结(背诵6句)

  1. LongAdder/DoubleAdder是JDK1.8高性能分段累加计数器;

  2. 底层采用base+Cell分段数组,分散线程竞争压力;

  3. 低并发修改base,高并发映射Cell,冲突自动扩容;

  4. 相比AtomicLong,超高并发规避CAS空转,性能大幅提升;

  5. 缺点是最终求和非实时、弱一致性,内存占用更高;

  6. 生产海量流量统计优先LongAdder,低并发简单计数用AtomicLong。

6.6 ABA 问题

ABA问题是CAS无锁并发经典漏洞,指线程读取共享数据为A,其他线程先将A修改为B、再修改回A;原线程判定数据未发生变更,正常执行CAS修改,无法识别中间篡改流程,导致业务逻辑隐藏漏洞。CAS仅比对内存值,不识别修改轨迹,这是ABA问题产生的核心根源。

6.6.1 ABA问题完整产生流程

  1. 线程T1:从内存读取数据A,准备执行CAS修改;

  2. 线程T2:抢占CPU,将数据A修改为B,随后再次改回A;

  3. 线程T1:再次读取内存值仍为A,判定数据无修改,执行CAS赋值;

  4. 结果:T1无法感知中间A→B→A的篡改过程,引发业务逻辑异常。

6.6.2 原生ABA问题代码复现

import java.util.concurrent.atomic.AtomicInteger;

/**
 * ABA问题原生复现演示
 * 直观查看CAS无法识别中间篡改的漏洞
 */
public class ABAOriginalDemo {
    // 原子整型,初始值100
    private static final AtomicInteger ATOMIC_NUM = new AtomicInteger(100);

    public static void main(String[] args) throws InterruptedException {
        // 线程1:模拟业务线程,读取值后阻塞
        new Thread(() -> {
            int expect = ATOMIC_NUM.get();
            System.out.println("线程1读取初始值:" + expect);
            // 休眠2秒,让线程2完成ABA篡改
            try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}
            // CAS修改:预期100,修改为200
            boolean result = ATOMIC_NUM.compareAndSet(expect, 200);
            System.out.println("线程1 CAS修改结果:" + result + ",最终值:" + ATOMIC_NUM.get());
        }, "线程1").start();

        // 线程2:模拟恶意篡改线程,完成ABA流程
        new Thread(() -> {
            // A -> B -> A
            ATOMIC_NUM.compareAndSet(100, 150);
            ATOMIC_NUM.compareAndSet(150, 100);
            System.out.println("线程2完成ABA篡改,最终还原值:" + ATOMIC_NUM.get());
        }, "线程2").start();
    }
}

6.6.3 ABA问题实际危害(生产痛点)

  1. 资金交易漏洞:转账金额来回篡改,CAS判定无异常,造成资金扣减错乱;

  2. 链表节点丢失:并发修改链表,节点被删除又复原,导致链表断链、数据丢失;

  3. 状态判定失效:设备状态、订单状态反复切换,程序误判状态未变更;

  4. 数据统计失真:计数变量来回修改,统计结果掩盖真实篡改记录。

6.6.4 四大解决方案(面试必背)

  1. 版本号机制(主流):使用AtomicStampedReference,每次修改自增版本号,同时校验数据+版本号;

  2. 布尔标记机制:使用AtomicMarkableReference,仅标记数据是否被修改,轻量化防篡改;

  3. 时间戳替代版本号:自定义时间戳字段,每次修改记录时间,比对时间戳判定篡改;

  4. 加锁兜底:高敏感业务放弃CAS,使用synchronized/Lock排他锁,杜绝并发篡改。

6.6.5 优劣方案对比

解决方案

优点

缺点

AtomicStampedReference

记录修改次数,精准防ABA

需手动维护版本号,代码繁琐

AtomicMarkableReference

轻量化、内存占用低

无法记录修改次数,仅判断是否修改

自定义时间戳

灵活适配业务,通用性强

需手动编码,开发成本高

排他锁

彻底杜绝并发篡改,安全性最高

加锁开销大,并发性能下降

6.6.6 生产使用规范(避坑)

  1. 简单计数、无业务溯源:普通AtomicInteger即可,无需防ABA;

  2. 资金、订单、交易敏感数据:强制使用带版本号AtomicStampedReference;

  3. 仅需判定是否修改、无需次数:优先AtomicMarkableReference节省内存;

  4. 超高并发敏感业务:放弃无锁CAS,使用显式锁保证绝对安全。

6.6.7 面试满分背诵总结(6句)

  1. ABA是CAS无锁并发特有漏洞,数据还原但修改轨迹被忽略;

  2. 成因是CAS仅比对内存值,不记录修改过程;

  3. 危害隐藏性高,易造成资金、链表、业务状态异常;

  4. 核心解决方案:版本号、布尔标记、时间戳、排他锁;

  5. Stamped精准计数防篡改,Markable轻量化判定修改;

  6. 普通计数无需处理ABA,敏感业务必须加版本管控。


第七部分 AQS 抽象队列同步器(JUC 底层核心)

7.1 核心架构(完整版|面试必背)

AQS全称AbstractQueuedSynchronizer抽象队列同步器,是JUC并发包底层基石,所有锁、并发工具类底层均依赖AQS实现。核心架构由三大核心属性、双向CLH同步队列、内部节点、模板方法构成,内置双向 FIFO 同步队列,通过 state 状态值控制资源抢占,采用模板方法模式,子类只需重写少量方法即可实现自定义锁。

简洁:内置双向 FIFO 同步队列,通过 state 状态值控制资源抢占。

7.1.1 三大核心全局属性

  1. state 同步状态变量(核心):int类型,volatile修饰,保障可见性;不同锁含义不同,ReentrantLock代表加锁次数、Semaphore代表剩余许可数、CountDownLatch代表剩余倒计时数;通过CAS无锁修改,保证并发安全。

  2. head 头节点:双向同步队列头部节点,不存储业务线程,作为虚拟哨兵节点,专门用于唤醒下一个阻塞节点,减少并发竞争开销。

  3. tail 尾节点:双向同步队列尾部节点,新竞争失败的线程节点,通过CAS自旋挂载到队列尾部,排队等待获取锁。

7.1.2 CLH双向同步队列

  1. 结构:双向链表结构,每个节点保存前驱、后继引用,支持从尾部入队、头部出队,保证FIFO先进先出排队规则。

  2. 作用:存储抢占资源失败的线程,线程挂起阻塞,避免空转消耗CPU;资源释放后由头节点唤醒后继节点。

  3. 特性:非公平锁允许新线程插队,公平锁严格按照队列顺序执行,无插队行为。

7.1.3 内部Node节点组成

每一个阻塞线程都会被封装为Node节点,存入同步队列,核心字段:

  1. thread:绑定当前阻塞线程;

  2. prev:前驱节点引用;

  3. next:后继节点引用;

  4. waitStatus:节点等待状态(对应五大节点状态);

  5. nextWaiter:条件队列后继节点,用于Condition精准唤醒。

7.1.4 整体执行架构流程

  1. 资源抢占:线程调用acquire()尝试CAS修改state抢占资源;修改成功直接执行业务逻辑。

  2. 失败入队:抢占失败,封装为Node节点,自旋CAS挂载到队列尾部。

  3. 线程阻塞:节点挂载完成,校验前驱节点状态,安全判断后阻塞挂起,释放CPU。

  4. 资源释放:持有锁线程执行完毕,调用release()修改state释放资源。

  5. 后继唤醒:头节点唤醒下一个有效阻塞节点,竞争资源继续执行。

7.1.5 AQS架构设计亮点(面试加分)

  1. 模板方法模式:封装通用排队、阻塞、唤醒逻辑,子类只需实现tryAcquire/tryRelease简单方法;

  2. 双队列设计:同步队列+条件队列,兼顾普通排队、精准唤醒场景;

  3. 无锁入队:通过CAS实现节点入队,不依赖额外锁,性能高效;

  4. 自适应唤醒:规避无效唤醒,减少上下文切换开销。

7.2 两大独占共享模式(底层源码+实战代码补全)

AQS 将同步资源抢占分为独占模式、共享模式,两种模式队列唤醒、资源释放逻辑完全不同,是JUC锁工具底层核心区分点,面试高频考点。

简洁:

  1. 独占模式:ReentrantLock

  2. 共享模式:CountDownLatch、Semaphore

7.2.1 独占模式(Exclusive)

① 核心特性

  1. 同一时刻仅允许一个线程持有资源,排他性抢占;

  2. 资源释放仅唤醒一个后继阻塞线程;

  3. 不可共享、无传播唤醒机制;

  4. 典型实现类:ReentrantLock、ReentrantReadWriteLock写锁

② AQS核心重写方法

  1. tryAcquire():尝试独占获取资源

  2. tryRelease():独占模式释放资源

  3. isHeldExclusively():判断当前线程是否持有独占锁

③ 独占模式简易自定义锁(手写AQS)

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * 基于AQS手写独占不可重入锁
 * 模拟ReentrantLock底层独占实现原理
 */
public class ExclusiveAqsLock {
    // 自定义同步器:继承AQS
    private static class Sync extends AbstractQueuedSynchronizer {
        // 加锁:state=0无锁,CAS修改为1加锁成功
        @Override
        protected boolean tryAcquire(int arg) {
            // CAS无锁修改state
            if (compareAndSetState(0, 1)) {
                // 标记当前持有锁的线程
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        // 释放锁:state重置为0
        @Override
        protected boolean tryRelease(int arg) {
            // 没有加锁直接报错
            if (getState() == 0) {
                throw new IllegalMonitorStateException();
            }
            // 清空持有锁线程
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        // 判断是否持有独占锁
        @Override
        protected boolean isHeldExclusively() {
            return getState() == 1;
        }
    }

    // 初始化同步器
    private final Sync sync = new Sync();

    // 对外加锁方法
    public void lock() {
        sync.acquire(1);
    }

    // 对外解锁方法
    public void unlock() {
        sync.release(1);
    }

    // 测试独占锁:同一时刻只有一个线程执行
    public static void main(String[] args) {
        ExclusiveAqsLock lock = new ExclusiveAqsLock();
        // 开启3条线程竞争独占锁
        for (int i = 1; i <= 3; i++) {
            new Thread(() -> {
                lock.lock();
                try {
                    System.out.println(Thread.currentThread().getName() + " 获取独占锁,执行业务");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                    System.out.println(Thread.currentThread().getName() + " 释放独占锁");
                }
            }, "线程" + i).start();
        }
    }
}

7.2.2 共享模式(Shared)

① 核心特性

  1. 同一时刻允许多个线程同时持有资源,共享抢占;

  2. 资源释放后存在传播唤醒机制,连续唤醒后继共享线程;

  3. 支持限流、计数器、栅栏等并发场景;

  4. 典型实现类:Semaphore、CountDownLatch、读写锁读锁

② AQS核心重写方法

  1. tryAcquireShared():尝试共享获取资源,返回int值(负数失败、正数成功、无剩余资源)

  2. tryReleaseShared():共享模式释放资源,支持唤醒传播

③ 共享模式简易限流工具(手写AQS信号量)

import java.util.concurrent.locks.AbstractQueuedSynchronizer;

/**
 * 基于AQS手写共享模式限流信号量
 * 模拟Semaphore底层共享实现原理,限制最大并发线程数
 */
public class SharedAqsSemaphore {
    // 自定义共享同步器
    private static class Sync extends AbstractQueuedSynchronizer {
        // 构造方法:初始化最大许可数(最大并发线程数)
        public Sync(int permits) {
            setState(permits);
        }

        // 共享获取资源:获取1个许可
        @Override
        protected int tryAcquireShared(int acquires) {
            for (;;) {
                int available = getState();
                int remain = available - acquires;
                // 许可不足直接返回负数,获取失败;CAS修改许可数
                if (remain < 0 || compareAndSetState(available, remain)) {
                    return remain;
                }
            }
        }

        // 共享释放资源:归还1个许可
        @Override
        protected boolean tryReleaseShared(int releases) {
            for (;;) {
                int current = getState();
                int next = current + releases;
                // CAS累加许可数
                if (compareAndSetState(current, next)) {
                    return true;
                }
            }
        }
    }

    private final Sync sync;

    // 初始化限流数量
    public SharedAqsSemaphore(int permits) {
        this.sync = new Sync(permits);
    }

    // 获取许可
    public void acquire() {
        sync.acquireShared(1);
    }

    // 释放许可
    public void release() {
        sync.releaseShared(1);
    }

    // 测试共享限流:最大允许2个线程同时执行
    public static void main(String[] args) {
        // 最大并发2
        SharedAqsSemaphore semaphore = new SharedAqsSemaphore(2);
        // 开启5条线程,限流执行
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                semaphore.acquire();
                try {
                    System.out.println(Thread.currentThread().getName() + " 获取许可,执行任务");
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    semaphore.release();
                    System.out.println(Thread.currentThread().getName() + " 释放许可");
                }
            }, "任务线程" + i).start();
        }
    }
}

7.2.3 独占 & 共享 核心区别(面试对比表)

对比维度

独占模式

共享模式

并发线程数

同一时刻仅1个线程

允许多个线程同时执行

资源唤醒

单次仅唤醒1个线程

支持传播唤醒、批量唤醒

核心方法

tryAcquire/tryRelease

tryAcquireShared/tryReleaseShared

典型场景

排他锁、互斥业务

限流、计数器、读共享

返回值

boolean:成功/失败

int:剩余资源标识

7.2.4 面试满分总结(背诵6句)

  1. AQS分为独占、共享两大模式,是JUC所有并发工具底层根基;

  2. 独占模式单线程占用资源,典型实现为ReentrantLock;

  3. 共享模式多线程并发占用,典型实现为Semaphore、CountDownLatch;

  4. 独占单次唤醒单线程,共享支持链式传播唤醒;

  5. 独占返回布尔结果,共享返回int判定剩余资源;

  6. 读写锁融合两种模式:写独占、读共享。

7.3 节点五大状态(waitStatus|面试必考)

AQS内部Node节点依靠waitStatus整型常量标记节点状态,共5种状态,状态之间可流转,是线程阻塞、唤醒、取消的核心标识,全部由负数标识(初始态除外),源码常量定义+详细解析如下:

状态名称

常量值

核心含义

触发场景与流转规则

初始态

0

默认空白状态

线程刚封装为Node节点、刚入队未阻塞;无任何特殊标记,是节点默认初始化状态。

SIGNAL 待唤醒

-1

后继节点等待唤醒

当前节点释放锁后,需要唤醒后继阻塞节点;队列中绝大多数阻塞节点都是该状态,是最常用状态。

CANCELLED 已取消

1

节点终止、不可恢复

线程超时、被中断、主动取消竞争;该节点永久失效,不会再次争抢锁,队列中会被自动清理,唯一正数状态。

CONDITION 条件等待

-2

条件队列阻塞

线程调用Condition.await(),节点存入条件等待队列;未被signal唤醒前,不会进入同步队列争抢锁。

PROPAGATE 传播共享

-3

共享锁传播唤醒

仅用于共享模式,资源释放后持续向后传播唤醒,保证多线程共享资源,适配Semaphore、CountDownLatch。

7.3.1 高频面试必背要点

  1. 正负规则:仅CANCELLED为正数(1),其余全部为负数或0,正数代表节点失效;

  2. SIGNAL优先级最高:同步队列阻塞节点统一标记为-1,是生产最常见状态;

  3. 状态不可逆:节点一旦变为CANCELLED取消状态,永远无法恢复,只能被清理;

  4. 队列隔离:CONDITION(-2)仅存在条件队列,不会进入同步队列;

  5. 专属场景:PROPAGATE(-3)专属共享锁,独占锁永远不会出现该状态。

7.4 核心方法(完整版补全|源码级解析)

AQS 所有方法分为对外暴露公共方法、子类重写模板方法、内部私有工具方法,以下整理面试必考、源码高频核心方法,全部附带作用解析、使用场景、执行逻辑,通俗易懂无晦涩冗余。

简洁:acquire () 获取资源、release () 释放资源。

7.4.1 对外公共核心方法(使用者调用)

  1. acquire(int arg):独占模式获取资源,不可中断;流程:尝试获取资源→失败入队→阻塞等待,ReentrantLock.lock()底层调用。

  2. acquireInterruptibly(int arg):独占可中断获取,阻塞过程可被interrupt()打断,抛出中断异常。

  3. tryAcquireNanos(int arg, long nanosTimeout):独占限时获取,超时自动放弃,防止永久阻塞死锁。

  4. release(int arg):独占模式释放资源,修改state状态,唤醒后继阻塞节点。

  5. acquireShared(int arg):共享模式获取资源,不可中断,适用于信号量、计数器。

  6. acquireSharedInterruptibly(int arg):共享模式可中断获取资源。

  7. releaseShared(int arg):共享模式释放资源,唤醒传播后继共享节点。

7.4.2 子类重写模板方法(自定义锁实现)

AQS默认抛出异常,子类根据模式选择性重写,遵循模板方法设计模式

  1. tryAcquire(int arg):独占尝试获取资源,返回boolean;成功true、失败false。

  2. tryRelease(int arg):独占尝试释放资源,返回boolean;释放成功true。

  3. isHeldExclusively():判断当前线程是否持有独占锁,多用于重入锁、锁状态判断。

  4. tryAcquireShared(int arg):共享尝试获取资源,返回int;负数失败、0成功无剩余、正数成功有剩余资源。

  5. tryReleaseShared(int arg):共享尝试释放资源,返回boolean;是否释放成功。

7.4.3 内部私有底层方法(AQS核心底层)

  1. addWaiter(Node mode):线程竞争失败,封装为Node节点,CAS自旋快速入队。

  2. enq(final Node node):初始化队列、节点自旋入队,保障多线程入队安全。

  3. acquireQueued(final Node node, int arg):队列内节点循环竞争锁,判断前驱节点状态,安全阻塞线程。

  4. shouldParkAfterFailedAcquire(Node pred, Node node):判断当前节点是否需要阻塞,清理队列中CANCELLED失效节点。

  5. parkAndCheckInterrupt():调用LockSupport.park()阻塞线程,唤醒后返回中断标记。

  6. unparkSuccessor(Node node):唤醒当前节点的后继有效阻塞节点,跳过失效取消节点。

  7. doReleaseShared():共享模式传播唤醒,持续向后唤醒共享节点,实现批量放行。

7.4.4 工具辅助方法(状态判断)

  1. getState():获取同步状态变量state值。

  2. setState(int newState):直接修改state值,无CAS,适用于线程安全场景。

  3. compareAndSetState(int expect, int update):CAS无锁修改state,底层Unsafe实现,保障并发安全。

  4. setExclusiveOwnerThread(Thread thread):设置独占锁持有线程。

7.4.5 面试满分必背总结(8句)

  1. AQS方法分为公共调用、子类重写、底层私有三类,职责划分清晰;

  2. 独占核心:acquire/release,共享核心:acquireShared/releaseShared;

  3. 自定义锁只需重写少量模板方法,通用排队逻辑AQS封装;

  4. addWaiter快速入队,enq处理队列为空初始化场景;

  5. shouldParkAfterFailedAcquire清洗失效节点,优化队列结构;

  6. parkAndCheckInterrupt完成线程阻塞,依赖LockSupport工具;

  7. 共享模式doReleaseShared实现传播唤醒,区别于独占单次唤醒;

  8. CAS修改state保障无锁并发,是AQS线程安全的底层基石。

7.5 核心原理

  1. 失败线程进入队列排队

  2. 头节点唤醒后继节点

  3. 共享锁唤醒传播机制

  4. CLH 队列排队机制

7.6 依赖 AQS 实现类

AQS是JUC并发核心基石,JUC包下绝大多数锁、并发工具类底层均依托AQS实现,分为独占锁实现类、共享锁实现类、混合锁实现类、其他衍生工具类,下面分类详解,标注AQS抢占模式、底层原理、面试核心考点:

7.6.1 独占模式实现类(排他加锁、单线程占用)

  1. ReentrantLock:可重入独占锁,基于AQS实现公平/非公平锁,依靠state记录重入次数,生产最常用显式锁;

  2. ReentrantReadWriteLock.WriteLock:读写锁中的写锁,排他独占模式,写操作互斥,保证数据修改安全;

  3. StampedLock.WriteLock:乐观读写锁的写锁,独占模式,适用于低并发写入场景。

7.6.2 共享模式实现类(多线程并发、资源共享)

  1. Semaphore:信号量,共享模式,state代表可用许可数,用于接口限流、资源抢占、控制最大并发数;

  2. CountDownLatch:倒计时计数器,共享模式,state为剩余倒计时次数,主线程等待子线程全部执行完毕,不可重置;

  3. CyclicBarrier:循环屏障,基于AQS+ReentrantLock实现,共享排队机制,线程互相等待集齐后批量执行,支持重置;

  4. ReentrantReadWriteLock.ReadLock:读写锁中的读锁,共享模式,多线程可同时读,读操作无互斥,提升读并发性能;

  5. StampedLock.ReadLock:乐观读锁,无锁优化,共享模式,适用于读多写少、无数据一致性强校验场景。

7.6.3 独占+共享混合实现类

  1. ReentrantReadWriteLock:经典混合锁,写锁独占、读锁共享,底层维护两个AQS同步器,实现读写分离;支持锁降级(写锁降级为读锁),不支持锁升级。

  2. StampedLock:改进版读写锁,混合模式,包含悲观读、写锁、乐观读三种模式,无重入特性,高并发读写性能优于普通读写锁。

7.6.4 其他AQS衍生并发工具类

  1. Phaser:阶段同步屏障,基于AQS优化实现,整合CountDownLatch与CyclicBarrier特性,支持多阶段分批执行、动态增减线程;

  2. SynchronousQueue:同步阻塞队列,底层依托AQS实现线程一对一传递数据,无容量、不存储元素,常用于线程池瞬时任务中转;

  3. LinkedBlockingQueue:无界阻塞队列,内部使用ReentrantLock(AQS实现)保证入队、出队线程安全,分离读写锁提升并发能力。

7.6.5 面试必背总结(6句满分话术)

  1. AQS分为独占、共享、混合三大实现模式,支撑全部JUC核心工具;

  2. 独占代表ReentrantLock,适合互斥业务,单线程占用资源;

  3. 共享代表Semaphore、CountDownLatch,用于限流、线程协同;

  4. 读写锁为混合模式,写独占读共享,适配读多写少业务;

  5. 阻塞队列、阶段屏障底层均依赖AQS实现线程排队阻塞;

  6. 所有实现类核心逻辑:state管控资源、CLH队列排队阻塞。


第八部分 七大 JUC 并发工具类

简洁:

  1. CountDownLatch:倒计时计数器,主线程等待多线程完成,不可重置

  2. CyclicBarrier:循环屏障,线程互相等待集齐再执行,可重置、支持屏障回调

  3. Semaphore:信号量,控制并发数量、限流、资源抢占

  4. Phaser:阶段屏障,多阶段分批协同执行

  5. Exchanger:双线程数据交换

  6. CompletableFuture:异步编排、任务组合、回调处理

  7. ForkJoinPool:分支合并池,工作窃取算法,适合大数据拆分计算

8.1 CountDownLatch 倒计时计数器(减法计数器)

8.1.1 核心概念

作用:维护一个递减计数器,主线程等待子线程全部执行完毕,计数器归零后主线程放行。

底层原理:基于AQS共享模式,state为初始计数,每调用countDown()一次state-1;await()阻塞主线程,直到state=0唤醒。

核心特点不可重置、一次性使用、减法计数、主线程等待子线程

8.1.2 核心API

  1. CountDownLatch(int count):构造方法,初始化计数器数量

  2. countDown():计数器减一,无阻塞,执行即递减

  3. await():阻塞当前线程,直到计数器归零

  4. await(long time, TimeUnit unit):限时等待,超时自动放行

  5. getCount():获取当前剩余计数

8.1.3 生产实战代码

import java.util.concurrent.CountDownLatch;

/**
 * CountDownLatch 实战:并行批量任务
 * 场景:主线程等待5个子线程加载资源,全部完成后主线程汇总执行
 */
public class CountDownLatchDemo {
    // 初始化计数器:5个任务
    private static final CountDownLatch LATCH = new CountDownLatch(5);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + ":加载资源完成");
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 计数器必须放在finally,保证一定递减,防止主线程永久阻塞
                    LATCH.countDown();
                }
            }, "资源线程" + i).start();
        }
        System.out.println("主线程:等待所有资源加载完毕...");
        // 主线程阻塞,等待计数器归零
        LATCH.await();
        System.out.println("主线程:全部资源加载完成,开始执行业务汇总");
    }
}

8.1.4 高频易错点

  1. countDown() 必须放入finally,防止异常导致计数器无法递减,主线程永久阻塞;

  2. 计数器归零后不可重置,只能一次性使用;

  3. 仅能控制主线程等待子线程,无法实现线程互相等待;

  4. await()可中断,会抛出中断异常。

8.1.5 面试满分总结

  1. 底层AQS共享模式,state存储剩余计数;

  2. 减法计数器、不可重置、一次性消费;

  3. 适用并行任务汇总、资源批量加载;

  4. finally执行countDown,杜绝线程卡死。

8.2 CyclicBarrier 循环屏障(加法计数器)

8.2.1 核心概念

作用:加法计数器,线程之间互相等待,集齐指定数量线程后统一批量放行,支持循环复用。

底层原理:基于ReentrantLock+Condition,维护计数阈值,线程到达屏障点阻塞,集齐数量后批量唤醒,自动重置计数器。

核心特点可循环复用、加法计数、线程互相等待、支持屏障回调任务

8.2.2 核心API

  1. CyclicBarrier(int parties):初始化等待线程数量

  2. CyclicBarrier(int parties, Runnable barrierAction):集齐线程后执行回调任务

  3. await():线程到达屏障,阻塞等待集齐线程

  4. reset():手动重置屏障,主动清空计数

  5. getNumberWaiting():获取当前阻塞线程数

8.2.3 生产实战代码

import java.util.concurrent.CyclicBarrier;

/**
 * CyclicBarrier 实战:多人组队任务
 * 场景:集齐3名玩家,统一开局,重复组队(循环复用)
 */
public class CyclicBarrierDemo {
    // 初始化屏障:集齐3个线程放行,附带开局回调任务
    private static final CyclicBarrier BARRIER = new CyclicBarrier(3, () -> {
        System.out.println("【系统回调】:玩家集齐,游戏正式开局!");
    });

    public static void main(String[] args) {
        // 开启6个线程,分两批组队,体现循环复用
        for (int i = 1; i <= 6; i++) {
            new Thread(() -> {
                try {
                    System.out.println(Thread.currentThread().getName() + ":玩家就位,等待队友...");
                    // 到达屏障点阻塞
                    BARRIER.await();
                    System.out.println(Thread.currentThread().getName() + ":进入游戏对局");
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }, "玩家" + i).start();
        }
    }
}

8.2.4 高频易错点

  1. 任意线程中断,屏障直接破损,所有阻塞线程抛出异常;

  2. 自动循环重置,无需手动赋值,适合批量轮询任务;

  3. 回调任务由最后一个到达的线程执行;

  4. 不可设置超时永久等待,需手动reset修复破损屏障。

8.2.5 CountDownLatch VS CyclicBarrier(面试必考对比)

对比维度

CountDownLatch

CyclicBarrier

计数方式

减法计数

加法计数

复用性

不可重置、一次性

自动重置、循环复用

等待关系

主线程等子线程

子线程互相等待

底层实现

AQS共享模式

ReentrantLock+Condition

回调任务

支持屏障回调

8.3 Semaphore 信号量(限流工具)

8.3.1 核心概念

作用:控制最大并发线程数,实现接口限流、资源抢占、池化资源管控。

底层原理:AQS共享模式,state代表可用许可数,acquire()占用许可、release()归还许可。

核心特点公平/非公平可选、许可可重复利用、超高并发限流首选

8.3.2 核心API

  1. Semaphore(int permits):初始化许可数,默认非公平

  2. Semaphore(int permits, boolean fair):设置公平/非公平锁

  3. acquire():阻塞获取1个许可,无许可则等待

  4. release():归还1个许可

  5. tryAcquire():非阻塞尝试获取,失败直接返回false

  6. availablePermits():获取当前剩余许可

8.3.3 生产实战代码

import java.util.concurrent.Semaphore;

/**
 * Semaphore 实战:停车场限流
 * 场景:停车场仅有3个车位,10辆车排队驶入,控制最大并发为3
 */
public class SemaphoreDemo {
    // 初始化3个许可(3个车位),非公平锁
    private static final Semaphore SEMAPHORE = new Semaphore(3);

    public static void main(String[] args) {
        // 10辆汽车争抢车位
        for (int i = 1; i <= 10; i++) {
            new Thread(() -> {
                try {
                    // 获取车位许可,无车位则阻塞排队
                    SEMAPHORE.acquire();
                    System.out.println(Thread.currentThread().getName() + ":成功驶入停车场");
                    Thread.sleep(2000);
                    System.out.println(Thread.currentThread().getName() + ":驶离停车场,归还车位");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 归还许可,必须finally执行
                    SEMAPHORE.release();
                }
            }, "汽车" + i).start();
        }
    }
}

8.3.4 高频易错点

  1. release()必须放入finally,防止许可丢失,导致永久限流;

  2. 默认非公平锁,吞吐量更高,生产优先使用;

  3. 支持批量获取/归还许可(acquire(int));

  4. 不可设置负数许可,初始化必须大于等于0。

8.3.5 面试满分总结

  1. AQS共享实现,state存储许可数量;

  2. 核心用于限流、资源抢占、连接池管控;

  3. 默认非公平,吞吐量优于公平锁;

  4. acquire占用、release归还,成对使用。

8.4 Phaser 阶段同步屏障(进阶屏障)

8.4.1 核心概念

作用:整合CountDownLatch与CyclicBarrier特性,支持多阶段分批执行、动态增减线程、分层等待,JDK7推出进阶同步工具。

底层原理:基于优化版AQS,分阶段存储线程状态,支持注册、抵达、等待、进阶下一阶段。

核心特点:动态线程数、多阶段执行、支持批量注册、无需提前固定线程数量。

8.4.2 核心API

  1. register():动态注册单个线程

  2. bulkRegister(int parties):批量注册线程

  3. arrive():线程抵达当前阶段,不阻塞

  4. arriveAndAwaitAdvance():抵达并阻塞,等待同阶段线程集齐进阶

  5. getPhase():获取当前执行阶段

  6. isTerminated():判断屏障是否终止

8.4.3 生产实战代码

import java.util.concurrent.Phaser;

/**
 * Phaser 实战:多阶段任务执行
 * 场景:分2阶段完成任务,动态注册线程,阶段切换统一等待
 */
public class PhaserDemo {
    private static final Phaser PHASER = new Phaser();

    public static void main(String[] args) {
        // 动态注册5个线程
        PHASER.bulkRegister(5);
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                // 第一阶段:数据加载
                System.out.println(Thread.currentThread().getName() + ":第一阶段-加载数据");
                // 抵达并等待同阶段线程集齐
                PHASER.arriveAndAwaitAdvance();

                // 第二阶段:数据计算
                System.out.println(Thread.currentThread().getName() + ":第二阶段-计算数据");
                PHASER.arriveAndAwaitAdvance();

                System.out.println(Thread.currentThread().getName() + ":全部阶段执行完成");
            }, "任务线程" + i).start();
        }
    }
}

8.4.4 使用场景

复杂多阶段任务、分批异步处理、动态线程编排、大数据分层计算。

8.5 Exchanger 双线程数据交换

8.5.1 核心概念

作用:仅用于两个线程之间数据交换,线程成对匹配,互相传递数据,一对一交换。

底层原理:基于CAS无锁实现,维护交换槽位,线程到达交换点阻塞,配对成功后互换数据。

核心特点:仅限双线程、成对交换、超时防阻塞、无锁高性能。

8.5.2 核心API

  1. exchange(V x):传入数据,阻塞等待配对交换

  2. exchange(V x, long timeout, TimeUnit unit):限时交换,超时抛出异常

8.5.3 生产实战代码

import java.util.concurrent.Exchanger;

/**
 * Exchanger 实战:双线程数据互换
 * 场景:生产者、消费者一对一交换数据
 */
public class ExchangerDemo {
    private static final Exchanger<String> EXCHANGER = new Exchanger<>();

    public static void main(String[] args) {
        // 生产者线程
        new Thread(() -> {
            try {
                String data = "商品数据";
                System.out.println("生产者:准备交换数据 = " + data);
                // 阻塞等待消费者,互换数据
                String receive = EXCHANGER.exchange(data);
                System.out.println("生产者:收到消费者回执 = " + receive);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "生产者").start();

        // 消费者线程
        new Thread(() -> {
            try {
                String data = "确认签收";
                System.out.println("消费者:准备交换回执 = " + data);
                String receive = EXCHANGER.exchange(data);
                System.out.println("消费者:收到生产者商品 = " + receive);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "消费者").start();
    }
}

8.5.4 局限性与使用场景

  1. 局限性:只能两个线程交换,多线程会随机配对,数据错乱;

  2. 适用场景:双人通信、缓冲区数据交换、简单数据对接。

8.6 CompletableFuture 异步编排工具(生产高频)

8.6.1 核心概念

作用:JDK1.8推出,替代Future,实现异步回调、任务串行、并行、异常捕获、多任务组合,彻底解决Future阻塞get()痛点。

底层原理:基于线程池+回调钩子,无阻塞异步执行,任务完成自动触发回调。

8.6.2 核心API分类

① 创建异步任务

  1. supplyAsync():有返回值异步任务(常用)

  2. runAsync():无返回值异步任务

② 串行回调执行

  1. thenApply():接收上一步结果,处理后返回新结果

  2. thenAccept():接收结果,无返回值

  3. thenRun():不接收结果,单纯执行后置任务

③ 多任务组合

  1. allOf():所有任务全部完成,才触发回调

  2. anyOf():任意一个任务完成,立即触发回调

  3. thenCombine():两个任务结果合并处理

④ 异常处理

  1. exceptionally():异常兜底回调

  2. whenComplete():无论成功失败,都会执行

8.6.3 生产标准代码

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;

/**
 * CompletableFuture 实战:异步任务编排
 * 链式调用、异常兜底、非阻塞回调
 */
public class CompletableFutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 异步执行任务,链式编排
        CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> {
            System.out.println("第一步:执行加法计算");
            return 10 + 20;
        }).thenApply(res -> {
            System.out.println("第二步:结果乘以2");
            return res * 2;
        }).exceptionally(e -> {
            System.out.println("任务异常:" + e.getMessage());
            return 0;
        });
        // 获取最终结果(非必要不调用get,避免阻塞)
        System.out.println("最终计算结果:" + future.get());
    }
}

8.6.4 生产避坑规范

  1. 默认使用ForkJoinPool公共线程池,IO密集型业务自定义线程池隔离

  2. 禁止频繁get()阻塞获取结果,优先回调处理;

  3. 必须加exceptionally异常兜底,防止异步任务静默报错;

  4. allOf适合批量接口并行查询,大幅缩短耗时。

8.7 ForkJoinPool 分支合并池(大数据计算)

8.7.1 核心概念

作用:JDK1.7推出,专为大数据拆分计算设计,采用分治思想+工作窃取算法,将大任务拆分为小任务,递归执行,最后合并结果。

核心特性:工作窃取、递归拆分、低开销、适合CPU密集型批量计算。

8.7.2 核心组成

  1. ForkJoinPool:分支合并线程池,管理任务执行

  2. ForkJoinTask:抽象任务类,提供fork()拆分、join()合并

  3. RecursiveTask:有返回值递归任务(常用)

  4. RecursiveAction:无返回值递归任务

8.7.3 生产实战代码

import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

/**
 * ForkJoinPool 实战:1~100累加求和
 * 拆分规则:大于10个数继续拆分,小于等于10直接计算
 */
public class ForkJoinDemo extends RecursiveTask<Integer> {
    // 计算区间
    private final int start;
    private final int end;
    // 拆分阈值
    private static final int THRESHOLD = 10;

    public ForkJoinDemo(int start, int end) {
        this.start = start;
        this.end = end;
    }

    @Override
    protected Integer compute() {
        int sum = 0;
        // 区间小于阈值,直接计算
        if (end - start <= THRESHOLD) {
            for (int i = start; i <= end; i++) {
                sum += i;
            }
        } else {
            // 中间拆分,递归分支
            int mid = (start + end) / 2;
            ForkJoinDemo left = new ForkJoinDemo(start, mid);
            ForkJoinDemo right = new ForkJoinDemo(mid + 1, end);
            // 异步拆分执行
            left.fork();
            right.fork();
            // 合并结果
            sum = left.join() + right.join();
        }
        return sum;
    }

    public static void main(String[] args) {
        // 创建分支合并池
        ForkJoinPool pool = new ForkJoinPool();
        Integer result = pool.invoke(new ForkJoinDemo(1, 100));
        System.out.println("1~100累加结果:" + result);
        pool.shutdown();
    }
}

8.7.4 工作窃取算法原理

  1. 每个线程维护双端队列,存放拆分的子任务;

  2. 空闲线程从繁忙线程队列尾部窃取任务执行;

  3. 减少线程空闲等待,最大化利用CPU资源;

  4. 仅适合CPU密集型,禁止IO阻塞任务。

8.8 七大工具类终极面试对比总结(必背一页纸)

工具类

核心作用

计数方式

底层实现

适用场景

CountDownLatch

主线程等子线程

减法、不可重置

AQS共享

批量资源加载、任务汇总

CyclicBarrier

子线程互相等待

加法、可循环

ReentrantLock

组队任务、批量轮询

Semaphore

控制并发限流

许可、可复用

AQS共享

接口限流、连接池管控

Phaser

多阶段分层执行

动态、多阶段

优化AQS

复杂分层、动态线程

Exchanger

双线程数据交换

成对匹配

CAS无锁

双人通信、数据对接

CompletableFuture

异步任务编排

任务链式

线程池+回调

接口异步、多任务组合

ForkJoinPool

大数据拆分计算

递归拆分

工作窃取算法

海量数据、CPU密集计算

8.1 CompletableFuture 核心

  1. supplyAsync:有返回值异步

  2. runAsync:无返回值异步

  3. thenApply、thenAccept、thenRun 串行执行

  4. allOf 全部完成、anyOf 任意一个完成

  5. 自定义线程池隔离,避免共用公共池阻塞


第九部分 线程池(企业核心重点)

9.1 五大线程池运行状态

RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED,五大状态不可逆流转,由线程池内部ctl复合变量(线程数+运行状态)管控,下面逐状态详解、标注流转条件、线程行为、源码考点。

9.1.1 五大运行状态详细解析(面试必背)

  1. RUNNING(运行状态) 触发条件:线程池初始化创建完成,默认进入RUNNING状态; 核心权限:接收新任务、处理阻塞队列等待任务、执行正在运行任务; 底层标识:ctl高位存储状态,RUNNING=-536870912; 业务场景:正常对外提供线程调度服务。

  2. SHUTDOWN(关闭状态) 触发条件:调用 shutdown() 平缓关闭方法; 核心权限拒绝接收新任务,继续执行队列中积压任务、执行正在运行任务; 行为特征:不会中断活跃线程,优雅平滑收尾存量任务; 流转条件:队列任务全部执行完毕、工作线程数归零,进入TIDYING。

  3. STOP(停止状态) 触发条件:调用 shutdownNow() 强制关闭方法; 核心权限:拒绝新任务、丢弃队列未执行任务、强制中断正在执行任务的线程行为特征:遍历工作线程,执行interrupt()中断标记,终止运行中任务; 流转条件:所有中断线程执行完毕,工作线程数为0,进入TIDYING。

  4. TIDYING(整理状态) 触发条件:SHUTDOWN/STOP执行完毕,线程池无存活工作线程、无积压任务; 核心权限:中间过渡状态,无任务、无线程,不可接收任何任务; 行为特征:执行内部钩子方法 terminated(),可自定义线程池销毁后置逻辑; 流转条件:terminated()钩子方法执行完成,进入终止状态。

  5. TERMINATED(终止状态) 触发条件:整理状态执行完毕; 核心权限:线程池彻底死亡,永久不可复用; 行为特征:所有资源释放、线程销毁、队列清空; 注意事项:终止后的线程池无法再次提交任务,直接抛出异常。

9.1.2 状态完整流转链路(不可逆)

链路1(平缓关闭):RUNNING → shutdown() → SHUTDOWN → 队列&线程清空 → TIDYING → terminated()执行 → TERMINATED

链路2(强制关闭):RUNNING → shutdownNow() → STOP → 全部线程中断销毁 → TIDYING → terminated()执行 → TERMINATED

9.1.3 高频面试易错点

  1. 状态不可逆:线程池状态只能单向流转,一旦进入SHUTDOWN/STOP,无法回退RUNNING;

  2. ctl复合变量:JUC线程池用一个int变量ctl,高3位存状态、低29位存线程数,节省内存;

  3. shutdown与shutdownNow核心区别:前者不丢任务、不中断运行线程;后者丢弃队列任务、强制中断线程;

  4. terminated钩子方法:默认空实现,可重写用于销毁资源、打印日志、监控上报;

  5. 终止后提交任务:TERMINATED状态下execute提交任务,直接抛出RejectedExecutionException拒绝异常。

9.2 七大核心参数(面试重中之重|生产手写必背)

ThreadPoolExecutor 完整构造方法包含七大核心参数,是自定义线程池的核心,每一个参数标注源码定义、作用、生产配置、踩坑点,拒绝模糊概念,适配面试背诵+线上实操。

9.2.1 七大参数逐行详解

1.核心线程数 corePoolSize

源码类型:int

释义:线程池常驻存活的线程数量,即使空闲也不会被回收(默认规则);

执行规则:任务提交,优先创建核心线程执行任务,直到核心线程数打满;

生产配置:CPU密集型=CPU核心数+1,IO密集型=CPU核心数*2;

坑点:核心线程过多占用常驻内存,过少导致频繁创建线程。

2.最大线程数 maximumPoolSize

源码类型:int

释义:线程池允许创建的最大工作线程总数;

执行规则:核心线程已满、队列已满,才会创建非核心线程,直到达到最大值;

计算公式:最大线程数 >= 核心线程数;

生产配置:IO密集型可适当拉大,CPU密集型尽量偏小,防止CPU打满。

3.空闲存活时间 keepAliveTime

源码类型:long

释义:非核心线程空闲闲置的最大存活时长;

执行规则:非核心线程空闲时间超过该值,自动被回收,释放资源;

默认规则:核心线程永久不回收;

拓展:开启allowCoreThreadTimeOut后,核心线程也会超时回收。

4.时间单位 unit

源码类型:TimeUnit

释义:搭配keepAliveTime使用,指定时间单位;

常用单位:TimeUnit.SECONDS(秒)、MILLISECONDS(毫秒);

生产规范:业务线程池统一使用秒级,可读性更高。

5.阻塞队列 workQueue

源码类型:BlockingQueue<Runnable>

释义:存储等待执行任务的阻塞队列,仅存放提交的Runnable任务;

执行规则:核心线程已满,新任务进入队列排队;队列满才扩容非核心线程;

生产选型:无界队列、有界队列严格区分,生产禁止无界队列防止OOM

6.线程工厂 threadFactory

源码类型:ThreadFactory

释义:专门用于创建线程的工厂类,统一定义线程属性;

生产规范:必须自定义线程工厂,设置业务线程名称、守护线程优先级、异常捕获

坑点:默认工厂创建无名称线程,线上故障无法溯源排查。

7.拒绝策略 handler

源码类型:RejectedExecutionHandler

释义:线程池、队列全部打满,无法处理新任务时的兜底拒绝规则;

默认策略:AbortPolicy直接抛出异常;

生产规范:根据业务自定义降级、丢弃、告警策略,禁止默认报错。

9.2.2 线程池执行完整流程(面试必考流程图话术)

  1. 提交任务,判断当前核心线程数 < 核心线程数:新建核心线程执行任务;

  2. 核心线程已满,判断阻塞队列是否未满:任务入队排队等待;

  3. 队列已满,判断当前线程数 < 最大线程数:新建非核心线程执行任务;

  4. 线程数达到最大值、队列已满:触发拒绝策略

  5. 任务执行完毕,非核心线程空闲超时,自动回收销毁。

9.2.3 高频易错坑点(生产踩坑)

  1. 不要混淆核心线程与最大线程:只有队列满了才会扩容非核心线程;

  2. 无界队列会导致最大线程数失效:任务无限堆积,引发OOM内存溢出;

  3. 默认线程工厂无线程名,线上排查堆栈无法定位业务;

  4. keepAliveTime仅作用非核心线程,默认不回收核心线程;

  5. 七大参数赋值必须合法:核心线程数不能大于最大线程数。

9.2.4 面试满分背诵总结(6句极简话术)

  1. 七大参数:核心数、最大数、超时时间、时间单位、阻塞队列、线程工厂、拒绝策略;

  2. 执行顺序:核心线程→阻塞队列→非核心线程→拒绝策略;

  3. 非核心线程超时回收,核心线程默认常驻;

  4. 生产必须用有界队列,杜绝无界队列OOM;

  5. 自定义线程工厂,方便线上故障溯源;

  6. 拒绝策略适配业务,禁止直接抛异常。

9.3 六大常用阻塞队列(面试高频+生产选型)

阻塞队列是线程池核心存储容器,属于JUC包下线程安全队列,自带阻塞入队、阻塞出队特性;当队列满时写入线程阻塞,队列空时读取线程阻塞,天然适配生产者消费者模型。六大队列覆盖绝大多数业务场景,下面逐个详解底层、特性、优缺点、生产用法、代码示例。

简洁:

  1. AbortPolicy:直接抛异常(默认)

  2. CallerRunsPolicy:主线程执行任务

  3. DiscardPolicy:丢弃当前任务

  4. DiscardOldestPolicy:丢弃队列最久任务

  5. 自定义拒绝策略

  6. 限流降级策略

9.3.1 ArrayBlockingQueue(有界数组阻塞队列)

  1. 底层结构:固定长度数组,初始化必须指定容量,长度不可变

  2. 锁机制:全局唯一ReentrantLock(生产一把锁,读写互斥)

  3. 阻塞条件:队列满,put()阻塞;队列空,take()阻塞

  4. 排序规则:先进先出FIFO,有序存储

  5. 优点:结构简单、内存连续、无扩容开销、线程安全

  6. 缺点:读写互斥、并发吞吐量低、容量固定不可动态扩容

  7. 生产场景:固定并发量、任务波动小、低并发业务线程池

  8. 面试坑点:初始化必须传容量,无无参构造;一把锁导致读写不能并行

// 固定容量为5的有界阻塞队列
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(5);

9.3.2 LinkedBlockingQueue(无界/有界链表阻塞队列)

  1. 底层结构:单向链表结构,节点动态新增销毁

  2. 锁机制:两把ReentrantLock(写入锁、读取锁分离,读写并行)

  3. 容量规则:无参构造默认容量Integer.MAX_VALUE(近乎无界),有参可指定容量

  4. 排序规则:先进先出FIFO

  5. 优点:读写锁分离、并发吞吐量高、链表动态扩容、无固定长度限制

  6. 缺点:无参无界队列极易堆积任务引发OOM;频繁创建节点造成GC压力

  7. 生产场景:newFixedThreadPool、newSingleThreadExecutor底层队列;常规业务异步任务

  8. 面试坑点:生产禁止使用无参构造,必须手动指定容量,防止无限堆积OOM

// 手动指定容量,避免无界OOM
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(100);

9.3.3 SynchronousQueue(同步移交队列|无容量)

  1. 底层结构:无数组、无链表,容量永久为0

  2. 存储特性:不存储任何任务,生产者提交任务必须等待消费者消费,一对一移交

  3. 锁机制:CAS无锁+栈/队列算法,高性能移交

  4. 模式选择:默认非公平栈模式,可设置公平队列模式

  5. 优点:无任务堆积、实时响应、吞吐量极高、无内存占用

  6. 缺点:无缓冲、生产者必须阻塞等待消费

  7. 生产场景:newCachedThreadPool底层队列;瞬时高并发、短耗时任务、无积压需求

  8. 面试坑点:不能调用peek()获取元素,永远返回null;无容量,put后必须等待take

// 同步移交队列,不存任务,一对一传递
SynchronousQueue<String> queue = new SynchronousQueue<>();

9.3.4 DelayQueue(延时阻塞队列|定时任务)

  1. 底层结构:优先级队列+可延迟元素,底层基于最小堆

  2. 元素要求:元素必须实现Delayed接口,重写延时时间方法

  3. 出队规则:只有元素延时时间到期,才能被取出;未到期则阻塞

  4. 锁机制:ReentrantLock独占锁

  5. 优点:天然支持延时任务、优先级排序、无需额外定时线程

  6. 缺点:排序消耗CPU、元素实现复杂、不适合高频瞬时任务

  7. 生产场景:订单超时关闭、红包过期、延时重试、缓存失效

  8. 面试坑点:无到期元素时,take()永久阻塞;不允许存储null元素

// 延时队列,元素必须实现Delayed接口
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();

9.3.5 PriorityBlockingQueue(优先级阻塞队列)

  1. 底层结构:可变长度数组,最小堆排序算法

  2. 排序规则:自定义Comparator比较器,优先级高的元素优先出队,无序入队、有序出队

  3. 容量特性:无界队列,自动扩容,初始容量11

  4. 锁机制:全局ReentrantLock独占锁

  5. 优点:支持任务优先级、自动扩容、高优先级任务优先执行

  6. 缺点:排序耗时、扩容消耗内存、低优先级线程容易饥饿

  7. 生产场景:消息优先级推送、紧急任务插队、权重排序业务

  8. 面试坑点:无界队列有OOM风险;非FIFO,打破先进先出规则

// 自定义比较器,实现任务优先级排序
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(11, Comparator.comparing(Task::getLevel));

9.3.6 LinkedTransferQueue(无锁高效转移队列)

  1. 底层结构:CAS无锁单向链表,JDK1.7新增高性能队列

  2. 核心特性:融合SynchronousQueue+LinkedBlockingQueue优势,支持批量转移、预占节点

  3. 关键方法:transfer(),生产者直接把元素转移给消费者,无消费者则阻塞

  4. 锁机制:全程CAS无锁,无悲观锁开销

  5. 优点:并发性能天花板、无锁开销、支持批量操作、吞吐量极高

  6. 缺点:源码复杂、日常业务使用频率低、调试难度大

  7. 生产场景:超高并发中间件、网关转发、海量异步消息流转

  8. 面试坑点:JDK1.7推出,无锁实现;transfer()区别于put(),必须等待消费

// 高性能无锁转移队列,超高并发专用
LinkedTransferQueue<String> queue = new LinkedTransferQueue<>();

9.3.7 六大阻塞队列终极对比表(面试必背)

队列名称

底层结构

容量

锁机制

核心场景

ArrayBlockingQueue

固定数组

有界

单锁

低并发、固定任务量

LinkedBlockingQueue

单向链表

可界/无界

双锁

常规业务线程池

SynchronousQueue

无存储结构

容量0

CAS无锁

瞬时高并发、无积压

DelayQueue

最小堆

无界

单锁

延时过期、定时任务

PriorityBlockingQueue

可变数组

无界

单锁

任务优先级排序

LinkedTransferQueue

单向链表

无界

CAS无锁

超高并发中间件

9.3.8 生产队列选型黄金规则

  1. 常规业务:优先带容量LinkedBlockingQueue,读写分离、性能均衡;

  2. 瞬时高并发:SynchronousQueue,无积压、实时移交;

  3. 定时过期任务:DelayQueue,无需额外定时器;

  4. 有优先级需求:PriorityBlockingQueue,紧急任务插队;

  5. 超高并发中间件:LinkedTransferQueue,无锁高性能;

  6. 固定少量并发:ArrayBlockingQueue,结构简单易维护。

9.4 六大拒绝策略(源码详解+生产场景+代码)

线程池在核心线程已满、阻塞队列已满、最大线程数打满的饱和状态下,新提交任务会触发拒绝策略。JDK原生提供4种拒绝策略,额外扩展2种企业常用自定义策略,合称六大拒绝策略,下面逐个详解底层源码、执行逻辑、优缺点、适用场景,附带实操代码。

9.4.1 AbortPolicy(直接抛异常|JDK默认策略)

  1. 执行逻辑:直接抛出RejectedExecutionException运行时异常,中断任务提交,拒绝执行新任务。

  2. 源码原理:判断线程池非运行状态,直接throw异常,无任何兜底处理。

  3. 优点:报错直观、快速感知线程池饱和,及时发现并发压力。

  4. 缺点:直接报错、中断业务,无容错能力,容易导致接口报错。

  5. 适用场景:后台定时任务、非核心业务、需要严格监控异常的任务。

  6. 生产禁忌:禁止用于用户直连接口,会直接抛出异常影响用户体验。

// 默认拒绝策略:AbortPolicy
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        2,
        5,
        10,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

9.4.2 CallerRunsPolicy(调用者执行策略|主线程兜底)

  1. 执行逻辑:线程池饱和后,新任务由提交任务的主线程执行,不丢弃、不报错。

  2. 源码原理:判断线程池运行状态,调用任务run()方法,直接在调用线程执行。

  3. 优点:无任务丢失、无异常抛出,简单兜底,保证任务一定执行。

  4. 缺点:阻塞主线程、拖慢接口响应速度,吞吐量急剧下降。

  5. 适用场景:任务不允许丢失、对响应耗时不敏感、低优先级业务。

// 调用者执行策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        2,
        5,
        10,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.CallerRunsPolicy()
);

9.4.3 DiscardPolicy(静默丢弃策略|无感知舍弃)

  1. 执行逻辑:线程池饱和,直接静默丢弃当前提交的新任务,不报错、无日志、无提醒

  2. 源码原理:空实现,拒绝方法内无任何代码,直接舍弃任务。

  3. 优点:无异常、无阻塞、性能损耗极低。

  4. 缺点:任务丢失无感知,线上故障难以排查,存在数据遗漏风险。

  5. 适用场景:可丢弃的日志埋点、统计上报、非核心冗余任务。

// 静默丢弃策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        2,
        5,
        10,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardPolicy()
);

9.4.4 DiscardOldestPolicy(丢弃最旧任务|留存新任务)

  1. 执行逻辑:线程池饱和,丢弃队列头部存活最久、未执行的旧任务,腾出空间执行当前新任务。

  2. 源码原理:获取队列,poll()删除队首旧任务,再次尝试提交当前新任务。

  3. 优点:优先保留最新任务,适配时效性强的业务。

  4. 缺点:旧任务无提醒丢失,任务顺序混乱,不适合有序业务。

  5. 适用场景:实时性要求高、旧数据无意义的业务(如实时推送、实时监控)。

// 丢弃最旧任务策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        2,
        5,
        10,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.DiscardOldestPolicy()
);

9.4.5 自定义拒绝策略(企业通用|个性化兜底)

  1. 执行逻辑:实现RejectedExecutionHandler接口,重写拒绝方法,自定义兜底逻辑。

  2. 常用拓展:任务持久化入库、打印告警日志、推送监控报警、重试机制。

  3. 优点:高度灵活、适配业务、可溯源、无莫名丢失任务。

  4. 缺点:需要手动编码,开发成本略高。

  5. 适用场景:绝大多数线上生产业务,企业级标准规范。

/**
 * 自定义拒绝策略:日志告警+任务持久化
 * 生产通用模板,可直接复用
 */
public class CustomRejectPolicy implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 1.打印告警日志,记录丢失任务信息
        System.err.println("线程池饱和,任务被拒绝,当前时间:" + System.currentTimeMillis());
        // 2.可拓展:任务存入Redis/数据库,定时重试
        // 3.可拓展:推送钉钉/企业微信告警,通知运维
    }
}

// 使用自定义拒绝策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
        2,
        5,
        10,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(10),
        Executors.defaultThreadFactory(),
        new CustomRejectPolicy()
);

9.4.6 限流降级策略(高并发专属|大厂方案)

  1. 执行逻辑:结合令牌桶、漏桶算法,线程池饱和时触发流量降级,返回友好提示、熔断接口。

  2. 底层原理:整合Sentinel、Resilience4j限流组件,超出阈值直接熔断,保护服务不雪崩。

  3. 优点:服务熔断降级、防止雪崩、保护核心接口、用户体验友好。

  4. 缺点:需要引入限流组件,架构复杂度提升。

  5. 适用场景:秒杀、抢购、网关、高并发流量接口。

9.4.7 六大拒绝策略终极对比+生产选型(必背)

拒绝策略

核心行为

优缺点

生产适用场景

AbortPolicy

直接抛出异常

报错直观、影响业务

后台定时、非核心任务

CallerRunsPolicy

主线程执行任务

无丢失、阻塞主线程

低优先级、不可丢任务

DiscardPolicy

静默丢弃新任务

无报错、任务易丢失

日志埋点、冗余统计任务

DiscardOldestPolicy

丢弃队列最旧任务

保留新任务、顺序混乱

实时推送、监控时效性任务

自定义拒绝策略

告警+持久化+重试

灵活可控、可溯源

绝大多数线上业务(推荐)

限流降级策略

熔断降级、流量管控

防雪崩、架构复杂

秒杀、网关、高并发接口

9.4.8 面试满分总结(7句背诵话术)

  1. JDK原生4种拒绝策略:抛异常、主线程执行、丢弃新任务、丢弃旧任务;

  2. AbortPolicy是默认策略,直接抛出拒绝异常;

  3. DiscardPolicy静默丢失任务,无任何日志,生产慎用;

  4. DiscardOldestPolicy淘汰队列头部旧任务,适配实时业务;

  5. CallerRunsPolicy不丢任务,但会阻塞主线程;

  6. 生产优先自定义拒绝策略,做日志告警+任务持久化;

  7. 超高并发接口采用限流降级,防止服务雪崩。

9.5 内置四大线程池(生产禁止使用)

JDK通过Executors工具类封装4种便捷线程池,底层全部存在致命缺陷,阿里巴巴开发手册强制禁止生产使用,仅适用于测试、学习、本地简单demo,下面逐个拆解底层参数、源码、优缺点、禁用原因、适用场景。

9.5.1 newFixedThreadPool(固定线程池)

  1. 创建方式:创建固定线程数量的线程池,线程永久存活

  2. 底层源码参数:核心线程数=自定义固定值、最大线程数=核心线程数、无空闲回收时间、队列=无界LinkedBlockingQueue

  3. 执行特点:线程数量恒定,不会扩容、不会回收,任务无限存入队列

  4. 优点:线程可控、执行有序、无频繁创建销毁线程开销

  5. 致命缺点:使用无界队列,高并发下任务无限堆积,内存持续飙升,触发OOM内存溢出

  6. 生产禁用原因:无任务上限,无法触发拒绝策略,海量任务积压打爆JVM内存

  7. 适用场景:任务量平稳、并发量极低、测试环境串行批量任务

// 固定3条工作线程
ExecutorService fixedPool = Executors.newFixedThreadPool(3);

9.5.2 newSingleThreadExecutor(单一线程池)

  1. 创建方式:内部仅有1条工作线程,串行执行所有任务

  2. 底层源码参数:核心线程数=1、最大线程数=1、无空闲回收时间、队列=无界LinkedBlockingQueue

  3. 执行特点:严格串行执行,任务排队依次执行,不会并发错乱

  4. 优点:单线程执行,无线程安全竞争,任务执行有序

  5. 致命缺点:同样使用无界队列,海量任务积压引发OOM;单线程执行效率极低,吞吐量差

  6. 生产禁用原因:无任务上限、无拒绝策略,高并发极易内存溢出,且无法利用多核CPU

  7. 适用场景:简单串行任务、日志顺序打印、单机极简同步任务

// 仅有一条工作线程,串行执行
ExecutorService singlePool = Executors.newSingleThreadExecutor();

9.5.3 newCachedThreadPool(缓存线程池)

  1. 创建方式:无固定线程数,按需创建线程,空闲线程自动回收

  2. 底层源码参数:核心线程数=0、最大线程数=Integer.MAX_VALUE、空闲存活时间60秒、队列=SynchronousQueue同步移交队列

  3. 执行特点:任务到来无空闲线程则新建线程,线程空闲60秒自动销毁,实时移交任务无队列堆积

  4. 优点:响应速度快、瞬时并发能力强、空闲线程自动回收

  5. 致命缺点:最大线程数近乎无限,高并发瞬间疯狂创建线程,线程数量打爆操作系统上限,造成CPU飙高、线程栈溢出、服务卡死

  6. 生产禁用原因:无线程数量限制,恶意流量/突发流量瞬间创建上千条线程,操作系统线程调度崩溃

  7. 适用场景:大量短耗时、瞬时突发、无压力测试任务

// 可无限创建线程,空闲线程60s回收
ExecutorService cachedPool = Executors.newCachedThreadPool();

9.5.4 newScheduledThreadPool(定时线程池)

  1. 创建方式:支持延迟执行、周期性循环执行的定时线程池

  2. 底层源码参数:核心线程数=自定义、最大线程数=Integer.MAX_VALUE、队列=延时无界DelayedWorkQueue

  3. 执行特点:支持延迟执行、固定频率执行、固定间隔执行,适配定时任务

  4. 优点:自带定时调度能力,无需手动封装延时逻辑

  5. 致命缺点:最大线程数无上限、队列无界,异常定时任务堆积、线程无限创建,引发OOM+线程溢出

  6. 生产禁用原因:边界不可控,异常任务持续堆积,内存泄露、线程泛滥风险极高

  7. 适用场景:本地简单定时测试、非线上业务延时任务

// 核心线程数2,无限扩容非核心线程
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 延迟3秒执行任务
scheduledPool.schedule(()-> System.out.println("定时任务执行"),3,TimeUnit.SECONDS);

9.5.5 四大内置线程池致命缺陷汇总(面试必背)

  1. Fixed/Single:无界阻塞队列,任务无限积压,触发OOM内存溢出;

  2. Cached/Scheduled:最大线程数MAX_VALUE,无限创建线程,耗尽系统线程资源;

  3. 全部无自定义拒绝策略,饱和后无兜底,线上故障无法降级;

  4. 默认无线程名称、无异常捕获,线上故障无法溯源排查;

  5. 阿里规范硬性约束:禁止使用Executors创建线程池,必须手动自定义ThreadPoolExecutor。

9.5.6 生产替代方案(标准手写模板)

生产统一手动创建ThreadPoolExecutor,自定义七大参数、有界队列、业务线程名、自定义拒绝策略,规避所有内置线程池漏洞。

/**
 * 企业标准自定义线程池(可直接生产复用)
 * 规避内置线程池所有缺陷:有界队列、有限线程、自定义拒绝策略
 */
public class BusinessThreadPool {
    // 全局公共业务线程池
    public static final ThreadPoolExecutor BUSINESS_POOL = new ThreadPoolExecutor(
            // 核心线程数:IO密集型=CPU*2
            Runtime.getRuntime().availableProcessors() * 2,
            // 最大线程数
            20,
            // 非核心线程空闲存活时间
            10,
            TimeUnit.SECONDS,
            // 有界队列,固定容量防止OOM
            new LinkedBlockingQueue<>(100),
            // 自定义线程工厂,命名+异常捕获,方便排查
            new ThreadFactoryBuilder().setNameFormat("business-pool-%d").build(),
            // 自定义拒绝策略,日志告警+持久化兜底
            new CustomRejectPolicy()
    );
}

9.6 业务线程数配置公式(生产完整版|面试必考)

常规简易公式(入门背诵,通用基础配置)

  1. CPU 密集型:核心线程数 = CPU 核心数 + 1 适用场景:大量计算、加密解密、循环逻辑、无IO阻塞、CPU持续高占用 设计逻辑:多出1条线程兜底,防止线程偶然阻塞导致CPU空转,最大化利用CPU算力

  2. IO 密集型:核心线程数 = CPU 核心数 * 2 适用场景:数据库查询、Redis缓存、网络请求、文件读写、接口调用 设计逻辑:IO阻塞时线程休眠,多线程可复用CPU,提升吞吐量


进阶精准公式(企业生产调优|核心必背)

通用公式:核心线程数 = CPU核心数 / (1 - 阻塞系数)

  1. 阻塞系数β:线程阻塞时间 / 线程总执行时间,取值范围 0~1

  2. CPU密集型:阻塞系数0~0.2,极少阻塞,线程专注计算

  3. 普通IO密集型:阻塞系数0.5左右,一半时间阻塞等待IO

  4. 重度IO密集型:阻塞系数0.8~0.9,大部分时间阻塞(如慢SQL、第三方接口)

9.6.1 公式实战计算案例

  1. 案例1:8核CPU,CPU密集型任务,阻塞系数0.1 核心线程数 = 8 / (1 - 0.1) ≈ 9,贴合简易公式【核心数+1】

  2. 案例2:8核CPU,普通IO任务,阻塞系数0.5 核心线程数 = 8 / (1 - 0.5) = 16,贴合简易公式【核心数*2】

  3. 案例3:8核CPU,重度IO慢接口,阻塞系数0.8 核心线程数 = 8 / (1 - 0.8) = 40,需大幅扩容线程,适配长时间阻塞任务

9.6.2 最大线程数配置规范

  1. CPU密集型:最大线程数 = 核心线程数(不扩容,避免CPU上下文切换)

  2. 普通IO密集型:最大线程数 = 核心线程数 * 1.5 ~ 2(预留扩容余量)

  3. 重度IO密集型:最大线程数 = 核心线程数 * 2 ~ 3(应对流量峰值)

9.6.3 生产特殊场景优化配置

  1. 机器配置受限:低配置服务器,核心线程数下调20%,防止CPU打满告警

  2. 延迟敏感业务:优先调大核心线程数,减少任务排队延迟,牺牲少量CPU换响应速度

  3. 非核心兜底任务:缩小线程数、加大队列容量,降低资源占用,不抢占核心业务资源

  4. 多线程池共存:多个业务线程池总和,核心线程总数不超过CPU核心数*5,避免线程泛滥

9.6.4 面试满分总结(6句背诵)

  1. 简易公式:CPU密集+1,IO密集乘2;

  2. 精准公式:核心数=CPU核数/(1-阻塞系数);

  3. 阻塞系数越大、IO阻塞越久,需要线程数越多;

  4. CPU密集严控线程数,防止上下文切换;

  5. IO密集适当扩容,利用阻塞时间提升吞吐量;

  6. 线上优先压测调优,公式仅作初始参考基准。

9.7 线程池关闭

  1. shutdown ():平缓关闭,执行完队列任务

  2. shutdownNow ():强制关闭,中断正在执行任务

9.8 线程池踩坑点

  1. 坑点1:线程池内部异常静默丢失(高频踩坑) 问题现象:线程池执行任务抛出异常,无控制台打印、无业务报错,线上悄无声息任务失败,难以排查; 产生原因:ThreadPoolExecutor内部捕获任务异常,未主动向外抛出,普通无返回值任务异常直接被吞; 解决方案:①任务内部手动try-catch捕获异常并打印日志;②自定义线程工厂设置全局异常处理器UncaughtExceptionHandler;③使用Future接收返回值,get()方法捕获执行异常。 

// 全局异常处理器,防止线程池异常丢失
new ThreadFactoryBuilder()
        .setNameFormat("business-pool-%d")
        .setUncaughtExceptionHandler((thread,throwable)->{
            System.err.println("线程池任务异常:"+throwable.getMessage());
        }).build();
  1. 坑点2:核心线程无空闲回收,长期占用资源 问题现象:业务低峰期无任务,核心线程常驻内存,一直占用线程资源,造成资源浪费; 底层原理:默认配置下allowCoreThreadTimeOut=false,仅非核心线程执行超时回收,核心线程永久存活; 解决方案:低流量、间歇性业务,开启参数allowCoreThreadTimeOut(true),让核心线程空闲超时自动销毁,节省服务器资源。

  2. 坑点3:线程池无监控,线上故障无法溯源 问题现象:线上任务积压、线程卡死、CPU飙高,无法快速定位线程池状态、堆积任务数、活跃线程数; 监控核心指标:活跃线程数、完成任务数、队列积压数量、最大并发线程数、拒绝任务次数; 解决方案:自定义监控定时打印线程池指标,接入Prometheus+Grafana可视化监控,队列积压阈值触发告警。

// 线程池核心监控指标
System.out.println("活跃线程数:"+pool.getActiveCount());
System.out.println("队列积压数:"+pool.getQueue().size());
System.out.println("已完成任务:"+pool.getCompletedTaskCount());
  1. 坑点4:定时线程池ScheduledThreadPool任务堆积串行阻塞 问题现象:单个定时任务执行耗时过长,阻塞后续定时任务,任务执行间隔错乱、叠加积压; 底层原理:scheduleAtFixedRate&scheduleWithFixedDelay底层单线程串行执行,上一个任务未结束,下一个任务无法执行; 解决方案:耗时定时任务单独开辟子线程执行,拆分任务,避免定时任务内部阻塞。

  2. 坑点5:线程池混用、业务耦合互相抢占资源 问题现象:接口同步任务、异步日志、定时任务共用同一个线程池,高并发下核心业务被非核心任务阻塞,接口超时; 核心原则:业务隔离、池隔离,核心业务、非核心业务、定时任务拆分独立线程池; 优化方案:拆分核心业务池、异步兜底池、定时任务池,互不抢占线程资源,保障核心接口优先级。

  3. 坑点6:任务耗时过长,线程长期不释放 问题现象:线程池内执行慢SQL、第三方超时接口、大文件IO,线程一直被占用,新任务大量积压; 解决方案:①给任务添加超时时间,使用try-catch+超时中断;②第三方接口设置连接超时、读取超时;③耗时任务单独隔离线程池。

  4. 坑点7:线程池忘记关闭,造成内存泄露 问题现象:临时线程池、定时线程池使用完毕未关闭,常驻JVM,线程一直存活,内存缓慢泄漏; 适用场景:临时批量处理任务、一次性异步任务、非全局常驻线程池; 解决方案:临时线程池使用try-finally,finally中执行shutdown平缓关闭,防止线程泄露。

  5. 坑点8:队列容量设置不合理引发故障 错误配置:①队列容量过大,任务积压过多导致OOM;②队列容量过小,频繁触发拒绝策略,业务报错; 生产规范:常规业务队列容量设置50~200,瞬时高并发业务结合限流,队列缩小+扩容最大线程数。

  6. 坑点9:线程池优先级滥用无效 问题现象:自定义线程优先级,认为高优先级线程优先执行,实际无效果; 底层原理:Java线程优先级仅为JVM调度建议,操作系统不保证执行顺序,高并发下优先级失效; 避坑方案:不要依赖线程优先级做业务排序,优先级仅用于系统底层调度。

  7. 坑点10:Lambda任务无法排查堆栈 问题现象:线程池直接提交Lambda匿名任务,线上线程堆栈无业务类名、无方法名,故障无法定位代码位置; 解决方案:自定义线程名称、拆分业务任务类,禁止大量匿名Lambda任务,方便堆栈排查。


第十部分 JUC 并发容器(完整版|面试+生产)

JUC并发容器位于java.util.concurrent包下,全部是线程安全集合,专为高并发场景设计;区别于普通集合+Collections.synchronizedxxx包装类,JUC容器采用CAS无锁、分段锁、写时复制等优化,并发吞吐量碾压同步包装类。 核心设计思想:细分锁粒度、减少锁竞争、无锁CAS、读写分离,适配不同并发业务场景。


10.1 JUC容器分类总览

  1. 并发List:CopyOnWriteArrayList、CopyOnWriteArraySet

  2. 并发Map:ConcurrentHashMap、ConcurrentSkipListMap

  3. 并发队列:阻塞队列、非阻塞队列(前文线程池已详解阻塞队列)

  4. 并发Set:CopyOnWriteArraySet、ConcurrentSkipListSet

简洁:

  1. CopyOnWriteArrayList / CopyOnWriteArraySet:写时复制,读极快,写开销大

  2. ConcurrentHashMap:1.7 分段锁、1.8 数组 + 链表 + 红黑树,扩容迁移、helpTransfer

  3. ConcurrentLinkedQueue:无锁高性能并发队列

  4. ConcurrentSkipListMap:有序并发 Map

  5. 普通集合包装类 synchronizedMap 性能差,不推荐高并发使用


10.2 CopyOnWriteArrayList(写时复制数组列表)

10.2.1 底层原理(面试必考)

写时复制机制:新增、修改、删除等写操作时,先拷贝原数组生成新数组,在新数组完成写操作,修改完毕将原数组引用指向新数组;读操作全程无锁,直接读取原数组。

10.2.2 核心特性

  1. 加锁方式:写操作加ReentrantLock独占锁,防止多线程并发写覆盖;读操作无锁。

  2. 数据一致性:最终一致性,非实时强一致;写操作未完成时,读线程依旧读取旧数组数据。

  3. 元素特性:允许存储null元素,可重复存储元素。

  4. 扩容机制:每次写操作拷贝数组,扩容无需单独触发,直接生成指定长度新数组。

10.2.3 优缺点详解

  1. 优点:读无锁、读取吞吐量极高、并发读性能碾压ArrayList+同步锁;遍历不会抛出并发修改异常。

  2. 缺点:写操作需要拷贝数组,内存占用翻倍、写开销极大;频繁增删改场景CPU消耗高。

10.2.4 生产适用场景

读多写少场景:配置信息、白名单、本地缓存、常量列表、极少修改的静态数据。

10.2.5 高频易错坑点

  1. 内存占用高:写操作永久存在新旧双数组,大数据量下极易占用堆内存。

  2. 数据弱一致性:写操作期间读取旧数据,无法满足实时强一致业务。

  3. 不适合频繁写:频繁增删会反复拷贝数组,引发频繁GC、CPU飙升。

  4. 迭代器不可修改:迭代器仅可读,不支持add/remove修改操作。

10.2.6 代码示例

import java.util.concurrent.CopyOnWriteArrayList;
/**
 * 写时复制列表:读多写少专用
 */
public class CopyOnWriteListDemo {
    public static void main(String[] args) {
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        // 多线程并发读写,无并发修改异常
        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                list.add("元素" + i);
            }
        }).start();
        // 读操作无锁,实时读取旧数组数据
        list.forEach(System.out::println);
    }
}

10.3 CopyOnWriteArraySet(写时复制无序集合)

10.3.1 底层原理

底层直接依托CopyOnWriteArrayList实现,通过addIfAbsent()方法保证元素唯一性,添加前遍历判断元素是否存在,存在则放弃添加。

10.3.2 核心特性&;适用场景

  1. 继承写时复制所有特性:读无锁、写拷贝、弱一致性。

  2. 去重逻辑基于遍历判断,大数据量下查重效率极低。

  3. 生产场景:少量元素、读多写少、去重静态集合,如权限标识、功能开关集合。

10.3.3 致命缺点

addIfAbsent去重时间复杂度O(n),元素数量超过1000不推荐使用,查重耗时严重。

10.4 ConcurrentHashMap(并发哈希映射|面试重中之重)

JDK最常用并发容器,替代HashMap+同步锁,线程安全、并发性能极高;严格区分JDK1.7、JDK1.8底层实现,面试必背版本差异。

10.4.1 JDK1.7 底层原理

  1. 数据结构:Segment分段锁 + 数组 + 链表。

  2. 分段锁机制:默认16个Segment分段,每段独立ReentrantLock,不同分段互不阻塞,并发度=16。

  3. 扩容逻辑:单段独立扩容,不会全局重哈希,扩容仅影响当前分段。

  4. 查询时间:哈希冲突严重,链表过长,查询效率偏低。

10.4.2 JDK1.8 底层原理(主流版本)

  1. 数据结构:数组 + 链表 + 红黑树,废弃分段锁

  2. 加锁方式:CAS乐观锁 + synchronized内置锁,锁粒度细化到数组桶位

  3. 树化阈值:链表长度≥8 && 数组容量≥64,链表转为红黑树;链表长度≤6,树退化为链表。

  4. 扩容机制:全局扩容,多线程协助迁移(helpTransfer),并发扩容减少阻塞。

  5. 哈希算法:优化扰动函数,减少哈希碰撞,高低位混合哈希。

10.4.3 JDK1.7 & 1.8 核心区别(必背8条)

  1. 数据结构:1.7分段锁+链表;1.8数组+链表+红黑树。

  2. 锁实现:1.7 ReentrantLock;1.8 synchronized+CAS。

  3. 锁粒度:1.7分段;1.8桶位,粒度更细、并发更高。

  4. 扩容:1.7单段扩容;1.8多线程协助迁移。

  5. 树化:1.7无红黑树;1.8链表超长转红黑树。

  6. 空值:两个版本key、value均不允许为null

  7. 初始化:1.7默认容量16*16;1.8默认容量16,负载因子0.75。

  8. 并发度:1.7固定16;1.8无固定并发度,依托桶位锁。

10.4.4 核心高频考点

  1. 为什么不用ReentrantLock?:synchronized经过JDK1.6锁优化,偏向锁+轻量级锁性能优于ReentrantLock,低竞争场景开销更低。

  2. 为什么红黑树阈值是8?:泊松分布,链表长度超过8概率极低,平衡树化开销与查询效率。

  3. helpTransfer机制:扩容时,空闲线程协助迁移其他桶位数据,缩短扩容耗时。

  4. 死链问题:JDK1.7多线程扩容存在循环链表死链,CPU飙高;1.8彻底修复。

10.4.5 生产使用规范

  1. 初始化指定预估容量,避免频繁扩容:初始容量=预估元素数量/0.75+1。

  2. 高并发统计场景,优先使用computeIfAbsent原子复合操作。

  3. 批量遍历采用迭代器遍历,禁止for循环快速失败异常。

10.5 并发有序集合(跳表系列)

10.5.1 ConcurrentSkipListMap(并发有序Map)

  1. 底层结构:跳表(多层有序链表),无红黑树、无锁CAS实现。

  2. 排序规则:默认key自然排序,支持自定义比较器。

  3. 核心优势:有序、高并发、插入删除查询时间复杂度O(logn)。

  4. 适用场景:高并发有序存储、时间轴排序、区间查询业务。

  5. 对比TreeMap:TreeMap单线程红黑树;ConcurrentSkipListMap并发跳表,线程安全。

10.5.2 ConcurrentSkipListSet(并发有序Set)

  1. 底层依托ConcurrentSkipListMap实现,key存储元素,value为固定占位对象。

  2. 有序、去重、线程安全,适合高并发有序去重场景。

10.6 非阻塞并发队列

10.6.1 ConcurrentLinkedQueue(无锁高性能队列)

  1. 底层结构:单向链表,全程CAS无锁实现,无悲观锁开销。

  2. 特性:无界、FIFO先进先出、不允许null元素、高并发吞吐量极高。

  3. 底层优化:头尾节点松弛更新,减少CAS竞争,提升并发性能。

  4. 适用场景:超高并发无阻塞消息流转、异步任务排队。

  5. 坑点:size()时间复杂度O(n),高并发下不准,推荐isEmpty()判断空。

10.6.2 ConcurrentLinkedDeque(无锁双向队列)

  1. 双向链表结构,支持头尾双向增删,CAS无锁。

  2. 适合双端进出、高并发消息插队、头尾消费场景。

10.7 同步包装类(不推荐使用)

10.7.1 Collections.synchronizedxxx 原理

  1. 底层使用对象悲观锁,全部方法串行互斥,读写全部阻塞。

  2. 锁粒度极大、并发吞吐量极低、性能差。

  3. 迭代器非线程安全,遍历需手动加锁,否则抛出并发修改异常。

10.7.2 生产禁用场景

高并发业务禁止使用synchronizedMap、synchronizedList,一律替换为JUC原生并发容器。

10.8 JUC并发容器终极选型表(生产直接抄)

容器名称

底层结构

锁机制

适用生产场景

CopyOnWriteArrayList

动态数组

写锁读无锁

读多写少、静态配置列表

ConcurrentHashMap

数组+链表+红黑树

CAS+ synchronized

通用高并发键值存储

ConcurrentSkipListMap

跳表

CAS无锁

高并发有序排序业务

ConcurrentLinkedQueue

单向链表

CAS无锁

超高并发无阻塞消息队列

Synchronized包装类

原集合封装

全局悲观锁

低并发、临时过渡场景

10.9 面试满分总结(必背10句)

  1. JUC并发容器专为高并发设计,优于同步包装类,锁粒度更细;

  2. CopyOnWrite采用写时复制,读无锁,仅适合读多写少;

  3. CopyOnWriteArraySet底层依赖List,查重效率低,少量元素使用;

  4. ConcurrentHashMap1.7分段锁,1.8桶位锁+红黑树;

  5. 1.8废弃ReentrantLock,改用synchronized,低竞争性能更优;

  6. 跳表系列容器天然有序,无锁实现,适合排序业务;

  7. ConcurrentLinkedQueue无锁高性能,size()不准优先isEmpty;

  8. 同步包装类全局加锁,并发吞吐量极低,生产禁用;

  9. 所有JUC容器均不允许存储null,规避空指针歧义;

  10. 高并发优先无锁CAS容器,低并发简单业务可适度使用锁容器。


第十一部分 ThreadLocal 线程本地存储

11.1 作用

线程私有变量,线程间数据隔离

11.2 底层结构

底层结构:每个Thread线程内部持有独立ThreadLocalMap,并非ThreadLocal维护数据,彻底实现线程隔离;ThreadLocalMap为自定义简易哈希表,无链表、仅数组结构,核心细节如下:

1、存储结构:底层Entry数组,Entry是ThreadLocalMap静态内部类,key为ThreadLocal对象(弱引用WeakReference),value为线程绑定的业务数据(强引用);

2、哈希寻址:采用简单哈希算法:hashCode & (table.length - 1) 定位数组下标;

3、哈希冲突:无链表、无红黑树,仅采用线性探测法向后寻址,空位存放冲突元素;

4、初始容量:默认初始化容量16,负载因子固定为2/3,数组元素达到阈值触发扩容,扩容为原容量2倍;

5、引用设计:key弱引用、value强引用,该设计是内存泄漏的核心诱因;

6、归属关系:数据归属于当前线程,其他线程无法访问,线程销毁后,当前线程内部ThreadLocalMap同步销毁。

11.3 内存泄漏根源

内存泄漏根源(面试必考、逐句背诵):核心成因是Key弱引用、Value强引用的引用搭配设计缺陷。

1、引用结构:ThreadLocalMap内部Entry实体,key为ThreadLocal对象(弱引用),value为业务存储数据(强引用);

2、回收逻辑:当外部无强引用指向ThreadLocal对象时,GC会自动回收弱引用的key,导致Entry中key变为null;

3、滞留问题:被回收key对应的value仍是强引用,无法被GC回收;

4、泄漏闭环:当前线程长期存活(如线程池核心线程),失效的null-key条目持续堆积在ThreadLocalMap中,大量无效value常驻堆内存,造成内存泄漏;

5、拓展危害:若线程池线程复用,旧脏数据残留,会引发业务数据串值、脏数据Bug。

直白总结:key被GC回收、value没人删、线程不销毁、数据一直堆,最终内存泄漏。

11.4 解决方案

核心解决方案:使用完毕,强制调用 remove() 清除数据,以下为完整、可背诵、生产落地的全套解决方案,包含基础规范、底层清除、避坑写法、进阶优化:

1、基础强制规范(必做):ThreadLocal 使用完毕后,必须在 finally 代码块中执行 remove(),手动清空当前线程绑定的value数据,断开强引用,让GC正常回收资源,从根源杜绝内存泄漏;禁止使用完放任不管。

2、try-finally 标准写法(生产模板):所有ThreadLocal赋值逻辑,必须放入try代码块,remove()写入finally,保证无论业务是否异常,都能强制清除数据,防止残留脏数据。

3、避免静态全局滥用:禁止将ThreadLocal定义为static全局常量长期持有,全局ThreadLocal生命周期等同于JVM,极易造成大量线程内存累积泄漏。

4、线程池使用专属规范:线程池线程永久复用,业务执行结束必须清除上下文,防止下一次任务复用线程,读取上一个任务残留脏数据,引发数据串位Bug。

5、主动触发探测清除:ThreadLocalMap底层自带启发式清除机制,扩容、rehash、set赋值时,会主动扫描key为null的脏Entry并清除,但该机制不可靠、触发时机不确定,不能依赖自动清除。 6、进阶替代方案:JDK21+推荐使用ScopedValue作用域值,无内存泄漏、无需手动remove、天然适配线程池;线程池业务透传优先使用阿里TransmittableThreadLocal。

错误禁忌:仅设置null赋值(threadLocal.set(null))无法彻底清除,只是重置value,底层Entry依旧存在,无法解决内存泄漏,必须使用remove()。

11.5 衍生类(全量补全|面试+生产高频)

ThreadLocal 包含三大核心衍生类,分别解决父子线程传值、线程池复用传值、不可变上下文问题,生产开发高频使用,区别及底层原理如下:

简洁:

  1. InheritableThreadLocal:父子线程数据传递

  2. TransmittableThreadLocal:线程池跨线程透传(阿里)

11.5.1 InheritableThreadLocal(JDK原生|父子线程数据透传)

  1. 核心作用:原生ThreadLocal仅当前线程可见,此类支持父线程向子线程自动传递数据,创建子线程时拷贝父线程上下文数据。

  2. 底层原理:重写ThreadLocal的childValue()方法,线程初始化时,JDK主动将父线程ThreadLocalMap数据拷贝至新建子线程Map,属于一次性拷贝。

  3. 适用场景:一次性新建子线程、简单异步任务、主线程向新建子线程透传用户信息。

  4. 致命缺陷不支持线程池复用。线程池线程长期存活,线程复用后不会重新拷贝父线程数据,出现上下文旧数据残留、串值问题。

/**
 * InheritableThreadLocal 父子线程传值示例
 */
public class InheritThreadLocalDemo {
    private static final InheritableThreadLocal<String> USER_INFO = new InheritableThreadLocal<>();

    public static void main(String[] args) {
        // 父线程赋值
        USER_INFO.set("用户ID:10001");
        // 新建子线程,自动继承父线程数据
        new Thread(() -> System.out.println("子线程获取:" + USER_INFO.get())).start();
        // 输出:子线程获取:用户ID:10001
    }
}

11.5.2 TransmittableThreadLocal(阿里开源|线程池专用透传)

  1. 核心作用:解决InheritableThreadLocal线程池复用串值痛点,完美适配线程池,实现异步线程上下文透传,是企业生产首选。

  2. 底层原理: 修饰线程池、任务包装,拦截线程池提交任务;

  3. 任务提交时,捕获主线程上下文快照;

  4. 任务执行前,将快照赋值给复用线程;

  5. 任务执行完毕,还原旧上下文,彻底杜绝脏数据残留。

  6. 依赖引入:非JDK原生,需引入maven依赖com.alibaba:transmittable-thread-local

  7. 生产场景:Spring异步注解、线程池异步任务、链路追踪TraceId、登录用户上下文透传(互联网公司标配)。

11.5.3 ScopedValue(JDK21+|新一代官方替代方案)

  1. 核心定位:JDK21推出,官方换代工具,彻底替代ThreadLocal,解决内存泄漏、手动remove痛点。

  2. 核心优势: 作用域绑定,代码块执行完毕自动销毁数据,无内存泄漏;

  3. 无需手动remove,语法简洁安全;

  4. 天然适配虚拟线程、结构化并发;

  5. 不可变设计,线程安全、禁止篡改。

  6. 适用场景:高版本JDK新项目、虚拟线程业务、轻量化上下文透传。

11.5.4 三大衍生类终极对比(面试必背)

衍生类

底层能力

优缺点

生产选型

ThreadLocal

单线程数据隔离

原生轻量、存在内存泄漏

简单单线程业务,用完必删

InheritableThreadLocal

父子线程一次性传值

不支持线程池、复用串值

临时新建线程、简单异步

TransmittableThreadLocal

线程池复用精准透传

第三方依赖、稳定成熟

主流线上业务、线程池异步(推荐)

ScopedValue

作用域自动回收、无泄漏

高版本JDK专属、无泄漏

JDK21+新项目、虚拟线程

11.5.5 面试满分总结(6句背诵)

  1. 原生ThreadLocal仅单线程隔离,存在内存泄漏,必须手动remove;

  2. InheritableThreadLocal支持父子线程传值,禁止用于线程池;

  3. TransmittableThreadLocal阿里开源,适配线程池,解决复用串值;

  4. TTL底层靠任务包装+上下文快照,实现线程复用数据隔离;

  5. ScopedValue是JDK21官方替代,无泄漏、无需手动清除;

  6. 生产线程池异步透传,优先选用TransmittableThreadLocal。

11.6 实战场景

ThreadLocal是生产开发高频工具,核心用途为线程上下文数据隔离、无参透传、避免重复创建消耗,以下整理互联网企业真实落地实战场景,附带标准业务代码、开发规范、避坑要点,全部可直接复用。

11.6.1 用户登录上下文透传(最常用)

业务场景:Web接口请求链路,拦截器解析Token获取用户信息,存入ThreadLocal,全局Controller、Service、Dao无需传参,随时获取登录用户,避免参数透传冗余。

核心优势:解耦用户参数,整条请求链路共享用户数据,不污染业务方法入参。

/**
 * 登录用户上下文工具类(生产标准模板)
 * 全局静态ThreadLocal,存储当前线程登录用户信息
 */
public class UserContext {
    // 私有静态ThreadLocal,存储用户信息实体
    private static final ThreadLocal<UserInfo> USER_THREAD_LOCAL = new ThreadLocal<>();

    // 设置用户信息
    public static void setUser(UserInfo userInfo){
        USER_THREAD_LOCAL.set(userInfo);
    }

    // 获取当前登录用户
    public static UserInfo getUser(){
        return USER_THREAD_LOCAL.get();
    }

    // 获取用户ID(高频简化方法)
    public static Long getUserId(){
        UserInfo userInfo = USER_THREAD_LOCAL.get();
        return userInfo == null ? null : userInfo.getUserId();
    }

    // 【强制】请求结束手动清除,防止内存泄漏+线程复用串值
    public static void remove(){
        USER_THREAD_LOCAL.remove();
    }
}

// 用户信息实体
class UserInfo{
    private Long userId;
    private String username;
    // getter、setter省略
}

拦截器使用规范:请求进入拦截器解析token、存入上下文;finally中强制remove,保证请求结束资源清空。

11.6.2 链路追踪ID透传

业务场景:微服务项目,生成全局唯一TraceId,存入ThreadLocal,日志打印、异常报错、接口调用全程携带链路ID,线上快速排查整条请求链路日志。

生产规范:MDC日志框架底层基于ThreadLocal实现,无需手动维护,原理一致。

// 简易链路追踪工具
public class TraceContext {
    private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();

    // 生成并设置链路ID
    public static void generateTraceId(){
        TRACE_ID.set(UUID.randomUUID().toString().replace("-",""));
    }

    // 获取链路ID
    public static String getTraceId(){
        return TRACE_ID.get();
    }

    // 清除链路ID
    public static void clear(){
        TRACE_ID.remove();
    }
}

11.6.3 线程私有工具类(避免全局共享并发问题)

业务场景:非线程安全工具类,全局共享会引发并发异常,使用ThreadLocal为每个线程单独创建实例,既保证线程安全,又避免频繁创建对象消耗性能。

经典案例:SimpleDateFormat线程不安全,使用ThreadLocal封装,替代全局静态实例。

/**
 * 线程安全时间格式化工具
 * 禁止全局static SimpleDateFormat,改用ThreadLocal隔离实例
 */
public class DateUtil {
    private static final ThreadLocal<SimpleDateFormat> DATE_FORMAT = ThreadLocal.withInitial(() ->
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
    );

    public static String format(Date date){
        return DATE_FORMAT.get().format(date);
    }
}

补充优化:JDK8+优先使用DateTimeFormatter(天生线程安全),无需ThreadLocal封装。

11.6.4 数据库会话/事务上下文绑定

业务场景:单线程事务链路,绑定当前线程数据库Connection连接,保证同一事务内所有数据库操作使用同一个连接,满足事务原子性。

底层原理:Spring事务、Mybatis SqlSession底层依靠ThreadLocal绑定连接,实现单线程事务隔离,互不干扰。

11.6.5 动态数据源切换(多数据源项目)

业务场景:项目配置多数据源(主库、从库、业务库),根据业务标识动态切换数据源,使用ThreadLocal存储当前线程数据源标记,拦截器根据标记切换连接。

// 动态数据源上下文
public class DataSourceContext {
    // 存储当前线程数据源标识
    private static final ThreadLocal<String> DATA_SOURCE_KEY = new ThreadLocal<>();

    // 设置数据源
    public static void setDataSource(String key){
        DATA_SOURCE_KEY.set(key);
    }

    // 获取数据源
    public static String getDataSource(){
        return DATA_SOURCE_KEY.get();
    }

    // 清除数据源
    public static void clear(){
        DATA_SOURCE_KEY.remove();
    }
}

11.6.6 本地临时缓存(线程内复用数据)

业务场景:单线程多次重复查询同一数据(如字典、配置),存入ThreadLocal做临时缓存,减少重复数据库、Redis查询,提升接口响应速度。

限制:仅当前线程生效,请求结束自动清除,不做全局持久化缓存。

11.6.7 实战开发硬性规范(生产必遵守)

  1. 必须手动清除:所有ThreadLocal使用完毕,finally块执行remove(),杜绝内存泄漏、线程串值;

  2. 禁止全局static滥用:非通用上下文工具,不要定义为静态常量,缩短生命周期;

  3. 线程池严格管控:异步任务、线程池任务,执行前后必须清空上下文;

  4. 存储轻量数据:仅存用户ID、标识、配置等轻量数据,禁止存储大对象;

  5. 禁止业务赋值null:set(null)不会清除Entry,必须使用remove()。

11.6.8 面试高频追问总结

  1. Q:为什么用户上下文一定要用ThreadLocal? A:实现线程隔离,整条请求无参透传,解耦业务代码,避免方法层层传参。

  2. Q:线程池使用ThreadLocal最大隐患? A:线程复用导致旧数据残留、上下文串值,必须手动remove清除。

  3. Q:MDC日志底层原理? A:基于ThreadLocal实现,单线程日志链路隔离,打印唯一TraceId。


第十二部分 死锁、活锁、饥饿

12.1 死锁四大必要条件

死锁是两个及以上线程互相持有对方所需资源,且互相永久等待、无法主动释放资源的阻塞状态,四大必要条件缺一不可,全部满足才会产生死锁,详细解析如下:

  1. 互斥条件:临界资源同一时刻仅能被一个线程占用,不可并行持有;已被占用的资源,其他线程必须阻塞等待。

  2. 请求且保持条件:线程已经持有部分锁/资源,在不释放已有资源的前提下,继续申请其他被占用的资源,不会主动释放已持有资源。

  3. 不可剥夺条件:线程已获取的资源,无法被其他线程强行抢占、回收;只能由持有线程主动执行完毕后,手动释放资源。

  4. 循环等待条件:多个线程形成闭环资源依赖,线程A持有资源1等待资源2,线程B持有资源2等待资源1,互相循环等待,无限僵持。

面试满分总结:互斥占用、持有请求、不可抢占、循环等待;破坏任意一条,即可杜绝死锁。

12.2 排查工具

Java 线程死锁、线程卡顿、CPU飙高排查全部依赖JDK自带命令行工具+可视化工具,无需额外部署,线上生产环境高频使用,重点整理jstack、jconsole、jvisualvm三大核心排查工具,附带实操命令、排查步骤、使用场景、面试考点,适配死锁、活锁、线程阻塞排查。

12.2.1 jstack(线上最常用|命令行排查)

核心定位:JDK自带命令行工具,无图形界面,轻量化、不占用额外内存,生产线上首选排查工具,专门抓取线程堆栈、定位死锁、线程阻塞、死循环。

  1. 常用实操命令jstack -l 进程PID > thread.log,导出线程堆栈日志,本地分析;

  2. 排查死锁标识:日志末尾搜索关键字Found one Java-level deadlock,出现该标识判定存在死锁;

  3. 可排查问题:死锁、线程阻塞、锁等待、线程死循环、休眠线程、线程池积压;

  4. 线程状态标识:排查重点关注BLOCKED(阻塞抢锁)、WAITING(无限等待)、TIMED_WAITING(限时等待)线程;

  5. 优缺点:优点:轻量化、不卡顿线上服务、无需停机;缺点:纯文本日志,需人工分析堆栈。

12.2.2 jconsole(简易可视化|轻量监控)

核心定位:JDK自带图形化监控工具,无需安装,操作简单,适合开发、测试环境快速可视化排查,内置线程监控、内存监控、MBean监控。

  1. 启动方式:cmd/终端直接输入 jconsole,选择运行中的Java进程一键连接;

  2. 核心功能:实时查看线程数量、线程状态、线程堆栈、检测死锁、监控堆内存使用;

  3. 死锁检测:内置死锁检测按钮,一键自动扫描线程循环依赖,直观展示死锁线程、锁对象;

  4. 优缺点:优点:可视化、零代码、上手简单;缺点:占用少量资源,禁止高并发生产环境使用。

12.2.3 jvisualvm(全能可视化|专业排查)

核心定位:JDK官方全能可视化排查工具,功能最全,集线程监控、内存分析、GC分析、性能采样、堆dump分析于一体,是Java并发故障终极排查工具。

  1. 启动方式:终端输入 jvisualvm,自动识别本地Java进程;

  2. 线程排查能力:实时监控所有线程运行状态、查看线程堆栈、一键dump线程快照、精准定位死锁、锁竞争、线程卡顿;

  3. 扩展能力:安装Visual GC插件,可视化查看GC回收过程,排查并发下频繁GC、内存抖动;

  4. dump分析:支持堆快照、线程快照保存,离线分析线上疑难故障;

  5. 优缺点:优点:功能齐全、可视化极强、适合深度排查;缺点:占用资源偏高,生产环境建议采样快照后离线分析。

12.2.4 补充工具(线上进阶排查)

  1. jmap:导出堆内存快照,排查死锁引发的内存堆积、对象溢出;

  2. jhat:解析堆dump文件,分析大对象、锁对象内存占用;

  3. arthas(阿里开源):线上神器,无侵入排查线程、锁、方法耗时,生产高并发故障首选,替代部分JDK原生工具。

12.2.5 线上排查流程(面试+生产标准流程)

  1. 第一步:top命令查看服务器CPU,定位高占用Java进程PID;

  2. 第二步:jstack导出线程堆栈,搜索deadlock判定是否死锁;

  3. 第三步:筛选BLOCKED、WAITING阻塞线程,查看锁依赖关系;

  4. 第四步:结合代码定位嵌套锁、锁顺序错乱问题;

  5. 第五步:重启服务临时恢复,优化代码规避死锁。

12.2.6 面试满分总结(6句背诵)

  1. 排查工具分为命令行、可视化两类,生产优先轻量化命令;

  2. jstack线上首选,导出堆栈,搜索deadlock判定死锁;

  3. jconsole简易可视化,适合测试环境快速检测死锁;

  4. jvisualvm功能最全,做线程、内存、GC深度分析;

  5. 线上禁止可视化工具常驻,优先快照+离线分析;

  6. 进阶排查使用Arthas,无侵入线上实时调试。

12.3 死锁详细解决手段(生产落地+代码实操)

1.顺序统一获取锁(最简单、最推荐) 核心原理:规定所有线程严格按照固定顺序申请多级锁,破坏循环等待条件,从根源杜绝死锁。所有线程无论执行逻辑,必须先申请序号小的锁、再申请序号大的锁,不会形成闭环等待。 适用场景:存在嵌套锁、多把锁依赖的业务,如账户转账、多资源加锁。 正反示例对比 错误写法(乱序加锁,必死锁):

// 线程A:先锁A账户、再锁B账户
new Thread(()->{synchronized(a){synchronized(b){}}}).start();
// 线程B:先锁B账户、再锁A账户(循环等待,触发死锁)
new Thread(()->{synchronized(b){synchronized(a){}}}).start();正确写法(统一顺序,彻底防死锁):// 所有线程统一顺序:先锁a、后锁b,无循环依赖
new Thread(()->{synchronized(a){synchronized(b){}}}).start();
new Thread(()->{synchronized(a){synchronized(b){}}}).start();

        生产规范:给所有业务锁定义唯一编号,加锁严格遵循编号从小到大,禁止随意嵌套加锁。

2.限时锁尝试(主动放弃、防永久阻塞) 核心原理:使用显式锁ReentrantLock的tryLock(time)限时获取锁,破坏请求且保持条件;规定时间内未获取到锁,主动放弃当前锁、释放已持有资源,避免无限僵持。 适配场景:第三方接口调用、超时敏感业务、不确定锁竞争时长的场景。

生产标准代码

public class TryLockDemo {
    private static final ReentrantLock lockA = new ReentrantLock();
    private static final ReentrantLock lockB = new ReentrantLock();

    public static void transfer() {
        // 限时3秒尝试获取锁,获取失败直接放弃
        if (lockA.tryLock(3, TimeUnit.SECONDS)) {
            try {
                if (lockB.tryLock(3, TimeUnit.SECONDS)) {
                    // 执行业务转账逻辑
                }
            } finally {
                // 逐层释放锁
                if(lockB.isHeldByCurrentThread()) lockB.unlock();
                if(lockA.isHeldByCurrentThread()) lockA.unlock();
            }
        } else {
            // 获取锁失败,重试或降级返回
            System.out.println("获取锁超时,业务降级处理");
        }
    }
}

优点:不会永久阻塞,可控性极强;

缺点:需手动处理超时降级逻辑。

3.统一锁层级、简化锁依赖 核心原理:梳理业务锁层级,区分主锁、副锁,禁止同级锁互相嵌套;将分散的多把锁合并为统一层级锁,减少锁之间的依赖关系,从架构层面规避死锁。

实操方案

       (1)、业务拆分:把嵌套锁逻辑拆分为串行执行,降低锁嵌套深度;

       (2)、锁合并:多个关联小锁合并为一把全局业务锁,减少锁竞争维度;

       (3)、层级划分:上层业务锁、下层数据锁,禁止下层反向嵌套上层锁。

适用场景:复杂订单流程、多级业务嵌套、大量锁依赖的老旧代码。

4.减少嵌套锁、最小化锁粒度

核心原理:遵循能不嵌套就不嵌套、能少锁就少锁原则,破坏请求且保持条件;缩小锁范围,仅锁住核心临界资源,无关代码剔除锁范围。

优化手段

       (1)、禁止无意义嵌套锁,同步代码块内部不再加其他锁;

       (2)、优先使用同步代码块,替代大范围同步方法;

       (3)、锁内禁止耗时IO、远程调用、sleep休眠,缩短锁持有时间;

避坑重点:锁持有时间越长,竞争概率越高,死锁风险成倍增加。

5.额外补充3种企业高级解决方案(面试加分)

死锁检测工具监控:线上结合jstack、Arthas定时采集线程堆栈,监控死锁关键字,触发告警及时通知开发;

6.加锁重试机制:获取锁失败后,短暂休眠随机时间再重试,避免活锁、频繁抢占;

7.读写锁分离:高并发读写场景,用ReentrantReadWriteLock替代独占锁,降低锁竞争烈度,减少死锁概率。

12.4 死锁、活锁、饥饿 三者详解+区别对比(面试必考)

1.死锁(DeadLock):互相永久等待

定义:两个及以上线程互相持有对方所需锁资源,形成循环依赖,无外力干预下永久阻塞,线程状态为BLOCKED/WAITING。

产生条件:必须同时满足死锁四大必要条件。

现象特征:线程卡死、CPU占用低、服务无报错、业务永久停滞。

典型案例:双向嵌套锁、无序多锁抢占。

2.活锁(LiveLock):一直尝试、一直失败

定义:线程无阻塞挂起,一直主动尝试获取资源,但因竞争策略互相谦让,始终无法执行业务,无限空转。

产生原因:线程获取锁失败后立刻释放资源,其他线程同步谦让,循环往复谁都无法执行。

现象特征:线程一直运行、CPU占用偏高、无阻塞堆栈、业务无进展。

解决方案:添加随机休眠时间,打破同步谦让节奏,错开抢占时机。

通俗举例:两人过独木桥,互相主动退让,一直左右避让永远无法通行。

3.饥饿(Starvation):长期抢不到资源

定义:某条线程优先级过低、或一直被高优先级线程抢占资源,长期无法获取锁,迟迟不能执行。 产生原因

        (1)、非公平锁大量高并发抢占,弱势线程持续抢锁失败;

        (2)、线程优先级设置过低,JVM调度优先级靠后;

         (3)、锁竞争激烈,大量线程排队,尾部线程长期轮空。

现象特征:线程无阻塞、无报错,长期处于就绪态,极少执行。

解决方案:使用公平锁、统一线程优先级、控制并发竞争量。

12.4.1 三者满分对比总结(背诵版)

类型

线程状态

CPU占用

核心特征

最优解决方案

死锁

阻塞等待

极低

互相持有、永久僵持

统一加锁顺序

活锁

持续运行

偏高

不断重试、无法执行

随机休眠错开竞争

饥饿

就绪轮转

正常

长期抢不到资源

改用公平锁


第十三部分 JDK 高版本并发新特性

13.1 Java21 虚拟线程 Virtual Thread(并发革命性升级|面试爆款)

虚拟线程是 JDK21 正式GA、JDK19预览 推出的轻量级线程,由JVM管理、而非操作系统内核管理,是Java并发编程史上颠覆性优化,彻底解决传统平台线程(内核线程)重量大、创建受限、阻塞开销高的痛点,官方定位:替代平台线程、消灭线程池、简化异步编码

简洁:

  1. 轻量级用户线程,开销极小,可海量创建。

  2. 告别线程池数量限制。

  3. 底层依托载体线程调度。

13.1.1 核心概念区分(面试必背)

  1. 平台线程(Platform Thread):传统原生线程,1:1映射操作系统内核线程,线程栈固定大小、内存开销大、创建数量上限极低(单机几千条),阻塞时内核态挂起、开销极高,我们以往使用的Thread、线程池线程均为平台线程。

  2. 虚拟线程(Virtual Thread):JVM用户态轻量级线程,不直接绑定内核线程,栈内存动态伸缩、极小内存占用,单机可轻松创建百万、千万级线程,阻塞不会挂起内核线程,无昂贵上下文切换。

  3. 载体线程(Carrier Thread):JVM内部复用少量平台线程作为载体,负责调度执行虚拟线程,虚拟线程阻塞时,载体线程不会阻塞,转而执行其他就绪虚拟线程,最大化利用CPU。

13.1.2 底层实现原理

  1. 映射模型:采用 M:N 映射,多条虚拟线程复用少量载体平台线程,区别于传统线程1:1内核映射;

  2. 栈内存优化:虚拟线程无固定栈大小,初始仅几百字节,栈内存随业务动态扩容、缩容,闲置时自动释放内存;平台线程默认栈内存1MB,内存占用差距巨大;

  3. 阻塞优化机制:虚拟线程遇到阻塞操作(sleep、锁等待、IO请求)时,不会阻塞载体线程,JVM将虚拟线程挂起保存上下文,载体线程调度其他就绪虚拟线程执行,彻底消除阻塞空转浪费;

  4. 调度方式:JVM自主调度,无需操作系统内核介入,用户态完成线程切换,上下文切换开销几乎可以忽略。

13.1.3 核心特性(满分总结)

  1. 海量创建无上限:单机支持百万级虚拟线程,无需手动限制线程数量,告别线程池核心参数调优烦恼;

  2. 极低内存开销:单条虚拟线程内存占用KB级别,对比平台线程1MB栈内存,内存压缩近千倍;

  3. 阻塞零性能损耗:IO阻塞、sleep等待不占用载体线程,不会造成内核态阻塞,适配大量IO密集型业务;

  4. 语法完全兼容:虚拟线程继承Thread类,原有线程API全部可用,无需修改老旧业务代码,无缝迁移;

  5. 无需手动线程池:官方明确:虚拟线程不需要池化、不建议复用,用完即销毁,简化编码模型;

  6. 天生守护线程:默认守护线程,JVM退出自动回收,无需手动管控生命周期。

13.1.4 标准代码示例(生产最简写法)

import java.util.concurrent.Executors;

/**
 * Java21 虚拟线程标准使用示例
 * 无需手动new Thread、无需线程池、极简编码
 */
public class VirtualThreadDemo {
    public static void main(String[] args) {
        // 1、批量创建虚拟线程(一键生成百万级线程)
        try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
            for (int i = 1; i <= 10000; i++) {
                int taskId = i;
                // 提交任务,每条任务分配一条独立虚拟线程
                executor.submit(() -> {
                    System.out.println("虚拟线程执行任务:" + taskId);
                    // IO阻塞、sleep不会占用载体线程,无性能损耗
                    Thread.sleep(500);
                });
            }
        }
        // try-with-resources自动关闭执行器,等待所有虚拟线程执行完毕
        System.out.println("全部任务执行完成");
    }
}

13.1.5 虚拟线程 VS 平台线程(高频对比面试题)

对比维度

平台线程(传统线程)

虚拟线程(Virtual Thread)

线程映射模型

1:1 绑定内核线程

M:N 复用载体线程

栈内存大小

固定1MB,开销大

动态伸缩,KB级别

创建数量上限

单机几千条,受限严重

百万+,无硬性上限

阻塞开销

阻塞内核线程,开销极高

不阻塞载体线程,无损耗

使用方式

必须线程池池化复用

用完即销毁,无需池化

适用场景

CPU密集型、少量并发

IO密集型、海量并发

13.1.6 生产适用&禁用场景

✅ 适用场景(核心落地业务)

  1. 海量IO密集型接口:HTTP请求、数据库查询、Redis调用、第三方接口调用,阻塞无损耗;

  2. 批量异步任务:批量文件处理、批量数据同步、定时任务批量执行;

  3. 网关高吞吐服务:网关转发、流量透传、短连接高频请求;

  4. 简单异步编码场景:告别线程池参数配置,简化异步业务开发。

❌ 不适用场景(避坑重点)

  1. 纯CPU密集型任务:大量计算、加密解密、大数据运算,虚拟线程调度开销大于平台线程;

  2. 本地锁自旋任务:CAS自旋、死循环运算,无法触发虚拟线程挂起优化;

  3. 依赖线程池隔离的业务:核心线程、非核心线程隔离的老旧复杂业务。

13.1.7 高频易错坑点(面试冷门考点)

  1. 禁止池化复用虚拟线程:虚拟线程设计初衷为用完即销毁,手动缓存复用会破坏JVM调度机制,性能倒退;

  2. 不绑定操作系统内核:无法通过top、jstack精准定位虚拟线程,底层无内核线程映射;

  3. 原生不支持定时调度:暂无定时虚拟线程,延时任务仍需依托平台线程;

  4. ThreadLocal仍存在内存泄漏:虚拟线程生命周期短,虽泄漏概率极低,但仍需遵循remove规范;

  5. 虚拟线程不可修改优先级:优先级固定,无抢占式自定义调度能力。

13.1.8 面试满分8句总结(直接背诵)

  1. 虚拟线程是JDK21正式推出的轻量级线程,JVM管理、非内核线程;

  2. 采用M:N映射,复用少量载体线程调度海量虚拟线程;

  3. 栈内存动态伸缩、开销极低,单机支持百万级线程;

  4. IO阻塞不占用载体线程,完美适配IO密集型业务;

  5. 语法兼容传统Thread,无需修改原有线程业务代码;

  6. 禁止池化复用,用完即销毁,彻底淘汰传统线程池;

  7. CPU密集型业务不推荐使用,调度无优势;

  8. 未来版本逐步替代平台线程,是Java并发长期演进方向。

13.2 结构化并发 StructuredTaskScope

StructuredTaskScope 是JDK21正式推出、JDK19预览的结构化并发工具,专门配合虚拟线程使用,用来统一管理一组子任务线程生命周期,解决传统异步编码线程泄露、异常散乱、任务不可控的痛点,是Java并发编码模型的重大革新,官方定义:让子线程生命周期受控于父线程,实现并发任务结构化管理

简洁:

  1. 父子线程生命周期绑定,父存子存、父亡子亡。

  2. 自动异常传播,单一任务失败批量取消。

  3. 极简API,替代CompletableFuture复杂回调。

13.2.1 诞生背景(传统并发痛点)

  1. 线程泄露严重:传统线程/线程池异步任务,父线程结束后,子线程仍在后台无意义运行,无法统一回收;

  2. 异常分散杂乱:多异步任务执行,单个任务异常无法统一捕获,异常散乱、排查困难;

  3. 回调地狱:CompletableFuture多任务嵌套编排,代码层级臃肿,可读性极差;

  4. 资源无法联动释放:业务中途报错,已发起的异步任务无法主动取消,造成资源空耗。

13.2.2 核心核心特性(面试必背)

  1. 生命周期结构化绑定:所有通过当前Scope创建的子线程,生命周期归属于父线程,父线程等待所有子任务执行完毕才会结束,杜绝线程泄露;

  2. 失败自动传播取消:自定义失败策略,单个子任务异常,自动取消其他未完成任务,快速失败、节省资源;

  3. 异常聚合统一处理:收集所有子任务异常,统一抛出、统一捕获,规避异常散乱问题;

  4. 天然适配虚拟线程:配合虚拟线程海量创建、低开销特性,实现高并发简洁编码;

  5. 无锁、无线程池冗余配置:极简编码,无需手动配置线程池参数,轻量化管理任务。

13.2.3 三大内置策略(核心API)

JDK内置三种任务管控策略,适配不同业务场景,生产按需选用:

  1. ShutdownOnFailure(失败即关闭|最常用):任意子任务抛出异常,立刻取消所有正在执行的任务,父线程终止,快速失败,适用于核心任务、强依赖业务;

  2. ShutdownOnSuccess(成功即关闭):任意子任务执行成功,立刻取消其他未完成任务,适用于多候选任务、只要一个结果的业务(多渠道查询、重试兜底);

  3. IgnoreErrors(忽略异常):不主动取消任务,等待所有子任务执行完毕,不管任务成功失败,适用于非核心批量任务、日志采集、数据统计。

13.2.4 标准生产代码示例(三种策略全覆盖)

① ShutdownOnFailure 失败即终止(核心业务)
import java.util.concurrent.StructuredTaskScope;

/**
 * 结构化并发:失败即关闭策略
 * 任意任务异常,全部任务终止,快速失败
 */
public class StructuredFailureDemo {
    public static void main(String[] args) throws Exception {
        // try-with-resources自动关闭scope,回收所有子线程
        try (StructuredTaskScope<String> scope = new StructuredTaskScope.ShutdownOnFailure()) {
            // 提交两个异步子任务(默认创建虚拟线程)
            StructuredTaskScope.Subtask<String> task1 = scope.fork(() -> {
                Thread.sleep(1000);
                return "用户信息查询成功";
            });
            StructuredTaskScope.Subtask<String> task2 = scope.fork(() -> {
                Thread.sleep(500);
                // 模拟任务异常
                throw new RuntimeException("订单查询异常");
            });

            // 等待所有任务完成,任意失败则抛出异常
            scope.join().throwIfFailed();
            // 获取结果(无异常才会执行)
            System.out.println(task1.get());
        }
    }
}

② ShutdownOnSuccess 成功即终止(择优任务)
import java.util.concurrent.StructuredTaskScope;

/**
 * 结构化并发:成功即关闭策略
 * 任意一个任务成功,终止其他任务,节省资源
 * 场景:多渠道查询商品、多接口重试兜底
 */
public class StructuredSuccessDemo {
    public static void main(String[] args) throws Exception {
        try (StructuredTaskScope<String> scope = new StructuredTaskScope.ShutdownOnSuccess<>()) {
            // 并行调用三个数据源接口
            scope.fork(() -> {Thread.sleep(800); return "数据库查询结果";});
            scope.fork(() -> {Thread.sleep(300); return "Redis缓存查询结果";});
            scope.fork(() -> {Thread.sleep(1000); return "第三方接口查询结果";});

            // 获取最先执行成功的任务结果,其余任务自动取消
            String result = scope.join().result();
            System.out.println("最优查询结果:" + result);
        }
    }
}

③ IgnoreErrors 忽略异常(批量非核心任务)
import java.util.concurrent.StructuredTaskScope;

/**
 * 结构化并发:忽略异常策略
 * 等待所有任务执行完毕,不主动取消,异常不阻断主流程
 * 场景:日志上报、数据埋点、批量异步统计
 */
public class StructuredIgnoreDemo {
    public static void main(String[] args) throws Exception {
        try (StructuredTaskScope<Void> scope = new StructuredTaskScope.IgnoreErrors<>()) {
            scope.fork(() -> {System.out.println("日志上报完成"); return null;});
            scope.fork(() -> {throw new RuntimeException("埋点数据上报失败");});
            // 等待全部任务执行,异常不中断流程
            scope.join();
            System.out.println("批量异步任务执行结束,忽略个别异常");
        }
    }
}

13.2.5 对比 CompletableFuture(面试高频对比)

对比维度

CompletableFuture

StructuredTaskScope

线程生命周期

线程独立,父线程无法管控子线程,易泄露

父子绑定,父线程统一管控,无泄露

异常处理

异常分散,需单独捕获,无法批量终止

异常聚合,支持快速失败、批量取消

代码可读性

链式嵌套,多任务易产生回调地狱

平铺编码,逻辑清晰,无嵌套冗余

适配线程

适配平台线程,虚拟线程适配性差

原生适配虚拟线程,性能极致

资源回收

手动管控,依赖线程池销毁规则

try-with-resources自动回收,无资源残留

13.2.6 生产适用场景

  1. 多接口并行查询:聚合查询用户、订单、支付数据,任意接口失败整体终止;

  2. 多渠道择优请求:同时请求缓存、数据库、第三方接口,取最快返回结果;

  3. 批量非核心异步任务:日志、埋点、数据归档,无需强一致性,忽略个别异常;

  4. 微服务并行调用:微服务多接口聚合,统一管控调用生命周期,减少资源浪费。

13.2.7 面试满分7句总结(直接背诵)

  1. StructuredTaskScope是JDK21结构化并发工具,适配虚拟线程;

  2. 核心思想:父子线程生命周期绑定,结构化管控杜绝线程泄露;

  3. 内置三大策略:失败终止、成功终止、忽略异常;

  4. 支持异常聚合、批量取消任务,资源利用率极高;

  5. 对比CompletableFuture,无回调地狱、代码简洁易维护;

  6. 依靠try-with-resources自动回收,无需手动关闭线程;

  7. 高版本JDK优先替代异步编排,是未来并发编码主流。

13.3 作用域值 ScopedValue

新一代线程上下文传递,替代 ThreadLocal,是 JDK21 正式推出、专为虚拟线程设计的上下文传递组件,官方定位:淘汰ThreadLocal、解决线程上下文污染、适配虚拟线程、实现不可变安全上下文透传,彻底修复ThreadLocal历史遗留痛点,是高版本Java并发上下文传递的最优方案。

13.3.1 诞生背景(为什么舍弃ThreadLocal?)

传统ThreadLocal存在大量无法根治的硬伤,尤其适配虚拟线程后弊端被无限放大,JDK推出ScopedValue针对性解决痛点:

  1. 可修改造成上下文污染:ThreadLocal支持set()重复赋值,多嵌套业务极易篡改上下文数据,引发脏数据;

  2. 生命周期不可控:ThreadLocal绑定线程,线程池复用、虚拟线程频繁创建销毁,易残留旧数据、内存泄漏;

  3. 无父子线程隔离管控:InheritableThreadLocal传递数据不可控,大批量子线程继承上下文引发数据混乱;

  4. 不适配虚拟线程:虚拟线程海量创建,ThreadLocal的Entry哈希表结构极易产生内存碎片、残留垃圾。

13.3.2 ScopedValue 核心核心特性(面试必背)

  1. 不可变上下文:绑定后数据只读,禁止二次修改,从根源杜绝上下文篡改,线程绝对安全;

  2. 作用域生命周期:数据仅在指定代码作用域内生效,代码执行完毕自动销毁,无残留、无内存泄漏;

  3. 天生适配虚拟线程:JDK为虚拟线程量身优化,无哈希表冗余,海量线程下内存开销极低;

  4. 安全父子传递:结合结构化并发,子线程自动继承父线程上下文,且只读不可篡改;

  5. 无手动清除要求:依托作用域自动回收,无需手动remove(),规避人为漏删bug;

  6. 非线程绑定:不永久挂载线程,仅绑定代码执行作用域,执行结束立即释放资源。

13.3.3 核心API 通俗易懂讲解

  1. ScopedValue.get():获取当前作用域绑定的上下文数据;

  2. ScopedValue.where():绑定键值对,开启作用域,声明上下文数据;

  3. where().run():同步执行,作用域内执行业务代码,执行完毕自动销毁上下文;

  4. where().call():异步执行,支持有返回值的业务逻辑;

  5. isBound():判断当前线程是否绑定了指定作用域数据。

13.3.4 标准生产代码示例(用户上下文透传模板)

import java.util.concurrent.StructuredTaskScope;

/**
 * ScopedValue 生产标准示例
 * 替代ThreadLocal实现登录用户上下文透传,只读不可改、自动销毁、无内存泄漏
 * 适配虚拟线程+结构化并发
 */
public class ScopedValueUserDemo {
    // 1、定义全局作用域值(泛型存储上下文对象)
    private static final ScopedValue<UserInfo> USER_SCOPE = ScopedValue.newInstance();

    public static void main(String[] args) throws Exception {
        // 2、绑定上下文,开启作用域
        UserInfo loginUser = new UserInfo(1001L, "张三");
        ScopedValue.where(USER_SCOPE, loginUser).run(() -> {
            // 3、当前作用域内任意位置获取上下文
            System.out.println("主线程获取用户:" + USER_SCOPE.get().getUsername());
            // 4、结合结构化并发,子线程自动继承上下文
            try (StructuredTaskScope<Void> scope = new StructuredTaskScope.IgnoreErrors<>()) {
                scope.fork(() -> {
                    // 子线程无需手动传递,直接获取父线程上下文
                    System.out.println("虚拟子线程获取用户ID:" + USER_SCOPE.get().getUserId());
                    return null;
                });
                scope.join();
            }
        });
        // 作用域结束,上下文自动销毁,此处获取直接报错
        // USER_SCOPE.get();
    }

    // 用户信息实体
    static class UserInfo{
        private Long userId;
        private String username;
        public UserInfo(Long userId, String username) {
            this.userId = userId;
            this.username = username;
        }
        // getter省略
        public Long getUserId() {return userId;}
        public String getUsername() {return username;}
    }
}

13.3.5 ScopedValue VS ThreadLocal(面试高频对比)

对比维度

ThreadLocal

ScopedValue

数据可变性

支持set重复修改,可变

绑定后只读,不可变,安全

生命周期

绑定线程,线程销毁才回收

绑定代码作用域,执行完即销毁

内存泄漏

极易内存泄漏,必须手动remove

自动回收,无泄漏风险

虚拟线程适配

适配性差,残留数据严重

原生适配,专为虚拟线程优化

父子线程传递

需InheritableThreadLocal,不可控

结构化并发自动安全传递

底层结构

哈希表存储,有哈希冲突

栈帧存储,无冗余开销

13.3.6 生产适用场景

  1. Web登录上下文透传:存储登录用户、权限信息,全程只读,禁止业务篡改;

  2. 微服务链路追踪:透传TraceId、SpanId,作用域结束自动清除链路标识;

  3. 多线程批量任务:批量异步任务共享全局配置、业务标识,无需重复传参;

  4. 结构化并发专属场景:配合VirtualThread+StructuredTaskScope做异步上下文传递。

13.3.7 高频易错坑点(面试冷门考点)

  1. 不可二次赋值:同一个作用域内,ScopedValue不可重复绑定数据,直接抛出异常;

  2. 作用域隔离:外层作用域数据,内层作用域可读取,内层修改不影响外层(隔离性);

  3. 非作用域不可获取:脱离绑定的代码块,调用get()直接报错,杜绝空残留;

  4. 不支持null绑定:禁止绑定null数据,从语法层面规避空上下文异常;

  5. 低版本JDK不兼容:JDK21及以上正式支持,无兼容降级方案。

13.3.8 面试满分7句总结(直接背诵)

  1. ScopedValue是JDK21推出的新一代上下文传递工具,替代ThreadLocal;

  2. 核心特性:只读不可变、作用域管控、自动回收、无内存泄漏;

  3. 底层基于栈帧存储,区别于ThreadLocal哈希表,开销极低;

  4. 原生适配虚拟线程与结构化并发,是高版本并发标配;

  5. 无需手动清除资源,代码执行完毕自动销毁上下文;

  6. 禁止重复赋值、禁止绑定null,语法层面保证线程安全;

  7. 企业演进方向:新项目用ScopedValue,老旧项目保留ThreadLocal。



第十四部分 线上高并发实战必学

本章节全部为生产落地硬核实战,剔除空洞理论,全部是线上高频踩坑、企业通用规范、高并发优化手段,适配互联网后端、微服务、分布式项目,所有代码可直接拷贝用于生产,是从面试工程师进阶为资深业务工程师的核心章节。

简洁:

  1. 多线程上下文透传用户信息、链路 ID

  2. 并发安全日期工具:禁用 SimpleDateFormat,使用 DateTimeFormatter

  3. 高并发随机数:ThreadLocalRandom

  4. 多线程大文件分片读取

  5. 多线程事务一致性处理

  6. 异步回调地狱优化

  7. 多线程结合限流、令牌桶、漏桶算法

  8. 线上线程池压测调优


14.1 多线程上下文透传(用户/链路ID)

14.1.1 业务痛点

微服务高并发场景下,异步线程、线程池子线程无法直接获取主线程的登录用户、TraceId、请求头;传统ThreadLocal在线程池复用场景下存在上下文污染、内存泄漏、数据串扰问题。

14.1.2 三种透传方案对比

  1. ThreadLocal:传统方案,适配低并发、无线程池复用场景,线程池极易数据串号;

  2. InheritableThreadLocal:仅支持新建线程传递,线程池复用线程失效,生产基本废弃;

  3. TransmittableThreadLocal(TTL):阿里开源,生产唯一推荐,完美适配线程池,解决复用线程上下文传递、数据隔离。

14.1.3 生产标准代码(TransmittableThreadLocal)

import com.alibaba.ttl.TransmittableThreadLocal;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 高并发上下文透传|生产通用模板
 * 依赖:maven引入 com.alibaba:transmittable-thread-local
 * 适配:线程池、异步任务、微服务链路追踪
 */
public class UserContextUtil {
    // 定义全局上下文容器
    private static final TransmittableThreadLocal<LoginUser> USER_TL = new TransmittableThreadLocal<>();

    // 存上下文
    public static void setUser(LoginUser user){ USER_TL.set(user); }
    // 取上下文
    public static LoginUser getUser(){ return USER_TL.get(); }
    // 强制清除,防止内存泄漏
    public static void clear(){ USER_TL.remove(); }

    public static void main(String[] args) {
        // 固定线程池,模拟线上复用线程
        ExecutorService pool = Executors.newFixedThreadPool(2);
        // 主线程存入用户信息
        setUser(new LoginUser(1001L,"admin"));

        // 异步子线程直接获取主线程上下文,无串号
        pool.execute(() -> System.out.println("子线程获取登录用户:" + getUser().getUsername()));
        pool.shutdown();
        clear();
    }

    // 登录用户实体
    static class LoginUser{
        private Long userId;
        private String username;
        // 构造、getter省略
        public LoginUser(Long userId, String username) {
            this.userId = userId;
            this.username = username;
        }
        public String getUsername() {return username;}
    }
}

14.1.4 高版本JDK最优方案

JDK21+ 直接使用 ScopedValue 替代TTL,无需引入第三方依赖,只读不可变、自动回收、无内存泄漏,前文已给生产模板,新项目优先使用。

14.1.5 线上强制规范

  1. 禁止使用原生ThreadLocal做线程池上下文传递;

  2. 所有异步接口、线程池任务,执行结束必须手动清空上下文;

  3. 链路追踪TraceId统一使用TTL透传,保证日志链路完整。

14.2 并发安全日期时间工具(线上高频BUG点)

14.2.1 致命坑点

SimpleDateFormat 线程极度不安全!底层共享calendar对象,多线程并发格式化时间,必定出现时间错乱、年份偏移、直接报错,是线上最常见低级BUG。

14.2.2 生产推荐方案

  1. JDK8+ DateTimeFormatter:不可变类、线程绝对安全,无锁高并发;

  2. FastDateFormat:旧项目兼容方案,apache工具类,安全高性能;

  3. 禁止:SimpleDateFormat、new Date()格式化拼接。

14.2.3 标准工具类代码(线上直接复用)

import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

/**
 * 线上高并发安全时间工具类
 * 全局静态常量格式化器,避免重复创建对象损耗性能
 */
public class DateUtil {
    // 全局唯一不可变格式化对象,线程安全
    private static final DateTimeFormatter YMD_HMS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    // 时间转字符串
    public static String formatNow(){
        return LocalDateTime.now().format(YMD_HMS);
    }
    // 字符串转时间
    public static LocalDateTime parse(String time){
        return LocalDateTime.parse(time,YMD_HMS);
    }
}

14.2.4 优化细节

  1. DateTimeFormatter定义为全局static常量,禁止方法内重复new;

  2. LocalDateTime无时区,跨时区业务使用ZonedDateTime;

  3. 禁止使用静态SimpleDateFormat,并发必崩。

14.3 高并发随机数(规避伪随机卡顿)

14.3.1 痛点分析

  1. Random:线程不安全,多线程竞争同一种子,CAS失败空转,并发卡顿;

  2. Math.random():底层仍是Random,高并发性能极差;

  3. ThreadLocalRandom:JDK7+,线程隔离种子,无竞争、无锁、超高并发。

14.3.2 生产标准写法

import java.util.concurrent.ThreadLocalRandom;

/**
 * 高并发随机数生成|生产唯一推荐
 * 适用:订单随机码、验证码、抽奖、分布式唯一后缀
 */
public class RandomUtil {
    // 获取指定区间随机整数
    public static int getRandom(int min,int max){
        return ThreadLocalRandom.current().nextInt(min,max+1);
    }
}

14.3.3 线上规范

  1. 所有高并发随机场景,强制使用ThreadLocalRandom;

  2. 禁止循环内频繁创建Random实例,造成种子冲突;

  3. 加密安全随机数使用SecureRandom(低并发、加密场景)。

14.4 多线程大文件分片读取(百万级文件处理)

14.4.1 业务场景

线上日志分析、批量导入、大数据解析、超大CSV/TXT文件,单线程读取速度极慢,IO阻塞严重,采用分片+多线程并行读取提升吞吐量。

14.4.2 核心实现思路

  1. 获取文件总字节大小,自定义分片区间;

  2. 线程池分配每段读取起始位置、结束位置;

  3. RandomAccessFile随机读写,精准定位文件指针;

  4. 并行读取、汇总数据,最后合并结果。

14.4.3 简化生产模板

import java.io.RandomAccessFile;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 大文件分片多线程读取
 * 适用:1GB+超大日志、批量数据文件
 */
public class BigFileReadDemo {
    // 分片大小:每片50MB
    private static final long SLICE_SIZE = 1024 * 1024 * 50;
    private static final String FILE_PATH = "D:/big_log.txt";

    public static void main(String[] args) throws Exception {
        // 根据CPU核心数创建线程池
        ExecutorService pool = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());
        RandomAccessFile file = new RandomAccessFile(FILE_PATH,"r");
        long totalSize = file.length();
        // 计算分片数量
        long sliceNum = (totalSize + SLICE_SIZE - 1) / SLICE_SIZE;

        for (long i = 0; i < sliceNum; i++) {
            long start = i * SLICE_SIZE;
            long end = Math.min(start + SLICE_SIZE, totalSize);
            // 提交分片读取任务
            pool.execute(() -> readSlice(start,end));
        }
        pool.shutdown();
    }

    // 分片读取逻辑
    private static void readSlice(long start, long end){
        try(RandomAccessFile raf = new RandomAccessFile(FILE_PATH,"r")){
            raf.seek(start);
            byte[] buffer = new byte[1024];
            long current = start;
            int len;
            while ((len = raf.read(buffer)) != -1 && current < end){
                // 业务解析、入库、清洗逻辑
                current += len;
            }
        }catch (Exception e){
            e.printStackTrace();
        }
    }
}

14.4.4 线上优化方案

  1. IO密集型文件读取,线程池核心数设置为 CPU核心数*2

  2. 使用try-with-resources自动关闭流,杜绝文件句柄泄露;

  3. 超大文件禁止一次性加载内存,防止OOM内存溢出。

14.5 多线程事务一致性(线上疑难痛点)

14.5.1 原生痛点

Spring默认事务仅支持单线程事务,多线程异步插入、更新数据,主线程回滚无法控制子线程,极易出现数据不一致、部分成功部分失败。

14.5.2 三种解决方案(生产分级)

  1. 最终一致性(简单业务):本地事务表+定时补偿、失败重试、幂等校验;

  2. 编程式事务(中等复杂度):TransactionTemplate手动管控事务,多线程汇总结果统一提交/回滚;

  3. 分布式事务(复杂业务):Seata TCC、XA模式,适配微服务多线程跨库事务。

14.5.3 多线程本地事务模板

import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.concurrent.CountDownLatch;

/**
 * 多线程事务一致性|简易生产模板
 * 原理:子线程独立事务,全部执行成功才手动提交,任意失败全部回滚
 */
public class MultiThreadTransactionDemo {
    // 注入Spring容器中的TransactionTemplate
    private TransactionTemplate transactionTemplate;

    public void batchSave(){
        CountDownLatch latch = new CountDownLatch(5);
        // 标记是否全部执行成功
        AtomicBoolean success = new AtomicBoolean(true);
        for (int i = 0; i < 5; i++) {
            new Thread(()->{
                try {
                    transactionTemplate.execute((TransactionStatus status)->{
                        // 数据库新增、修改业务逻辑
                        return null;
                    });
                }catch (Exception e){
                    success.set(false);
                }finally {
                    latch.countDown();
                }
            }).start();
        }
    }
}

14.5.4 线上避坑规范

  1. 禁止在异步线程使用注解式事务(@Transactional),传播机制失效;

  2. 多线程强一致性业务,禁止使用简单异步,优先同步编排;

  3. 必须加幂等唯一键,防止重试造成重复脏数据。

14.6 异步回调地狱优化(告别多层嵌套)

14.6.1 传统痛点

原始Future、Thread异步编码,多层依赖业务嵌套,代码层级臃肿、可读性极差,维护成本极高,俗称回调地狱。

14.6.2 三代异步编码演进

  1. 第一代:Thread+Runnable,无返回、嵌套混乱;

  2. 第二代:Future,阻塞get()、无法回调;

  3. 第三代:CompletableFuture,非阻塞、链式编排;

  4. 第四代:StructuredTaskScope(JDK21),结构化并发、无泄露。

14.6.3 CompletableFuture生产链式模板

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * 异步链式编排|消灭回调地狱
 * 串行执行、并行聚合、异常兜底、超时降级
 */
public class FutureChainDemo {
    private static final ExecutorService POOL = Executors.newFixedThreadPool(3);

    public static void main(String[] args) throws Exception {
        // 链式异步编排,平铺代码无嵌套
        CompletableFuture.supplyAsync(() -> "查询用户信息",POOL)
                .thenApply(user -> user + "|查询订单信息")
                .thenApply(order -> order + "|聚合返回结果")
                .exceptionally(e -> "业务异常兜底降级")
                .whenComplete((res,ex)-> System.out.println("最终结果:"+res));
    }
}

14.6.4 线上使用规范

  1. 异步任务必须指定自定义线程池,禁止使用默认ForkJoinPool;

  2. 所有异步链路必须加exceptionally异常兜底;

  3. 高版本JDK优先使用StructuredTaskScope替代CompletableFuture。

14.7 并发限流算法(线程池结合限流实战)

14.7.1 三大主流限流算法生产对比

限流算法

原理

生产适用场景

优缺点

漏桶算法

固定速率流出,强行削峰

接口平稳限流、支付回调

流量均匀,无法应对突发峰值

令牌桶算法

定时生成令牌,拿到令牌放行

秒杀、活动、突发高并发

支持瞬时流量爆发,生产首选

滑动窗口

拆分时间片,动态统计流量

网关全局限流、风控拦截

精度最高,内存开销略大

14.7.2 令牌桶极简实战代码

import java.util.concurrent.TimeUnit;

/**
 * 令牌桶限流|高并发接口限流模板
 * 控制每秒最大请求数,应对流量洪峰
 */
public class TokenBucketRateLimit {
    // 最大令牌容量
    private static final int MAX_CAPACITY = 20;
    // 每秒生成令牌数
    private static final int TOKEN_PER_SECOND = 10;
    // 当前令牌数量
    private static int tokenCount = 0;
    // 上次生成令牌时间
    private static long lastTime = System.currentTimeMillis();

    // 获取令牌,是否放行
    public static synchronized boolean tryAcquire(){
        long now = System.currentTimeMillis();
        // 计算时间差,补充生成令牌
        long diffTime = now - lastTime;
        int addToken = (int) (diffTime / TimeUnit.SECONDS.toMillis(1)) * TOKEN_PER_SECOND;
        tokenCount = Math.min(MAX_CAPACITY,tokenCount + addToken);
        lastTime = now;
        // 判断是否有令牌
        if(tokenCount > 0){
            tokenCount--;
            return true;
        }
        return false;
    }
}

14.7.3 线上限流规范

  1. 业务接口优先令牌桶,网关层使用滑动窗口;

  2. 限流拒绝策略禁止直接抛出异常,优先降级、兜底、排队;

  3. 多节点分布式限流,使用Redis+Lua保证原子性。

14.8 线上线程池压测调优(资深工程师核心能力)

14.8.1 核心参数黄金配置

  1. CPU密集型:核心线程数 = CPU核心数 + 1,减少上下文切换;

  2. IO密集型:核心线程数 = CPU核心数 * 2 ~ 5,适配阻塞等待;

  3. 队列容量:业务接口默认100~500,超大批量任务1000+;

  4. 拒绝策略:线上禁止AbortPolicy直接抛异常,优先CallerRunsPolicy回退主线程。

14.8.2 线上线程池致命坑

  1. 禁止使用Executors快捷创建线程池,无边界队列引发OOM;

  2. 禁止全局共享线程池,业务隔离,拆分支付、订单、日志独立线程池;

  3. 线程池必须自定义线程工厂,命名规范,方便堆栈排查;

  4. 定时任务线程池禁止处理耗时业务,造成任务堆积。

14.8.3 生产线程池最终模板(直接上线)

import java.util.concurrent.*;

/**
 * 线上标准线程池模板|无BUG、可直接上线
 * 命名规范、业务隔离、拒绝策略优雅、防止OOM
 */
public class ThreadPoolFactory {
    // CPU核心数
    private static final int CPU_NUM = Runtime.getRuntime().availableProcessors();

    // IO密集型通用线程池
    public static ThreadPoolExecutor getIoPool(){
        return new ThreadPoolExecutor(
                CPU_NUM * 2,
                CPU_NUM * 4,
                30L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(200),
                r -> new Thread(r,"business-io-thread-" + r.hashCode()),
                // 回退主线程,不丢失任务
                new ThreadPoolExecutor.CallerRunsPolicy()
        );
    }
}

14.8.4 压测监控指标

  1. 核心监控:线程活跃数、队列积压数量、任务拒绝次数、平均执行耗时;

  2. 优化标准:队列积压始终 < 30%,无拒绝任务,CPU利用率稳定在70%左右;

  3. 调优手段:压测逐步放大并发量,动态修改核心线程数,找到性能拐点。



第十五部分 高频面试查漏补缺(全覆盖)

  1. sleep 与 wait 区别

  2. 为什么放弃使用 stop 终止线程

  3. 双重检查锁为什么必须加 volatile

  4. ConcurrentHashMap 1.7 与 1.8 区别

  5. AQS 独占与共享原理

  6. 线程池参数如何合理配置

  7. 如何优雅关闭线程池

  8. 原子类为何不用锁也能保证线程安全

  9. ThreadLocal 内存泄漏原因与解决

  10. 偏向锁失效场景

  11. 手写生产者消费者三种实现

  12. 伪共享产生与解决

  13. 读写锁降级规则

  14. 虚拟线程使用场景与优势

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐