Java 多线程完整版学习文档(无遗漏终版)

文档说明

整合全部基础、进阶、底层源码、冷门死角、面试考点、线上实战、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多少对象,共用同一把类锁。

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

java 复制代码
/**
 * 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();
    }
}

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

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

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

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

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

java 复制代码
/**
 * 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 检测到连续多次对同一把锁频繁加锁、解锁,会自动合并锁范围,将多次加锁合并为一次,避免频繁进出锁消耗性能。

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

优化前(低效)

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

JVM 优化后(自动粗化)

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

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

4.5.2 锁消除(Lock Elimination)

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

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

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

java 复制代码
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 完整使用代码(基础标准写法|生产模板)

java 复制代码
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. 禁止:读锁 ➜ 写锁(升级):会死锁

锁降级标准代码模板

java 复制代码
// 锁降级流程:写锁 → 读锁 → 释放写锁
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 完整实战代码(乐观读标准模板|生产唯一写法)

java 复制代码
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标准模板)

java 复制代码
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替换)

java 复制代码
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问题)

java 复制代码
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标记位 ;无需记录修改次数,只判定对象是否被修改过,节省内存、性能更高。

② 适用场景

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

③ 简易代码示例

java 复制代码
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)

java 复制代码
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)

java 复制代码
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)

java 复制代码
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问题代码复现

java 复制代码
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)

java 复制代码
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信号量)

java 复制代码
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 生产实战代码

java 复制代码
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 生产实战代码

java 复制代码
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 生产实战代码

java 复制代码
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 生产实战代码

java 复制代码
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 生产实战代码

java 复制代码
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 生产标准代码

java 复制代码
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 生产实战代码

java 复制代码
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. 面试坑点:初始化必须传容量,无无参构造;一把锁导致读写不能并行

java 复制代码
// 固定容量为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

java 复制代码
// 手动指定容量,避免无界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

java 复制代码
// 同步移交队列,不存任务,一对一传递
SynchronousQueue<String> queue = new SynchronousQueue<>();

9.3.4 DelayQueue(延时阻塞队列|定时任务)

  1. 底层结构:优先级队列+可延迟元素,底层基于最小堆

  2. 元素要求:元素必须实现Delayed接口,重写延时时间方法

  3. 出队规则:只有元素延时时间到期,才能被取出;未到期则阻塞

  4. 锁机制:ReentrantLock独占锁

  5. 优点:天然支持延时任务、优先级排序、无需额外定时线程

  6. 缺点:排序消耗CPU、元素实现复杂、不适合高频瞬时任务

  7. 生产场景:订单超时关闭、红包过期、延时重试、缓存失效

  8. 面试坑点:无到期元素时,take()永久阻塞;不允许存储null元素

java 复制代码
// 延时队列,元素必须实现Delayed接口
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();

9.3.5 PriorityBlockingQueue(优先级阻塞队列)

  1. 底层结构:可变长度数组,最小堆排序算法

  2. 排序规则:自定义Comparator比较器,优先级高的元素优先出队,无序入队、有序出队

  3. 容量特性:无界队列,自动扩容,初始容量11

  4. 锁机制:全局ReentrantLock独占锁

  5. 优点:支持任务优先级、自动扩容、高优先级任务优先执行

  6. 缺点:排序耗时、扩容消耗内存、低优先级线程容易饥饿

  7. 生产场景:消息优先级推送、紧急任务插队、权重排序业务

  8. 面试坑点:无界队列有OOM风险;非FIFO,打破先进先出规则

java 复制代码
// 自定义比较器,实现任务优先级排序
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(),必须等待消费

java 复制代码
// 高性能无锁转移队列,超高并发专用
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. 生产禁忌:禁止用于用户直连接口,会直接抛出异常影响用户体验。

java 复制代码
// 默认拒绝策略: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. 适用场景:任务不允许丢失、对响应耗时不敏感、低优先级业务。

java 复制代码
// 调用者执行策略
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. 适用场景:可丢弃的日志埋点、统计上报、非核心冗余任务。

java 复制代码
// 静默丢弃策略
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. 适用场景:实时性要求高、旧数据无意义的业务(如实时推送、实时监控)。

java 复制代码
// 丢弃最旧任务策略
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. 适用场景:绝大多数线上生产业务,企业级标准规范。

java 复制代码
/**
 * 自定义拒绝策略:日志告警+任务持久化
 * 生产通用模板,可直接复用
 */
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. 适用场景:任务量平稳、并发量极低、测试环境串行批量任务

java 复制代码
// 固定3条工作线程
ExecutorService fixedPool = Executors.newFixedThreadPool(3);

9.5.2 newSingleThreadExecutor(单一线程池)

  1. 创建方式:内部仅有1条工作线程,串行执行所有任务

  2. 底层源码参数:核心线程数=1、最大线程数=1、无空闲回收时间、队列=无界LinkedBlockingQueue

  3. 执行特点:严格串行执行,任务排队依次执行,不会并发错乱

  4. 优点:单线程执行,无线程安全竞争,任务执行有序

  5. 致命缺点 :同样使用无界队列,海量任务积压引发OOM;单线程执行效率极低,吞吐量差

  6. 生产禁用原因:无任务上限、无拒绝策略,高并发极易内存溢出,且无法利用多核CPU

  7. 适用场景:简单串行任务、日志顺序打印、单机极简同步任务

java 复制代码
// 仅有一条工作线程,串行执行
ExecutorService singlePool = Executors.newSingleThreadExecutor();

9.5.3 newCachedThreadPool(缓存线程池)

  1. 创建方式:无固定线程数,按需创建线程,空闲线程自动回收

  2. 底层源码参数:核心线程数=0、最大线程数=Integer.MAX_VALUE、空闲存活时间60秒、队列=SynchronousQueue同步移交队列

  3. 执行特点:任务到来无空闲线程则新建线程,线程空闲60秒自动销毁,实时移交任务无队列堆积

  4. 优点:响应速度快、瞬时并发能力强、空闲线程自动回收

  5. 致命缺点 :最大线程数近乎无限,高并发瞬间疯狂创建线程,线程数量打爆操作系统上限,造成CPU飙高、线程栈溢出、服务卡死

  6. 生产禁用原因:无线程数量限制,恶意流量/突发流量瞬间创建上千条线程,操作系统线程调度崩溃

  7. 适用场景:大量短耗时、瞬时突发、无压力测试任务

java 复制代码
// 可无限创建线程,空闲线程60s回收
ExecutorService cachedPool = Executors.newCachedThreadPool();

9.5.4 newScheduledThreadPool(定时线程池)

  1. 创建方式:支持延迟执行、周期性循环执行的定时线程池

  2. 底层源码参数:核心线程数=自定义、最大线程数=Integer.MAX_VALUE、队列=延时无界DelayedWorkQueue

  3. 执行特点:支持延迟执行、固定频率执行、固定间隔执行,适配定时任务

  4. 优点:自带定时调度能力,无需手动封装延时逻辑

  5. 致命缺点:最大线程数无上限、队列无界,异常定时任务堆积、线程无限创建,引发OOM+线程溢出

  6. 生产禁用原因:边界不可控,异常任务持续堆积,内存泄露、线程泛滥风险极高

  7. 适用场景:本地简单定时测试、非线上业务延时任务

java 复制代码
// 核心线程数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,自定义七大参数、有界队列、业务线程名、自定义拒绝策略,规避所有内置线程池漏洞。

java 复制代码
/**
 * 企业标准自定义线程池(可直接生产复用)
 * 规避内置线程池所有缺陷:有界队列、有限线程、自定义拒绝策略
 */
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()方法捕获执行异常。
java 复制代码
// 全局异常处理器,防止线程池异常丢失
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可视化监控,队列积压阈值触发告警。

java 复制代码
// 线程池核心监控指标
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 代码示例

java 复制代码
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. 致命缺陷不支持线程池复用。线程池线程长期存活,线程复用后不会重新拷贝父线程数据,出现上下文旧数据残留、串值问题。

java 复制代码
/**
 * 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无需传参,随时获取登录用户,避免参数透传冗余。

核心优势:解耦用户参数,整条请求链路共享用户数据,不污染业务方法入参。

java 复制代码
/**
 * 登录用户上下文工具类(生产标准模板)
 * 全局静态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实现,无需手动维护,原理一致。

java 复制代码
// 简易链路追踪工具
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封装,替代全局静态实例。

java 复制代码
/**
 * 线程安全时间格式化工具
 * 禁止全局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存储当前线程数据源标记,拦截器根据标记切换连接。

java 复制代码
// 动态数据源上下文
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.顺序统一获取锁(最简单、最推荐) 核心原理 :规定所有线程严格按照固定顺序 申请多级锁,破坏循环等待条件,从根源杜绝死锁。所有线程无论执行逻辑,必须先申请序号小的锁、再申请序号大的锁,不会形成闭环等待。 适用场景 :存在嵌套锁、多把锁依赖的业务,如账户转账、多资源加锁。 正反示例对比 错误写法(乱序加锁,必死锁):

java 复制代码
// 线程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)限时获取锁,破坏请求且保持条件;规定时间内未获取到锁,主动放弃当前锁、释放已持有资源,避免无限僵持。 适配场景:第三方接口调用、超时敏感业务、不确定锁竞争时长的场景。

生产标准代码

java 复制代码
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 标准代码示例(生产最简写法)

java 复制代码
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 失败即终止(核心业务)
java 复制代码
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 成功即终止(择优任务)
java 复制代码
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 忽略异常(批量非核心任务)
java 复制代码
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 标准生产代码示例(用户上下文透传模板)

java 复制代码
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)

java 复制代码
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 标准工具类代码(线上直接复用)

java 复制代码
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 生产标准写法

java 复制代码
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 简化生产模板

java 复制代码
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 多线程本地事务模板

java 复制代码
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生产链式模板

java 复制代码
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 令牌桶极简实战代码

java 复制代码
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 生产线程池最终模板(直接上线)

java 复制代码
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. 虚拟线程使用场景与优势

相关推荐
考虑考虑2 小时前
JDK26中的LazyConstant
java·后端·java ee
Devin~Y2 小时前
互联网大厂 Java 面试实录:JVM、Spring Boot、MyBatis、Redis、Kafka、Spring AI、K8s 全链路追问小Y
java·jvm·spring boot·redis·kafka·mybatis·spring security
摇滚侠2 小时前
SpringCloud 面试题 真正的 offer 偏方 Java 基础 Java 高级
java·spring·spring cloud
tongluowan0072 小时前
Java 内存模型(JMM)- happens-before 与内存屏障
java·内存模型·happens-before
plainGeekDev2 小时前
Android Framework 面试题:Binder都说不清楚,简历别写精通了
android·java
Gauss松鼠会2 小时前
【GaussDB】基于SpringBoot实现操作GaussDB(DWS)的项目实战
java·数据库·经验分享·spring boot·后端·sql·gaussdb
玉米Yvmi2 小时前
大文件上传的基石:切片上传原理与实现详解
前端·javascript·面试
Gauss松鼠会2 小时前
【GaussDB】GaussDB 常见问题及解决方案汇总
java·数据库·算法·性能优化·gaussdb·经验总结
xiaogg36782 小时前
k8s 部署yaml文件和Dockerfile文件配置
java·docker·kubernetes