文档说明
整合全部基础、进阶、底层源码、冷门死角、面试考点、线上实战、JDK 新特性,全网最全无缺漏
第一部分 线程基础核心
1.1 基础概念(通俗易懂)
- 进程:操作系统资源分配的最小单位,独立内存、相互隔离。
- 线程:CPU调度最小单位,共享进程资源,开销极小。
- 并发:CPU时间片快速切换,同一时间段交替执行,宏观同时、微观串行。
- 并行:多CPU核心,同一时刻真正同时执行。
- 同步:线程排队执行,阻塞等待,数据安全。
- 异步:线程互不阻塞,后台执行,提高吞吐量。
- Java线程调度模型:抢占式调度,优先级仅为建议,不保证绝对执行顺序。
- 时间片轮转: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~10,默认5;优先级分为最小(1)、普通(5)、最高(10);仅操作系统调度建议,Java不保证优先级生效;底层依赖操作系统时间片抢占,不能用来控制业务执行顺序。
- 守护线程(后台线程):后台服务线程;JVM只要剩下守护线程,直接退出;优先级极低;不能执行业务、不能关闭资源、不建议手动IO操作;典型例子:GC线程、JVM监控线程;生命周期跟随JVM。
- 用户线程:默认创建全部为用户线程;JVM必须等待所有用户线程执行完毕才会正常退出;专门执行业务代码、允许资源读写、事务操作。
- 线程组ThreadGroup:批量管理线程,统一中断、统一查看状态、统一捕获异常;树形结构、支持父子线程组;实际开发极少手动使用,JVM底层默认使用;可以批量中断防止线程泄露。
- 线程唯一标识(Id):线程全局唯一id,不可重复、不可修改;自增生成,JVM内部维护;常用于日志链路追踪、线程排查。
- 线程名称(Name) :默认命名Thread-X;生产环境必须自定义线程名称,方便线上jstack排查故障;线程池务必自定义线程工厂命名。
- 线程是否存活(isAlive):NEW、TERMINATED判定为死亡;RUNNABLE/BLOCKED/WAITING/TIMED_WAITING判定为存活。
- 全局异常处理器UncaughtExceptionHandler:线程出现未捕获异常不会终止JVM,只会单独终止当前线程;默认异常打印简陋、线上无日志;生产必须自定义全局异常处理器,记录堆栈、报错位置、线程信息;杜绝线程静默死亡。
- 线程上下文类加载器:每个线程自带类加载器;父线程传递给子线程;Spring、Tomcat热加载、动态代理底层依赖;防止类加载泄漏。
- 线程禁止修改属性:线程进入TERMINATED终止状态后,禁止修改名称、优先级、是否守护线程;修改直接抛出非法状态异常。
1.4 四种线程创建方式(优劣对比)
- 继承Thread类:无法继承其他类,无返回值,不推荐。
- 实现Runnable接口:无返回值、无异常,解耦,常用。
- Callable+FutureTask:有返回值、可抛异常、支持泛型。
- 线程池创建:生产唯一推荐、复用线程、减少开销。
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 线程四大停止方式(优劣汇总)
- 方式一:自定义布尔变量标记停止(简单业务、无阻塞):自定义flag变量,循环判断标记,手动退出。
- 方式二:interrupt() + 判断中断状态(官方推荐、通用):修改中断标记,配合判断停止线程,支持阻塞线程唤醒。
- 方式三:捕获InterruptedException响应中断(阻塞线程专用):sleep/wait/join阻塞时,捕获异常退出。
- 方式四:废弃禁用方法: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句标准答案(必背)
- 什么是虚假唤醒? 线程未被notify唤醒,无故自动苏醒,属于操作系统底层机制。
- 为什么不能用if? if只判断一次,虚假唤醒后无条件执行,导致业务异常。
- 为什么要用while? 循环二次校验条件,不满足继续等待,从根源杜绝虚假唤醒。
- JDK官方要求? 所有wait()必须包裹在while循环内,是强制编码规范。
- 虚假唤醒能否避免? 不能根除,只能通过while循环规避风险。
1.8 线程通信方式(最全6种|面试必考)
核心概念 :多线程并行执行,彼此隔离;为了完成协同工作、数据传递、任务调度,需要线程之间进行通信。Java一共6种正规线程通信方式。
- 内置锁通信:wait、notify、notifyAll。
- 管道流通信:Piped输入输出流,线程之间传输数据。
- 共享变量通信:volatile标记变量。
1.8.1 六种通信方式总览(背诵清单)
- 共享变量通信:基础方式,配合volatile保证可见性。
- 内置锁等待唤醒:wait() / notify() / notifyAll()(synchronized)。
- 显式锁精准唤醒:Lock + Condition(await/signal)。
- 管道流通信:PipedInputStream / PipedOutputStream。
- 并发工具类通信:CountDownLatch、CyclicBarrier、Semaphore。
- 中间媒介通信:并发容器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 八大指令逐条详解(背诵版)
- lock(锁定):作用于主内存变量,把变量标记为线程独占状态,禁止其他线程修改;常用于锁操作、原子操作。
- unlock(解锁):作用于主内存变量,释放锁定的变量,其他线程可争抢锁定该变量;必须搭配lock使用。
- read(读取) :从主内存读取变量值,传输到工作内存,为load指令做准备。
- load(载入) :将read读取到的数据,存入当前线程的工作内存副本中。
- use(使用):把工作内存中的变量值,传递给线程执行引擎,用于代码运算、逻辑处理。
- assign(赋值):线程运算完毕后,将最新结果赋值给工作内存中的变量副本。
- store(存储):将工作内存中修改后的变量,传输到主内存,为write指令做准备。
- write(写入) :把store传输的数据,写入并更新主内存的共享变量。
2.2.2 变量完整读写流程(面试画图题)
读取流程(主内存→线程):主内存 → read读取 → load载入 → use使用(线程执行)
写入流程(线程→主内存):assign赋值 → store传出 → write写入 → 主内存更新
锁操作流程:lock锁定主内存变量 → 独占操作 → unlock解除锁定
2.2.3 八大强制语法规则(90%人记不全)
- read和load必须成对出现:不能单独读取、不载入;保证主内存数据成功进入工作内存。
- store和write必须成对出现:不能单独传输、不写入;保证工作内存数据刷回主内存。
- 变量不允许无原因丢弃:read后必须load、store后必须write。
- 不允许无赋值直接刷主内存:工作内存必须经过assign修改,才能执行store。
- use/assign必须有序:线程内变量使用、赋值遵循代码顺序。
- lock可重复加锁:加锁次数必须等于解锁次数(可重入锁底层规则)。
- lock锁定后,其他线程禁止操作该主内存变量。
- 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)本质都是为了解决这三大特性问题。
- 原子性:操作不可分割,中途不会被打断。
- 可见性:一个线程修改变量,其他线程立刻感知。
- 有序性:禁止编译器、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); } } |
③ 原子性解决方案
- synchronized:底层lock加锁,保证操作不可中断;
- Lock显式锁:手动独占锁,保证代码块原子性;
- 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; } } |
③ 可见性解决方案
- volatile:强制修改立即刷回主内存,读取强制拉取最新主内存数据;
- synchronized:解锁前刷新数据,加锁后读取最新数据;
- Lock锁:同内置锁,保证内存可见性。
④ 面试标准答案
可见性是解决多线程缓存不一致问题;底层依靠MESI缓存一致性协议+内存屏障;volatile专门解决可见性。
2.3.3 有序性(Ordering)
定义:代码执行顺序按照编写代码顺序执行,禁止编译器、CPU为了优化性能打乱指令执行顺序。
① 指令重排三类(必考)
- 编译器重排:JIT编译优化,调整代码顺序;
- CPU指令重排:CPU乱序执行,提高吞吐;
- 内存系统重排:缓存读写延迟造成顺序错乱。
② 重排危害(经典DCL漏洞)
创建对象三步:开辟内存 → 初始化对象 → 引用指向内存 ;指令重排后会变成:开辟内存 → 引用指向内存 → 初始化对象,出现半初始化对象,线程获取残缺对象,程序崩溃。
③ 有序性解决方案
- volatile:添加内存屏障,禁止上下指令重排;
- synchronized:同步代码块内代码串行,禁止重排;
- 内存屏障:四类屏障强制限制指令顺序。
④ 面试标准答案
有序性防止指令重排,单线程不会重排,多线程共享变量会出现重排漏洞;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 禁止指令重排四大方案
- volatile关键字(最常用):插入内存屏障,禁止上下指令重排,专门解决多线程重排问题。
- synchronized内置锁:同步代码块内代码串行执行,关闭重排优化。
- Lock显式锁:底层AQS保证代码串行,禁止指令乱序。
- 内存屏障:底层硬件指令,强制固定指令执行顺序。
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句)
- 四大屏障分类? LoadLoad(读读)、StoreStore(写写)、LoadStore(读写)、StoreLoad(写读)。
- 最强屏障? StoreLoad,开销最大、兼顾读写刷新,volatile写依赖它。
- 屏障核心作用? 禁止指令重排、强制内存同步,保障有序性+可见性。
- volatile底层屏障? 读加Load系列屏障,写加StoreLoad屏障。
- 使用场景? 无锁并发、volatile、CAS底层均依赖内存屏障。
2.6 Happens-Before 八条规则(必背)
- 程序顺序规则:单线程代码有序。
- volatile规则:写先行于读。
- 锁规则:解锁先行于加锁。
- 线程启动规则:start()先行于内部代码。
- Join规则:子线程结束先行于主线程后续代码。
- 中断规则:中断代码先行于异常捕获。
- 对象终结规则:初始化先行于销毁。
- 传递性: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 危害(高并发致命坑)
- 性能断崖式下跌:无业务竞争的变量产生虚假竞争,大量CPU资源浪费在缓存同步;
- 并发吞吐量降低:多线程并行执行变串行缓存刷新,丧失CPU并行优势;
- 线上偶发性能卡顿:低并发无感知,高并发流量下性能雪崩,排查难度极高;
- 无报错无异常:代码逻辑完全正确,仅底层缓存层面性能损耗。
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句标准答案(必背)
- 伪共享是什么? 多个无关变量共存同一缓存行,修改一个导致整行缓存失效,产生虚假竞争。
- 缓存行大小? 默认64字节,是CPU缓存最小读写单位。
- 产生条件? 多线程修改同一缓存行内不同独立变量。
- 最优解决方案? JDK8+使用@Contended注解,自动缓存行填充。
- 典型应用? LongAdder分段计数底层规避伪共享,高并发性能极强。
第三部分 Volatile关键字
简洁:
- 状态标记位。
- DCL双重检查锁单例。
- 一写多读场景。
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 禁止使用场景(避坑总结)
- 禁止多写场景:多个线程同时修改volatile变量,无法保证原子性,数据覆盖错乱;
- 禁止复合运算:i++、i=i+1、多变量联动计算,必须加锁或CAS;
- 禁止做事务变量:事务一致性要求原子性,volatile无法保障。
3.3 冷门易错点(95%程序员踩坑|深度补全)
简洁:
- 不能修饰构造方法。
- 防止构造方法逃逸。
- volatile数组:仅引用可见,内部元素不可见。
- DCL必须加volatile:防止指令重排导致半初始化对象。
详细:
- 修饰权限限制:volatile不能修饰构造方法、接口方法、局部变量;仅能修饰类成员变量、静态成员变量,修饰局部变量直接编译报错。
- 防止构造方法逃逸:对象引用被volatile修饰时,禁止在构造方法中向外发布当前对象引用,避免未初始化完成的对象被其他线程获取,造成引用逃逸BUG。
- volatile数组特殊性 :volatile仅修饰数组引用地址,保证数组引用可见性;数组内部元素不具备可见性、有序性,多线程修改数组元素仍存在线程安全问题。
- DCL单例强制加volatile底层原因:new对象分为开辟空间、初始化、引用赋值三步,无volatile会发生指令重排,出现引用赋值优先于初始化,导致其他线程获取半初始化空对象。
- volatile不阻塞线程:volatile属于无锁机制,不会造成线程阻塞、无上下文切换,开销远小于synchronized,是最轻量级同步关键字。
- volatile不能修饰常量:被final修饰的常量不可修改,搭配volatile无任何意义,编译器会优化剔除修饰符,代码无报错但冗余。
- volatile变量禁止编译优化:普通变量会被JIT编译器缓存、指令重排优化,volatile变量禁用缓存优化,每次强制从主内存读取,禁止代码合并、剔除。
- 线程切换可见性延迟:volatile可见性并非绝对实时,依赖CPU缓存刷写时机,高并发下存在纳秒级微弱延迟,不会影响业务,无法做到毫秒级绝对同步。
- 不支持复合操作易错点:除i++外,i = i + 1、i += 2、判断赋值联动等复合操作,均无法保证原子性,底层多条CPU指令极易被线程打断。
- volatile与final区别:final保证变量不可修改、编译期常量;volatile保证变量内存可见、禁止重排,二者作用完全不同,不可混用替代。
- 锁释放优先级:volatile无法替代锁,多线程写场景下,即使加volatile,仍需加锁保证原子性,volatile仅做辅助内存同步。
- 空值可见性:volatile修饰引用变量,置为null时,同样会刷新主内存,其他线程可实时感知null状态,无缓存残留。
3.4 局限性
无法解决复合运算、自增、多线程修改等非原子操作。
第四部分 Synchronized 内置锁
4.1 使用方式
-
修饰普通实例方法:对象锁
-
修饰静态方法:类锁
-
修饰同步代码块:自定义对象锁(生产最常用、粒度最小)
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 加锁底层原理(面试必背)
-
实例方法锁:隐式锁定 this,字节码无 monitorenter/monitorexit,标记 ACC_SYNCHRONIZED。
-
静态方法锁:隐式锁定 类.class,全局唯一 Class 锁。
-
代码块锁 :字节码生成 monitorenter、monitorexit 指令,手动进出锁。
4.1.3 使用方式易错点
-
锁对象禁止为:null、基本类型、常量池字符串(无效锁、死锁隐患);
-
实例锁 & 类锁互不阻塞,混用造成线程安全漏洞;
-
锁范围越大性能越差,优先使用同步代码块缩小临界区。
4.1.4 极易踩坑:锁失效四大场景
- 锁对象地址发生改变:锁对象不能为可变引用(Integer、String、Boolean),引用一改直接锁失效。
- 实例锁 & 静态锁混用:两把不同锁,并行执行,并发安全彻底失效。
- 同一类多把不同锁:锁对象不同,互不阻塞,达不到互斥效果。
- 加锁代码无共享变量:锁错代码位置,锁住无关代码,临界区裸露造成线程不安全。
4.1.5 面试5句标准答案(满分背诵)
- 区别? 实例锁锁this、静态锁锁Class、代码块锁自定义对象。
- 粒度? 静态锁 > 实例锁 > 同步代码块。
- 底层? 代码块靠monitor指令,方法锁靠标记位。
- 互斥? 实例锁和静态锁互不阻塞。
- 生产? 优先同步代码块,缩小锁范围提高并发。
4.2 核心特性
-
隐式加锁、自动释放锁
-
可重入锁,同一线程可多次获取
-
线程发生异常自动释放锁
-
不可中断、不可超时获取
4.3 对象头与 MarkWord
存储锁状态、线程 ID、GC 分代年龄、偏向线程信息,是锁升级载体
4.4 JDK1.6 锁升级全过程
完整升级流向:无锁 ➔ 偏向锁 ➔ 轻量级锁(自适应自旋) ➔ 重量级锁 ;JDK1.6对synchronized做大量优化,锁升级单向不可逆,只能升级、不能降级,目的是在不同竞争场景下平衡性能,尽可能减少重量级锁开销。
-
偏向锁:单线程无竞争,无开销
-
轻量级锁:多线程交替执行,自适应自旋
-
重量级锁:激烈竞争,阻塞排队
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 锁升级触发流程图解(背诵版)
-
对象新建 → 无锁(01);
-
单线程反复加锁 → 偏向锁(01 偏向位1);
-
出现第二条线程竞争 → 撤销偏向锁 → 升级轻量级锁(00);
-
并发激烈、自旋失败 → 膨胀重量级锁(10);
-
升级规则 :只能升级、不能降级,不可逆。
4.4.4 偏向锁批量机制(冷门面试)
-
批量重偏向:同一类大量对象发生偏向锁竞争,阈值20,JVM判定竞争频繁,将后续对象直接偏向新线程。
-
批量撤销:阈值40,竞争极度频繁,判定偏向锁无意义,直接关闭该类所有对象偏向锁,永久使用轻量级锁。
4.4.5 锁升级高频面试必背(8句满分)
-
JDK1.6之后新增锁优化:偏向锁、轻量级锁、自适应自旋;
-
锁升级顺序:无锁→偏向锁→轻量级锁→重量级锁;
-
锁单向不可逆,永远不会降级;
-
偏向锁适合单线程,轻量级适合交替执行,重量级适合激烈竞争;
-
偏向锁延迟加载,默认启动4秒后开启;
-
轻量级锁依靠CAS+自适应自旋,不阻塞;
-
重量级锁依赖操作系统Mutex互斥量,线程阻塞;
-
偏向锁一旦调用hashCode直接撤销偏向标记。
4.5 JVM 四大锁优化机制(JDK1.6 官方优化|面试必考)
JDK1.6 之后 JVM 对 synchronized 进行大量底层优化,除锁升级以外,还包含锁粗化、锁消除、偏向锁优化、自适应自旋四大优化,目的:减少加锁开销、减少CAS竞争、减少阻塞、最大化提升内置锁性能。
简洁:
-
锁粗化:合并连续加锁
-
锁消除:逃逸分析判定无共享直接消除锁
-
偏向锁撤销、批量重偏向、批量撤销
-
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专为高并发对象创建场景优化。
-
批量重偏向(阈值 20):一类对象超过20次偏向竞争,JVM判定线程切换频繁,将后续新建对象直接偏向新竞争线程,减少撤销开销。
-
批量撤销(阈值 40):一类对象竞争超过40次,判定偏向锁无意义,直接永久关闭该类所有对象偏向锁,默认使用轻量级锁。
JVM参数:-XX:BiasedLockingStartupDelay=4000 默认4秒延迟开启偏向锁。
4.5.4 自适应自旋锁(Adaptive Spinning)
原理:JDK1.6 摒弃固定自旋次数,JVM根据前一次锁竞争情况,动态调整自旋次数。
-
上次自旋成功抢到锁 ➜ 本次自旋次数增加;
-
上次自旋失败阻塞 ➜ 本次减少自旋次数,甚至不自旋;
优点:避免盲目自旋空转浪费CPU,兼顾低竞争、高竞争场景性能。
4.5.5 锁优化关闭参数(面试冷门)
-
关闭偏向锁:-XX:-UseBiasedLocking
-
关闭逃逸分析:-XX:-DoEscapeAnalysis(关闭后不会锁消除)
-
修改批量偏向阈值:-XX:BiasedLockingBulkRebiasThreshold=20
4.5.6 锁优化面试必背(7句满分话术)
-
JDK1.6四大锁优化:锁粗化、锁消除、偏向锁批量优化、自适应自旋;
-
锁粗化:合并频繁加锁,减少锁切换开销;
-
锁消除:逃逸分析判定无竞争,直接抹除锁;
-
批量重偏向:阈值20,偏向新线程;
-
批量撤销:阈值40,彻底关闭偏向锁;
-
自适应自旋:动态调整自旋次数,不固定循环;
-
所有优化目的:尽量不要进入重量级锁、不要进入内核态。
4.6 易错点
-
锁对象不能为 null:synchronized 锁对象为 null,直接抛出空指针异常;锁对象引用不可中途修改。
-
实例锁与静态类锁互不互斥:一把锁this、一把锁class,两种锁完全不冲突,并行执行,引发线程安全漏洞。
-
禁止使用字符串字面量做锁:例如synchronized("lock"),字符串常量池复用,不同业务、不同类共用同一把锁,莫名阻塞、死锁隐患极大。
-
禁止包装类缓存对象做锁:Integer、Long缓存-128~127,多个地方使用同一个数字锁,锁对象重合导致诡异阻塞。
-
锁对象不可动态修改引用:加锁期间如果修改锁对象引用,线程切换为不同锁对象,锁彻底失效,数据错乱。
-
同步方法不能修饰构造方法:语法报错,构造方法天生线程安全,不需要加锁。
-
子类不会继承父类同步锁:父类同步方法,子类重写后默认不带锁,必须手动重新加锁。
-
代码异常自动释放锁(重大坑点):同步代码报错,JVM强制释放锁,极易造成数据半更新、事务残缺、脏数据。
-
同步块内sleep不释放锁:sleep只让出CPU,不会释放Monitor,其他线程全部阻塞,极易造成线程卡顿积压。
-
循环内频繁加锁性能极差:哪怕JVM锁粗化优化,也不要在循环内部写锁,加重编译开销、降低吞吐量。
-
方法内部新建锁对象必定锁失效:每次进入方法new不同锁,线程互不争抢,锁完全无效。
-
偏向锁调用hashCode直接失效:偏向锁状态下,一旦调用hashCode,强制撤销偏向锁,直接升级轻量级锁。
-
偏向锁存在4秒延迟开启:程序刚启动前4秒,全部为轻量级锁,并非偏向锁。
-
synchronized不可中断:线程阻塞等待锁时,interrupt无法打断阻塞,这是和Lock最大区别。
-
空同步代码块依旧生成指令:空锁代码无业务意义,但仍生成monitor指令,浪费CPU开销。
-
锁粗化不会跨方法优化:仅优化同一方法内连续加锁,跨方法频繁加锁不会合并。
-
逃逸分析开启才会锁消除:关闭JVM逃逸分析,局部对象锁不会消除,白白浪费性能。
-
重量级锁切换内核态开销巨大:一旦膨胀为重量级锁,用户态切内核态,上下文切换严重卡顿。
-
锁只能锁住堆内存对象:常量、静态常量、元空间对象不适合做业务锁,容易全局锁粘连。
-
synchronized不能穿透线程逃逸:锁内new对象逃逸到外部,依旧存在并发安全问题。
4.6.1 synchronized 锁失效十大场景(面试必考)
-
锁对象不一致:时而锁this、时而锁class,混用锁导致失效。
-
锁对象被重新赋值:中途修改锁引用,多线程锁不同对象。
-
方法内部new临时锁:每次锁都是新对象,锁完全无效。
-
字符串常量池锁复用:不同业务共用同一字面量字符串锁。
-
加锁代码逻辑没有共享变量:锁加了但无意义,不保护临界资源。
-
volatile搭配synchronized顺序错乱:无法提升原子性,纯属多余。
-
子类重写丢失同步修饰:子类方法不加锁,打破父类线程安全。
-
锁范围过大、无关代码加锁:阻塞正常业务,吞吐量暴跌。
-
自旋时间过长CPU飙高:高并发下轻量级锁自旋空转,CPU占用100%。
-
异常吞锁:try-catch包住同步代码,异常自动释放锁引发脏数据。
4.6.2 synchronized 终极背诵总结(一页纸)
-
底层:对象头MarkWord + Monitor监视器锁;
-
升级:无锁→偏向→轻量级→重量级,单向不可逆;
-
优化:锁粗化、锁消除、自适应自旋、批量偏向;
-
特性:可重入、自动解锁、异常释放、不可中断;
-
用法:优先同步代码块、缩小临界区、不要锁常量、不要锁null;
-
坑点:混用锁、修改锁引用、字面量锁、循环内加锁、异常丢数据。
第五部分 Lock 显式锁体系
5.1 ReentrantLock 可重入锁(重点必考)
ReentrantLock 是 JDK 显式锁核心实现,基于 AQS 底层实现,手动加锁、手动解锁、灵活性远超 synchronized,是企业生产中最常用的显式锁。
-
手动 lock () 加锁、unlock () 释放锁
-
支持公平锁、非公平锁
-
灵活性远超 synchronized
5.1.1 核心特性
-
显式加解锁:手动 lock () 加锁、unlock () 释放锁,可控性极强
-
可重入特性:同一线程可反复多次加锁,不会自己阻塞自己
-
锁模式可选:支持公平锁、非公平锁(默认非公平)
-
可中断阻塞:线程阻塞等待锁时可被中断
-
超时防死锁:支持限时获取锁,避免永久死锁
-
精准条件唤醒:配合 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 状态变量实现可重入:
-
线程第一次加锁:state = 0 ➜ CAS 修改为 1,记录当前独占线程;
-
同一线程再次加锁:判断当前持有锁线程为本线程,state + 1;
-
重复加锁 N 次,state 累加为 N;
-
解锁一次 state - 1,必须加锁次数 = 解锁次数,state 归零才算真正释放锁。
5.1.4 公平锁 & 非公平锁(源码级区别)
① 非公平锁(默认)
-
特点:新线程直接抢占锁,不排队、插队执行
-
优点:吞吐量高、CPU 切换少、性能极强
-
缺点:老线程可能长期饥饿
-
底层:上来直接 CAS 抢锁,不管队列是否有等待线程
② 公平锁
-
特点:严格按照队列顺序排队,先进先出,禁止插队
-
优点:线程公平、无饥饿现象
-
缺点:大量上下文切换、吞吐量低、性能差
-
底层:先判断队列是否有前驱节点,有则入队排队
5.1.5 四种加锁方式对比(进阶API)
|---------------------|------------|----------|
| 加锁方法 | 特性 | 生产用途 |
| lock() | 阻塞加锁、不可中断 | 普通业务加锁 |
| lockInterruptibly() | 可中断阻塞 | 需要终止卡死线程 |
| tryLock() | 无阻塞尝试、立即返回 | 快速判断锁占用 |
| tryLock(time) | 限时等待、超时放弃 | 防死锁、超时降级 |
5.1.6 ReentrantLock 高频易错点(冷门坑)
-
必须手动解锁:忘记 unlock 永久死锁、线程卡死,必须写在 finally。
-
加锁解锁次数必须一致:重入加锁几次,必须解锁几次,state 不归零锁不释放。
-
不可在 if 内解锁:防止异常跳过解锁,必须固定 finally。
-
非公平锁性能远高于公平锁:生产默认非公平,除非业务强制公平。
-
锁内不要写耗时IO:占用锁时间过长,大量线程阻塞积压。
-
不能空解锁:未加锁直接 unlock,直接抛出异常。
5.1.7 面试满分总结(背诵)
-
ReentrantLock 基于 AQS,依靠 state 实现可重入;
-
默认非公平锁,吞吐量高,适合绝大多数业务;
-
四大加锁方式:阻塞、可中断、尝试、限时;
-
必须 finally 解锁,加解锁次数一致;
-
比 synchronized 灵活:可中断、可超时、可公平、多条件唤醒。
5.2 进阶获取锁方式
-
lockInterruptibly ():可中断锁,阻塞可被打断
-
tryLock ():无阻塞尝试获取
-
tryLock (time):超时限时获取,防死锁
5.3 Condition 条件队列
实现精准唤醒,替代 notifyAll,区分不同业务线程唤醒,解决内置锁唤醒粗暴、无法精准控制的痛点,是 Lock 体系专属的等待/通知组件
5.3.1 Condition 核心介绍
Condition 是 JDK1.5 随 Lock 推出的条件等待队列,绑定显式锁 ReentrantLock,替代 synchronized 的 wait()、notify()、notifyAll()。
核心最大优势:精准唤醒,可以将线程分组,指定唤醒某一组线程,不会盲目全部唤醒,减少无效竞争,极大提升并发性能。
5.3.2 核心常用 API
-
await():阻塞等待、释放锁,等效于 wait()
-
signal():唤醒单个等待线程,等效于 notify()
-
signalAll():唤醒全部等待线程,等效于 notifyAll()
-
awaitNanos():限时等待、支持超时自动唤醒
-
awaitUninterruptibly():等待过程不可被中断
5.3.3 底层原理(面试必考)
-
每一个 Condition 内部维护一条独立单向条件等待队列;
-
调用 await():当前线程释放锁,封装为节点加入条件队列;
-
调用 signal():将条件队列头部节点转移到 AQS 同步队列;
-
等待节点抢到锁后,从 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 高频易错点(面试踩坑)
-
await() 必须在循环中判断:防止多线程下虚假唤醒,和 wait 使用规范一致。
-
Condition 必须绑定 Lock:未加锁直接调用 await/signal 直接抛出异常。
-
读锁不能创建 Condition:ReadLock.newCondition() 直接报错,仅写锁可用。
-
禁止混用条件队列:生产者、消费者必须分开队列,防止唤醒错乱。
-
await 自动释放锁:和 wait 一样,阻塞时主动释放当前 Lock 锁。
5.3.7 面试满分总结(背诵5句)
-
Condition 是显式锁专属条件队列,替代 wait/notify;
-
支持多队列分组,实现精准唤醒,性能优于内置锁;
-
底层维护单向条件队列,节点转移至AQS同步队列完成唤醒;
-
await必须while循环、必须加锁、finally解锁;
-
生产多用于生产者消费者、线程精准阻塞唤醒场景。
5.4 读写锁 ReentrantReadWriteLock(读多写少专用)
ReentrantReadWriteLock 是 JDK 高性能读写分离锁,分为读锁(共享锁) 、写锁(排他锁),专门优化读多写少业务场景,大幅提升并发吞吐量,例如:缓存、配置类、静态数据。
-
读共享、写独占
-
支持写锁降级为读锁,禁止读锁升级写锁
5.4.1 四大锁互斥规则(面试必考)
-
读与读:共享不互斥:多个线程同时加读锁,完全并行,无阻塞。
-
读与写:互斥阻塞:有读锁,写锁阻塞;有写锁,读锁阻塞。
-
写与写:互斥阻塞:写锁独占,同一时刻只能一个线程修改。
-
写锁降级读锁:允许(唯一降级规则)。
5.4.2 核心特性
-
读写分离:拆分读锁、写锁,粒度更细,并发度更高。
-
可重入:读锁、写锁均支持同一线程重复加锁。
-
锁降级机制 :写锁可以降级为读锁,禁止读锁升级写锁。
-
支持公平/非公平:默认非公平,吞吐量更高。
-
锁饥饿问题:非公平模式下,读多写少容易造成写锁长期饥饿。
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 锁降级(面试高频难点)
定义:持有写锁的线程,获取读锁后,释放写锁,由写锁变为读锁。
规则硬性要求:
-
✅ 允许:写锁 ➜ 读锁(降级)
-
❌ 禁止:读锁 ➜ 写锁(升级):会死锁
锁降级标准代码模板
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位拆分使用:
-
高16位:读锁计数(共享锁、线程个数)
-
低16位:写锁计数(独占锁、重入次数)
5.4.6 优缺点总结
优点
-
读操作并行,极大提高读多写少场景吞吐量;
-
保证写操作原子性、独占性,数据安全;
-
细粒度锁,性能优于普通独占锁。
缺点
-
非公平模式下写锁容易饥饿;
-
不支持锁升级,升级直接死锁;
-
读写交替频繁场景性能反而变差。
5.4.7 高频易错点(生产踩坑)
-
禁止读锁升级写锁:自身持有读锁,无法获取写锁,永久死锁。
-
写锁必须降级:数据修改后需要立刻可见,降级保证数据一致性。
-
读锁不支持条件队列:readLock.newCondition() 直接报错。
-
写锁一定要最后释放:降级顺序不能颠倒。
-
大量读锁积压会饿死写锁:高并发读场景建议使用 StampedLock。
5.5 StampedLock(JDK8)
StampedLock 是 JDK8 新增的**改进型读写锁**,优化 ReentrantReadWriteLock 写锁饥饿问题,新增乐观读模式,无锁开销、超高并发性能,专门适配超高读多写少场景。底层同样基于AQS,引入**戳记Stamp**版本机制,区分锁状态,是JUC高性能锁代表。
5.5.1 三大锁模式(核心必考)
-
乐观读(Optimistic Reading):无锁、不加锁、无阻塞、无CAS,仅记录戳记,极致高性能,允许并发写。
-
悲观读(Read Lock):等效普通读写锁读锁,共享锁、阻塞写线程,线程安全。
-
写锁(Write Lock):独占锁,排他阻塞所有读写线程。
5.5.2 核心特性
-
解决写锁饥饿:打破读锁大量积压卡死写锁问题,读写排队公平性优于ReentrantReadWriteLock。
-
乐观读无开销:不加锁、不阻塞,适合超高并发只读场景。
-
不可重入 :致命特点,StampedLock不支持可重入,重复加锁直接死锁。
-
不支持条件队列Condition:无法精准唤醒,没有await/signal机制。
-
戳记Stamp校验:每次加锁返回long类型戳记,校验戳记判断数据是否被修改。
5.5.3 锁状态与戳记规则
StampedLock 通过long类型stamp戳记标记锁状态,底层二进制划分标识:
-
乐观读:戳记为偶数,无锁标记;
-
悲观读:戳记高位标识读锁占用;
-
写锁:戳记高位标识写锁占用;
-
每次解锁、加锁都会刷新戳记,用于校验数据是否被篡改。
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 乐观读执行流程(面试必背)
-
调用 tryOptimisticRead() 获取偶数戳记,不上锁;
-
无阻塞直接读取共享数据;
-
调用 validate(stamp) 校验戳记是否变更;
-
戳记未变:数据安全,读取完成;
-
戳记改变:读取期间被写线程修改,升级悲观读,重新读取。
5.5.6 StampedLock VS ReentrantReadWriteLock
|---------------|------------------------|-------------|
| 对比项 | ReentrantReadWriteLock | StampedLock |
| 可重入 | ✅ 支持 | ❌ 不支持 |
| 乐观读 | ❌ 无 | ✅ 支持、无锁开销 |
| 写锁饥饿 | 容易饥饿 | 解决饥饿问题 |
| Condition条件队列 | ✅ 支持 | ❌ 不支持 |
| 适用场景 | 普通读多写少 | 超高并发读多写少 |
5.5.7 高频易错坑点(生产严禁踩坑)
-
不可重入(最大坑):同一线程重复加锁直接死锁,绝对不能嵌套加锁。
-
乐观读必须校验戳记:不写validate校验,数据脏读、线程不安全。
-
不支持中断锁:无法响应中断,阻塞线程不能被interrupt终止。
-
禁止混用解锁:读锁戳记不能用写锁解锁,直接抛异常。
-
不支持Condition:不能做精准线程唤醒,不适合生产者消费者场景。
-
乐观读不能加耗时操作:读取逻辑必须简短,防止长时间持有数据快照。
5.5.8 面试满分总结(背诵6句)
-
StampedLock是JDK8高性能读写锁,新增乐观读模式;
-
依靠long戳记标识锁状态,偶数乐观、奇数悲观;
-
三种模式:乐观读、悲观读、写锁;
-
解决ReentrantReadWriteLock写锁饥饿问题;
-
致命缺点:不可重入、不支持中断、无Condition;
-
超高并发读多写少优先使用,普通业务用普通读写锁。
5.6 两大锁核心对比
|------|--------------|------------------|
| 特性 | synchronized | ReentrantLock |
| 锁类型 | 隐式 | 显式 |
| 可中断 | 不支持 | 支持 |
| 公平锁 | 仅非公平 | 可选 |
| 超时获取 | 无 | 支持 |
| 条件唤醒 | 单一 | 多 Condition 精准唤醒 |
第六部分 原子类全家桶(无锁并发)
6.1 基础原子类型(AtomicInteger/AtomicLong/AtomicBoolean)
基础原子类是JDK1.5引入的无锁并发工具 ,底层基于CAS自旋机制,不依赖操作系统互斥锁,在并发场景下保证基础数据类型操作原子性。性能远超synchronized、ReentrantLock,适合简单数值计数、状态标记场景。核心包含三大类:AtomicInteger、AtomicLong、AtomicBoolean。
-
AtomicReference
-
AtomicStampedReference:解决 ABA 问题(版本号)
-
AtomicMarkableReference:标记位解决 ABA
6.1.1 核心底层原理(CAS必考)
-
CAS机制:Compare And Swap,比较并交换,无锁乐观锁思想;包含三个参数:内存值V、旧预期值E、新修改值N。
-
执行逻辑:判断内存原值V是否等于预期值E,相等则无锁修改为新值N;不相等则本次修改失败,重新获取原值,循环自旋重试。
-
Unsafe类:底层依靠sun.misc.Unsafe直接操作内存偏移地址,实现原生CAS指令,规避Java语法层限制。
-
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 三类基础原子类细分使用场景
-
**AtomicInteger(整型原子类)**适用场景:接口请求计数、自增ID、简单整型统计、并发标记;
-
底层存储:int基础数据类型,占用内存小,性能最优。
-
**AtomicLong(长整型原子类)**适用场景:大数据量统计、流水号、日志ID、海量并发计数;
-
底层存储:long类型,支持超大数值,适配高量级统计。
-
**AtomicBoolean(布尔原子类)**适用场景:线程启停标记、开关状态、一次性初始化判断;
-
核心优势:无锁修改状态,常用于控制服务启停、防止重复初始化。
6.1.5 CAS优缺点(面试必背)
优点
-
无锁开销:全程用户态执行,不切换内核态,无线程阻塞、无上下文切换;
-
并发性能高:低竞争场景下,性能碾压synchronized重量级锁;
-
使用简单:API简洁,无需手动加锁解锁,代码简洁不易出错。
缺点
-
自旋空转消耗CPU:高并发竞争激烈时,CAS持续重试,CPU占用率飙升;
-
只能保证单个变量原子性:无法实现多变量复合原子操作;
-
存在ABA问题:数值先改后改还原,CAS无法识别中间修改过程。
6.1.6 高频易错坑点(生产避坑)
-
自增运算符不具备原子性:普通i++是读取、修改、写入三步操作,多线程必然错乱,必须使用原子类;
-
set()非原子一致性修改:set直接覆盖值,不做CAS校验,并发场景慎用;
-
高并发优先使用LongAdder:海量并发计数场景,AtomicLong自旋严重,性能不如分段累加LongAdder;
-
不能解决复合操作:例如判断+赋值组合操作,单纯原子类无法保证原子性,需加锁;
-
AtomicBoolean禁止频繁切换:高频状态切换会导致CAS重试,浪费CPU资源。
6.1.7 面试满分总结(背诵6句)
-
基础原子类包含AtomicInteger、AtomicLong、AtomicBoolean;
-
底层基于CAS+Unsafe+volatile实现,无锁保证原子性;
-
原理为比较并交换,原值匹配则修改,失败自旋重试;
-
低竞争性能极强,高竞争CPU空转严重;
-
存在ABA漏洞,仅适用于单一变量简单计数;
-
生产简单统计用原子类,海量计数优先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
-
get():获取当前引用对象
-
set(V newValue):直接覆盖修改引用
-
compareAndSet(V expect, V update):CAS比对引用,相等则替换
-
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());
}
}
④ 易错坑点
-
CAS比对的是内存地址:不是属性值,两个属性相同的new对象地址不同,替换失败;
-
无版本标记,线程来回修改引用,无法识别ABA篡改;
-
适合静态唯一引用对象,不适合频繁修改的业务对象。
6.2.3 AtomicStampedReference(版本号防ABA|重点必考)
① ABA问题完整复盘
ABA问题:线程A读取内存值为A,线程B先将A修改为B、再改回A;线程A判定原值未变,执行CAS修改,无法识别中间篡改过程,造成业务逻辑漏洞。基础CAS、AtomicReference均存在该问题。
② 底层防篡改原理
内部封装二元组:引用对象 + int版本号 ,每次修改引用,版本号自增+1;CAS比对时,同时校验引用地址+版本号,哪怕对象还原、版本号不同,直接判定篡改,拒绝修改。
③ 核心独有API
-
compareAndSet(V expectRef, V newRef, int expectStamp, int newStamp):双重校验(引用+版本)
-
getStamp():获取当前版本号
-
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 三者面试硬核区别(必背)
-
AtomicReference:纯引用CAS,无标记,存在ABA,适合静态不变对象;
-
AtomicStampedReference:引用+int版本号,记录修改次数,精准防ABA,生产最常用;
-
AtomicMarkableReference:引用+boolean标记,仅判断是否修改,轻量化、省内存。
6.2.6 高频易错坑点(生产避坑)
-
版本号必须手动自增:AtomicStampedReference不会自动叠加版本,需要手动+1,新手极易写错;
-
标记位不可重复使用:AtomicMarkable只有true/false,反复篡改无法识别次数;
-
引用比对依赖地址:所有引用原子类CAS比对均为内存地址,不是属性值;
-
禁止空引用CAS:null引用进行比对,直接抛出空指针异常。
6.2.7 面试满分总结(背诵5句)
-
引用原子类包含三种:普通引用、版本引用、标记引用;
-
AtomicReference无标记,原生存在ABA问题;
-
AtomicStampedReference依靠版本号,精准拦截ABA篡改;
-
AtomicMarkableReference用布尔标记,轻量化判定修改;
-
业务防篡改优先Stamped,简单状态判定优先Markable。
6.3 字段更新原子类
字段更新原子类包含AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater,属于轻量化原子工具类。无需把整个类封装为原子对象,仅对普通类中的**单个成员字段**实现无锁CAS原子修改,节省内存、无需创建额外原子对象,适合大量实体类字段并发更新场景,是生产中优化内存开销的关键工具。
6.3.1 三大字段原子类分类
-
AtomicIntegerFieldUpdater:专门修改实体类int类型成员字段
-
AtomicLongFieldUpdater:专门修改实体类long类型成员字段
-
AtomicReferenceFieldUpdater:专门修改实体类任意引用类型成员字段
6.3.2 核心使用硬性约束(必考易错)
字段更新原子类对被修改字段有严格语法要求,不满足直接报错:
-
修饰符必须为volatile:保证字段可见性,底层CAS依赖volatile禁止指令重排、保证内存可见
-
不能是private私有修饰:必须为public/protected/包访问权限,底层反射获取字段
-
不能是static静态字段:仅支持实例对象成员字段,不支持静态属性
-
不能是final修饰:final字段不可修改,无法进行CAS赋值
6.3.3 底层实现原理
-
底层依旧基于Unsafe类,通过反射获取字段内存偏移地址;
-
采用CAS无锁机制,直接修改堆内存中实体对象的字段值;
-
不创建额外包装对象,直接操作原实体类,内存开销极低;
-
仅能修改单个字段,不支持多字段复合原子操作。
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 生产适用场景
-
海量实体对象并发更新:批量对象单个字段修改,节省原子包装对象内存;
-
状态标记字段更新:实体类启停、状态流转、开关标记修改;
-
数据库乐观锁版本号:实体version版本号无锁自增;
-
自定义并发工具:手写简易自旋锁、计数器工具类。
6.3.7 高频易错坑点
-
字段修饰符违规:缺少volatile、private修饰直接初始化失败;
-
字段名写错:字符串传参字段名错误,运行期反射报错;
-
不支持复合操作:仅能单字段CAS修改,判断+赋值组合无法保证原子性;
-
不可修改数组字段:仅支持基础类型、普通引用类型,不支持数组;
-
反射存在性能损耗:初始化更新器依赖反射,建议全局常量定义更新器,不要重复创建。
6.3.8 面试满分总结(背诵5句)
-
字段更新原子类包含整型、长整型、引用型三类;
-
专门修改普通实体类字段,无需创建原子包装对象;
-
字段必须满足volatile、非私有、非静态、非final;
-
底层Unsafe+CAS实现,内存开销极低、适合海量对象;
-
多用于实体状态更新、乐观锁版本号自增场景。
6.4 数组原子类
AtomicIntegerArray
6.5 高并发分段累加(LongAdder/DoubleAdder)
AtomicLong 在超高并发下大量线程 CAS 自旋空转,CPU 占用飙升、性能严重卡顿。JDK1.8 推出 LongAdder、DoubleAdder 分段累加工具类,采用分散热点思想,将单个竞争变量拆分多段分散竞争,是海量并发计数生产首选,性能碾压 AtomicLong。
简洁:LongAdder、DoubleAdder,分散热点,超高并发优于 AtomicLong
6.5.1 核心核心架构
-
基础变量:base 基础数值(低并发直接累加);
-
分段数组:Cell[] 哈希分段数组,每个Cell独立计数;
-
累加规则:低并发修改base,高并发线程哈希映射到不同Cell,分散竞争;
-
最终总值:总值 = base + 所有Cell数组元素累加和。
6.5.2 为什么性能远超AtomicLong?(面试必考)
-
AtomicLong:仅有一个value变量,所有线程争抢同一内存地址,高并发CAS失败大量自旋,CPU空转严重;
-
LongAdder:采用分段锁思想,将竞争压力分散到多个Cell单元格,不同线程修改不同Cell,极少发生竞争,无需频繁自旋;
-
线程通过哈希算法绑定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 优缺点分析
✅ 优点
-
超高并发性能极强:分散竞争,规避大量CAS自旋,CPU占用低;
-
自适应扩容:Cell数组冲突自动扩容,进一步降低竞争概率;
-
使用简单:API简洁,无需手动加锁,天然线程安全。
❌ 缺点
-
无实时一致性:sum()汇总数值存在延迟,非强一致性;
-
内存占用更高:维护Cell数组,相比AtomicLong占用额外内存;
-
不支持复杂CAS逻辑:仅适合单纯累加、累减,无自定义CAS修改。
6.5.5 DoubleAdder 补充说明
-
专门针对double浮点类型分段累加,底层原理、架构、使用方式和LongAdder完全一致;
-
解决AtomicDouble高并发自旋卡顿问题,适用于浮点型海量统计;
-
浮点运算存在精度丢失,不适合金融高精度计算场景。
6.5.6 生产严格使用规范(避坑)
-
低并发简单计数:优先使用 AtomicLong,内存占用更低;
-
高并发海量计数:接口请求统计、日志计数、流量监控,强制使用 LongAdder;
-
需要实时精准取值:禁止使用LongAdder,汇总存在数据延迟;
-
需要自定义CAS修改:禁止使用分段累加类,改用AtomicLong;
-
sum()方法频繁调用会合并Cell数组,消耗性能,尽量减少汇总次数。
6.5.7 面试满分总结(背诵6句)
-
LongAdder/DoubleAdder是JDK1.8高性能分段累加计数器;
-
底层采用base+Cell分段数组,分散线程竞争压力;
-
低并发修改base,高并发映射Cell,冲突自动扩容;
-
相比AtomicLong,超高并发规避CAS空转,性能大幅提升;
-
缺点是最终求和非实时、弱一致性,内存占用更高;
-
生产海量流量统计优先LongAdder,低并发简单计数用AtomicLong。
6.6 ABA 问题
ABA问题是CAS无锁并发经典漏洞,指线程读取共享数据为A,其他线程先将A修改为B、再修改回A;原线程判定数据未发生变更,正常执行CAS修改,无法识别中间篡改流程,导致业务逻辑隐藏漏洞。CAS仅比对内存值,不识别修改轨迹,这是ABA问题产生的核心根源。
6.6.1 ABA问题完整产生流程
-
线程T1:从内存读取数据A,准备执行CAS修改;
-
线程T2:抢占CPU,将数据A修改为B,随后再次改回A;
-
线程T1:再次读取内存值仍为A,判定数据无修改,执行CAS赋值;
-
结果: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问题实际危害(生产痛点)
-
资金交易漏洞:转账金额来回篡改,CAS判定无异常,造成资金扣减错乱;
-
链表节点丢失:并发修改链表,节点被删除又复原,导致链表断链、数据丢失;
-
状态判定失效:设备状态、订单状态反复切换,程序误判状态未变更;
-
数据统计失真:计数变量来回修改,统计结果掩盖真实篡改记录。
6.6.4 四大解决方案(面试必背)
-
版本号机制(主流):使用AtomicStampedReference,每次修改自增版本号,同时校验数据+版本号;
-
布尔标记机制:使用AtomicMarkableReference,仅标记数据是否被修改,轻量化防篡改;
-
时间戳替代版本号:自定义时间戳字段,每次修改记录时间,比对时间戳判定篡改;
-
加锁兜底:高敏感业务放弃CAS,使用synchronized/Lock排他锁,杜绝并发篡改。
6.6.5 优劣方案对比
|-------------------------|----------------|------------------|
| 解决方案 | 优点 | 缺点 |
| AtomicStampedReference | 记录修改次数,精准防ABA | 需手动维护版本号,代码繁琐 |
| AtomicMarkableReference | 轻量化、内存占用低 | 无法记录修改次数,仅判断是否修改 |
| 自定义时间戳 | 灵活适配业务,通用性强 | 需手动编码,开发成本高 |
| 排他锁 | 彻底杜绝并发篡改,安全性最高 | 加锁开销大,并发性能下降 |
6.6.6 生产使用规范(避坑)
-
简单计数、无业务溯源:普通AtomicInteger即可,无需防ABA;
-
资金、订单、交易敏感数据:强制使用带版本号AtomicStampedReference;
-
仅需判定是否修改、无需次数:优先AtomicMarkableReference节省内存;
-
超高并发敏感业务:放弃无锁CAS,使用显式锁保证绝对安全。
6.6.7 面试满分背诵总结(6句)
-
ABA是CAS无锁并发特有漏洞,数据还原但修改轨迹被忽略;
-
成因是CAS仅比对内存值,不记录修改过程;
-
危害隐藏性高,易造成资金、链表、业务状态异常;
-
核心解决方案:版本号、布尔标记、时间戳、排他锁;
-
Stamped精准计数防篡改,Markable轻量化判定修改;
-
普通计数无需处理ABA,敏感业务必须加版本管控。
第七部分 AQS 抽象队列同步器(JUC 底层核心)
7.1 核心架构(完整版|面试必背)
AQS全称AbstractQueuedSynchronizer抽象队列同步器,是JUC并发包底层基石 ,所有锁、并发工具类底层均依赖AQS实现。核心架构由三大核心属性、双向CLH同步队列、内部节点、模板方法构成,内置双向 FIFO 同步队列,通过 state 状态值控制资源抢占,采用模板方法模式,子类只需重写少量方法即可实现自定义锁。
简洁:内置双向 FIFO 同步队列,通过 state 状态值控制资源抢占。
7.1.1 三大核心全局属性
-
state 同步状态变量(核心):int类型,volatile修饰,保障可见性;不同锁含义不同,ReentrantLock代表加锁次数、Semaphore代表剩余许可数、CountDownLatch代表剩余倒计时数;通过CAS无锁修改,保证并发安全。
-
head 头节点:双向同步队列头部节点,不存储业务线程,作为虚拟哨兵节点,专门用于唤醒下一个阻塞节点,减少并发竞争开销。
-
tail 尾节点:双向同步队列尾部节点,新竞争失败的线程节点,通过CAS自旋挂载到队列尾部,排队等待获取锁。
7.1.2 CLH双向同步队列
-
结构:双向链表结构,每个节点保存前驱、后继引用,支持从尾部入队、头部出队,保证FIFO先进先出排队规则。
-
作用:存储抢占资源失败的线程,线程挂起阻塞,避免空转消耗CPU;资源释放后由头节点唤醒后继节点。
-
特性:非公平锁允许新线程插队,公平锁严格按照队列顺序执行,无插队行为。
7.1.3 内部Node节点组成
每一个阻塞线程都会被封装为Node节点,存入同步队列,核心字段:
-
thread:绑定当前阻塞线程;
-
prev:前驱节点引用;
-
next:后继节点引用;
-
waitStatus:节点等待状态(对应五大节点状态);
-
nextWaiter:条件队列后继节点,用于Condition精准唤醒。
7.1.4 整体执行架构流程
-
资源抢占:线程调用acquire()尝试CAS修改state抢占资源;修改成功直接执行业务逻辑。
-
失败入队:抢占失败,封装为Node节点,自旋CAS挂载到队列尾部。
-
线程阻塞:节点挂载完成,校验前驱节点状态,安全判断后阻塞挂起,释放CPU。
-
资源释放:持有锁线程执行完毕,调用release()修改state释放资源。
-
后继唤醒:头节点唤醒下一个有效阻塞节点,竞争资源继续执行。
7.1.5 AQS架构设计亮点(面试加分)
-
模板方法模式:封装通用排队、阻塞、唤醒逻辑,子类只需实现tryAcquire/tryRelease简单方法;
-
双队列设计:同步队列+条件队列,兼顾普通排队、精准唤醒场景;
-
无锁入队:通过CAS实现节点入队,不依赖额外锁,性能高效;
-
自适应唤醒:规避无效唤醒,减少上下文切换开销。
7.2 两大独占共享模式(底层源码+实战代码补全)
AQS 将同步资源抢占分为独占模式、共享模式,两种模式队列唤醒、资源释放逻辑完全不同,是JUC锁工具底层核心区分点,面试高频考点。
简洁:
-
独占模式:ReentrantLock
-
共享模式:CountDownLatch、Semaphore
7.2.1 独占模式(Exclusive)
① 核心特性
-
同一时刻仅允许一个线程持有资源,排他性抢占;
-
资源释放仅唤醒一个后继阻塞线程;
-
不可共享、无传播唤醒机制;
-
典型实现类:ReentrantLock、ReentrantReadWriteLock写锁。
② AQS核心重写方法
-
tryAcquire():尝试独占获取资源
-
tryRelease():独占模式释放资源
-
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)
① 核心特性
-
同一时刻允许多个线程同时持有资源,共享抢占;
-
资源释放后存在传播唤醒机制,连续唤醒后继共享线程;
-
支持限流、计数器、栅栏等并发场景;
-
典型实现类:Semaphore、CountDownLatch、读写锁读锁。
② AQS核心重写方法
-
tryAcquireShared():尝试共享获取资源,返回int值(负数失败、正数成功、无剩余资源)
-
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句)
-
AQS分为独占、共享两大模式,是JUC所有并发工具底层根基;
-
独占模式单线程占用资源,典型实现为ReentrantLock;
-
共享模式多线程并发占用,典型实现为Semaphore、CountDownLatch;
-
独占单次唤醒单线程,共享支持链式传播唤醒;
-
独占返回布尔结果,共享返回int判定剩余资源;
-
读写锁融合两种模式:写独占、读共享。
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 高频面试必背要点
-
正负规则:仅CANCELLED为正数(1),其余全部为负数或0,正数代表节点失效;
-
SIGNAL优先级最高:同步队列阻塞节点统一标记为-1,是生产最常见状态;
-
状态不可逆:节点一旦变为CANCELLED取消状态,永远无法恢复,只能被清理;
-
队列隔离:CONDITION(-2)仅存在条件队列,不会进入同步队列;
-
专属场景:PROPAGATE(-3)专属共享锁,独占锁永远不会出现该状态。
7.4 核心方法(完整版补全|源码级解析)
AQS 所有方法分为对外暴露公共方法、子类重写模板方法、内部私有工具方法,以下整理面试必考、源码高频核心方法,全部附带作用解析、使用场景、执行逻辑,通俗易懂无晦涩冗余。
简洁:acquire () 获取资源、release () 释放资源。
7.4.1 对外公共核心方法(使用者调用)
-
acquire(int arg):独占模式获取资源,不可中断;流程:尝试获取资源→失败入队→阻塞等待,ReentrantLock.lock()底层调用。
-
acquireInterruptibly(int arg):独占可中断获取,阻塞过程可被interrupt()打断,抛出中断异常。
-
tryAcquireNanos(int arg, long nanosTimeout):独占限时获取,超时自动放弃,防止永久阻塞死锁。
-
release(int arg):独占模式释放资源,修改state状态,唤醒后继阻塞节点。
-
acquireShared(int arg):共享模式获取资源,不可中断,适用于信号量、计数器。
-
acquireSharedInterruptibly(int arg):共享模式可中断获取资源。
-
releaseShared(int arg):共享模式释放资源,唤醒传播后继共享节点。
7.4.2 子类重写模板方法(自定义锁实现)
AQS默认抛出异常,子类根据模式选择性重写,遵循模板方法设计模式。
-
tryAcquire(int arg):独占尝试获取资源,返回boolean;成功true、失败false。
-
tryRelease(int arg):独占尝试释放资源,返回boolean;释放成功true。
-
isHeldExclusively():判断当前线程是否持有独占锁,多用于重入锁、锁状态判断。
-
tryAcquireShared(int arg):共享尝试获取资源,返回int;负数失败、0成功无剩余、正数成功有剩余资源。
-
tryReleaseShared(int arg):共享尝试释放资源,返回boolean;是否释放成功。
7.4.3 内部私有底层方法(AQS核心底层)
-
addWaiter(Node mode):线程竞争失败,封装为Node节点,CAS自旋快速入队。
-
enq(final Node node):初始化队列、节点自旋入队,保障多线程入队安全。
-
acquireQueued(final Node node, int arg):队列内节点循环竞争锁,判断前驱节点状态,安全阻塞线程。
-
shouldParkAfterFailedAcquire(Node pred, Node node):判断当前节点是否需要阻塞,清理队列中CANCELLED失效节点。
-
parkAndCheckInterrupt():调用LockSupport.park()阻塞线程,唤醒后返回中断标记。
-
unparkSuccessor(Node node):唤醒当前节点的后继有效阻塞节点,跳过失效取消节点。
-
doReleaseShared():共享模式传播唤醒,持续向后唤醒共享节点,实现批量放行。
7.4.4 工具辅助方法(状态判断)
-
getState():获取同步状态变量state值。
-
setState(int newState):直接修改state值,无CAS,适用于线程安全场景。
-
compareAndSetState(int expect, int update):CAS无锁修改state,底层Unsafe实现,保障并发安全。
-
setExclusiveOwnerThread(Thread thread):设置独占锁持有线程。
7.4.5 面试满分必背总结(8句)
-
AQS方法分为公共调用、子类重写、底层私有三类,职责划分清晰;
-
独占核心:acquire/release,共享核心:acquireShared/releaseShared;
-
自定义锁只需重写少量模板方法,通用排队逻辑AQS封装;
-
addWaiter快速入队,enq处理队列为空初始化场景;
-
shouldParkAfterFailedAcquire清洗失效节点,优化队列结构;
-
parkAndCheckInterrupt完成线程阻塞,依赖LockSupport工具;
-
共享模式doReleaseShared实现传播唤醒,区别于独占单次唤醒;
-
CAS修改state保障无锁并发,是AQS线程安全的底层基石。
7.5 核心原理
-
失败线程进入队列排队
-
头节点唤醒后继节点
-
共享锁唤醒传播机制
-
CLH 队列排队机制
7.6 依赖 AQS 实现类
AQS是JUC并发核心基石,JUC包下绝大多数锁、并发工具类底层均依托AQS实现,分为独占锁实现类、共享锁实现类、混合锁实现类、其他衍生工具类,下面分类详解,标注AQS抢占模式、底层原理、面试核心考点:
7.6.1 独占模式实现类(排他加锁、单线程占用)
-
ReentrantLock:可重入独占锁,基于AQS实现公平/非公平锁,依靠state记录重入次数,生产最常用显式锁;
-
ReentrantReadWriteLock.WriteLock:读写锁中的写锁,排他独占模式,写操作互斥,保证数据修改安全;
-
StampedLock.WriteLock:乐观读写锁的写锁,独占模式,适用于低并发写入场景。
7.6.2 共享模式实现类(多线程并发、资源共享)
-
Semaphore:信号量,共享模式,state代表可用许可数,用于接口限流、资源抢占、控制最大并发数;
-
CountDownLatch:倒计时计数器,共享模式,state为剩余倒计时次数,主线程等待子线程全部执行完毕,不可重置;
-
CyclicBarrier:循环屏障,基于AQS+ReentrantLock实现,共享排队机制,线程互相等待集齐后批量执行,支持重置;
-
ReentrantReadWriteLock.ReadLock:读写锁中的读锁,共享模式,多线程可同时读,读操作无互斥,提升读并发性能;
-
StampedLock.ReadLock:乐观读锁,无锁优化,共享模式,适用于读多写少、无数据一致性强校验场景。
7.6.3 独占+共享混合实现类
-
ReentrantReadWriteLock :经典混合锁,写锁独占、读锁共享,底层维护两个AQS同步器,实现读写分离;支持锁降级(写锁降级为读锁),不支持锁升级。
-
StampedLock:改进版读写锁,混合模式,包含悲观读、写锁、乐观读三种模式,无重入特性,高并发读写性能优于普通读写锁。
7.6.4 其他AQS衍生并发工具类
-
Phaser:阶段同步屏障,基于AQS优化实现,整合CountDownLatch与CyclicBarrier特性,支持多阶段分批执行、动态增减线程;
-
SynchronousQueue:同步阻塞队列,底层依托AQS实现线程一对一传递数据,无容量、不存储元素,常用于线程池瞬时任务中转;
-
LinkedBlockingQueue:无界阻塞队列,内部使用ReentrantLock(AQS实现)保证入队、出队线程安全,分离读写锁提升并发能力。
7.6.5 面试必背总结(6句满分话术)
-
AQS分为独占、共享、混合三大实现模式,支撑全部JUC核心工具;
-
独占代表ReentrantLock,适合互斥业务,单线程占用资源;
-
共享代表Semaphore、CountDownLatch,用于限流、线程协同;
-
读写锁为混合模式,写独占读共享,适配读多写少业务;
-
阻塞队列、阶段屏障底层均依赖AQS实现线程排队阻塞;
-
所有实现类核心逻辑:state管控资源、CLH队列排队阻塞。
第八部分 七大 JUC 并发工具类
简洁:
-
CountDownLatch:倒计时计数器,主线程等待多线程完成,不可重置
-
CyclicBarrier:循环屏障,线程互相等待集齐再执行,可重置、支持屏障回调
-
Semaphore:信号量,控制并发数量、限流、资源抢占
-
Phaser:阶段屏障,多阶段分批协同执行
-
Exchanger:双线程数据交换
-
CompletableFuture:异步编排、任务组合、回调处理
-
ForkJoinPool:分支合并池,工作窃取算法,适合大数据拆分计算
8.1 CountDownLatch 倒计时计数器(减法计数器)
8.1.1 核心概念
作用:维护一个递减计数器,主线程等待子线程全部执行完毕,计数器归零后主线程放行。
底层原理:基于AQS共享模式,state为初始计数,每调用countDown()一次state-1;await()阻塞主线程,直到state=0唤醒。
核心特点 :不可重置、一次性使用、减法计数、主线程等待子线程。
8.1.2 核心API
-
CountDownLatch(int count):构造方法,初始化计数器数量
-
countDown():计数器减一,无阻塞,执行即递减
-
await():阻塞当前线程,直到计数器归零
-
await(long time, TimeUnit unit):限时等待,超时自动放行
-
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 高频易错点
-
countDown() 必须放入finally,防止异常导致计数器无法递减,主线程永久阻塞;
-
计数器归零后不可重置,只能一次性使用;
-
仅能控制主线程等待子线程,无法实现线程互相等待;
-
await()可中断,会抛出中断异常。
8.1.5 面试满分总结
-
底层AQS共享模式,state存储剩余计数;
-
减法计数器、不可重置、一次性消费;
-
适用并行任务汇总、资源批量加载;
-
finally执行countDown,杜绝线程卡死。
8.2 CyclicBarrier 循环屏障(加法计数器)
8.2.1 核心概念
作用:加法计数器,线程之间互相等待,集齐指定数量线程后统一批量放行,支持循环复用。
底层原理:基于ReentrantLock+Condition,维护计数阈值,线程到达屏障点阻塞,集齐数量后批量唤醒,自动重置计数器。
核心特点 :可循环复用、加法计数、线程互相等待、支持屏障回调任务。
8.2.2 核心API
-
CyclicBarrier(int parties):初始化等待线程数量
-
CyclicBarrier(int parties, Runnable barrierAction):集齐线程后执行回调任务
-
await():线程到达屏障,阻塞等待集齐线程
-
reset():手动重置屏障,主动清空计数
-
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 高频易错点
-
任意线程中断,屏障直接破损,所有阻塞线程抛出异常;
-
自动循环重置,无需手动赋值,适合批量轮询任务;
-
回调任务由最后一个到达的线程执行;
-
不可设置超时永久等待,需手动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
-
Semaphore(int permits):初始化许可数,默认非公平
-
Semaphore(int permits, boolean fair):设置公平/非公平锁
-
acquire():阻塞获取1个许可,无许可则等待
-
release():归还1个许可
-
tryAcquire():非阻塞尝试获取,失败直接返回false
-
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 高频易错点
-
release()必须放入finally,防止许可丢失,导致永久限流;
-
默认非公平锁,吞吐量更高,生产优先使用;
-
支持批量获取/归还许可(acquire(int));
-
不可设置负数许可,初始化必须大于等于0。
8.3.5 面试满分总结
-
AQS共享实现,state存储许可数量;
-
核心用于限流、资源抢占、连接池管控;
-
默认非公平,吞吐量优于公平锁;
-
acquire占用、release归还,成对使用。
8.4 Phaser 阶段同步屏障(进阶屏障)
8.4.1 核心概念
作用 :整合CountDownLatch与CyclicBarrier特性,支持多阶段分批执行、动态增减线程、分层等待,JDK7推出进阶同步工具。
底层原理:基于优化版AQS,分阶段存储线程状态,支持注册、抵达、等待、进阶下一阶段。
核心特点:动态线程数、多阶段执行、支持批量注册、无需提前固定线程数量。
8.4.2 核心API
-
register():动态注册单个线程
-
bulkRegister(int parties):批量注册线程
-
arrive():线程抵达当前阶段,不阻塞
-
arriveAndAwaitAdvance():抵达并阻塞,等待同阶段线程集齐进阶
-
getPhase():获取当前执行阶段
-
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
-
exchange(V x):传入数据,阻塞等待配对交换
-
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 局限性与使用场景
-
局限性:只能两个线程交换,多线程会随机配对,数据错乱;
-
适用场景:双人通信、缓冲区数据交换、简单数据对接。
8.6 CompletableFuture 异步编排工具(生产高频)
8.6.1 核心概念
作用 :JDK1.8推出,替代Future,实现异步回调、任务串行、并行、异常捕获、多任务组合,彻底解决Future阻塞get()痛点。
底层原理:基于线程池+回调钩子,无阻塞异步执行,任务完成自动触发回调。
8.6.2 核心API分类
① 创建异步任务
-
supplyAsync():有返回值异步任务(常用)
-
runAsync():无返回值异步任务
② 串行回调执行
-
thenApply():接收上一步结果,处理后返回新结果
-
thenAccept():接收结果,无返回值
-
thenRun():不接收结果,单纯执行后置任务
③ 多任务组合
-
allOf():所有任务全部完成,才触发回调
-
anyOf():任意一个任务完成,立即触发回调
-
thenCombine():两个任务结果合并处理
④ 异常处理
-
exceptionally():异常兜底回调
-
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 生产避坑规范
-
默认使用ForkJoinPool公共线程池,IO密集型业务自定义线程池隔离;
-
禁止频繁get()阻塞获取结果,优先回调处理;
-
必须加exceptionally异常兜底,防止异步任务静默报错;
-
allOf适合批量接口并行查询,大幅缩短耗时。
8.7 ForkJoinPool 分支合并池(大数据计算)
8.7.1 核心概念
作用 :JDK1.7推出,专为大数据拆分计算设计,采用分治思想+工作窃取算法,将大任务拆分为小任务,递归执行,最后合并结果。
核心特性:工作窃取、递归拆分、低开销、适合CPU密集型批量计算。
8.7.2 核心组成
-
ForkJoinPool:分支合并线程池,管理任务执行
-
ForkJoinTask:抽象任务类,提供fork()拆分、join()合并
-
RecursiveTask:有返回值递归任务(常用)
-
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 工作窃取算法原理
-
每个线程维护双端队列,存放拆分的子任务;
-
空闲线程从繁忙线程队列尾部窃取任务执行;
-
减少线程空闲等待,最大化利用CPU资源;
-
仅适合CPU密集型,禁止IO阻塞任务。
8.8 七大工具类终极面试对比总结(必背一页纸)
|-------------------|---------|---------|---------------|--------------|
| 工具类 | 核心作用 | 计数方式 | 底层实现 | 适用场景 |
| CountDownLatch | 主线程等子线程 | 减法、不可重置 | AQS共享 | 批量资源加载、任务汇总 |
| CyclicBarrier | 子线程互相等待 | 加法、可循环 | ReentrantLock | 组队任务、批量轮询 |
| Semaphore | 控制并发限流 | 许可、可复用 | AQS共享 | 接口限流、连接池管控 |
| Phaser | 多阶段分层执行 | 动态、多阶段 | 优化AQS | 复杂分层、动态线程 |
| Exchanger | 双线程数据交换 | 成对匹配 | CAS无锁 | 双人通信、数据对接 |
| CompletableFuture | 异步任务编排 | 任务链式 | 线程池+回调 | 接口异步、多任务组合 |
| ForkJoinPool | 大数据拆分计算 | 递归拆分 | 工作窃取算法 | 海量数据、CPU密集计算 |
8.1 CompletableFuture 核心
-
supplyAsync:有返回值异步
-
runAsync:无返回值异步
-
thenApply、thenAccept、thenRun 串行执行
-
allOf 全部完成、anyOf 任意一个完成
-
自定义线程池隔离,避免共用公共池阻塞
第九部分 线程池(企业核心重点)
9.1 五大线程池运行状态
RUNNING → SHUTDOWN → STOP → TIDYING → TERMINATED,五大状态不可逆流转,由线程池内部ctl复合变量(线程数+运行状态)管控,下面逐状态详解、标注流转条件、线程行为、源码考点。
9.1.1 五大运行状态详细解析(面试必背)
-
RUNNING(运行状态) 触发条件 :线程池初始化创建完成,默认进入RUNNING状态; 核心权限 :接收新任务、处理阻塞队列等待任务、执行正在运行任务; 底层标识 :ctl高位存储状态,RUNNING=-536870912; 业务场景:正常对外提供线程调度服务。
-
SHUTDOWN(关闭状态) 触发条件 :调用 shutdown() 平缓关闭方法; 核心权限 :拒绝接收新任务 ,继续执行队列中积压任务、执行正在运行任务; 行为特征 :不会中断活跃线程,优雅平滑收尾存量任务; 流转条件:队列任务全部执行完毕、工作线程数归零,进入TIDYING。
-
STOP(停止状态) 触发条件 :调用 shutdownNow() 强制关闭方法; 核心权限 :拒绝新任务、丢弃队列未执行任务、强制中断正在执行任务的线程 ; 行为特征 :遍历工作线程,执行interrupt()中断标记,终止运行中任务; 流转条件:所有中断线程执行完毕,工作线程数为0,进入TIDYING。
-
TIDYING(整理状态) 触发条件 :SHUTDOWN/STOP执行完毕,线程池无存活工作线程、无积压任务; 核心权限 :中间过渡状态,无任务、无线程,不可接收任何任务; 行为特征 :执行内部钩子方法 terminated() ,可自定义线程池销毁后置逻辑; 流转条件:terminated()钩子方法执行完成,进入终止状态。
-
TERMINATED(终止状态) 触发条件 :整理状态执行完毕; 核心权限 :线程池彻底死亡,永久不可复用; 行为特征 :所有资源释放、线程销毁、队列清空; 注意事项:终止后的线程池无法再次提交任务,直接抛出异常。
9.1.2 状态完整流转链路(不可逆)
链路1(平缓关闭):RUNNING → shutdown() → SHUTDOWN → 队列&线程清空 → TIDYING → terminated()执行 → TERMINATED
链路2(强制关闭):RUNNING → shutdownNow() → STOP → 全部线程中断销毁 → TIDYING → terminated()执行 → TERMINATED
9.1.3 高频面试易错点
-
状态不可逆:线程池状态只能单向流转,一旦进入SHUTDOWN/STOP,无法回退RUNNING;
-
ctl复合变量:JUC线程池用一个int变量ctl,高3位存状态、低29位存线程数,节省内存;
-
shutdown与shutdownNow核心区别:前者不丢任务、不中断运行线程;后者丢弃队列任务、强制中断线程;
-
terminated钩子方法:默认空实现,可重写用于销毁资源、打印日志、监控上报;
-
终止后提交任务: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 线程池执行完整流程(面试必考流程图话术)
-
提交任务,判断当前核心线程数 < 核心线程数:新建核心线程执行任务;
-
核心线程已满,判断阻塞队列是否未满:任务入队排队等待;
-
队列已满,判断当前线程数 < 最大线程数:新建非核心线程执行任务;
-
线程数达到最大值、队列已满:触发拒绝策略;
-
任务执行完毕,非核心线程空闲超时,自动回收销毁。
9.2.3 高频易错坑点(生产踩坑)
-
不要混淆核心线程与最大线程:只有队列满了才会扩容非核心线程;
-
无界队列会导致最大线程数失效:任务无限堆积,引发OOM内存溢出;
-
默认线程工厂无线程名,线上排查堆栈无法定位业务;
-
keepAliveTime仅作用非核心线程,默认不回收核心线程;
-
七大参数赋值必须合法:核心线程数不能大于最大线程数。
9.2.4 面试满分背诵总结(6句极简话术)
-
七大参数:核心数、最大数、超时时间、时间单位、阻塞队列、线程工厂、拒绝策略;
-
执行顺序:核心线程→阻塞队列→非核心线程→拒绝策略;
-
非核心线程超时回收,核心线程默认常驻;
-
生产必须用有界队列,杜绝无界队列OOM;
-
自定义线程工厂,方便线上故障溯源;
-
拒绝策略适配业务,禁止直接抛异常。
9.3 六大常用阻塞队列(面试高频+生产选型)
阻塞队列是线程池核心存储容器,属于JUC包下线程安全队列,自带阻塞入队、阻塞出队特性;当队列满时写入线程阻塞,队列空时读取线程阻塞,天然适配生产者消费者模型。六大队列覆盖绝大多数业务场景,下面逐个详解底层、特性、优缺点、生产用法、代码示例。
简洁:
-
AbortPolicy:直接抛异常(默认)
-
CallerRunsPolicy:主线程执行任务
-
DiscardPolicy:丢弃当前任务
-
DiscardOldestPolicy:丢弃队列最久任务
-
自定义拒绝策略
-
限流降级策略
9.3.1 ArrayBlockingQueue(有界数组阻塞队列)
-
底层结构:固定长度数组,初始化必须指定容量,长度不可变
-
锁机制:全局唯一ReentrantLock(生产一把锁,读写互斥)
-
阻塞条件:队列满,put()阻塞;队列空,take()阻塞
-
排序规则:先进先出FIFO,有序存储
-
优点:结构简单、内存连续、无扩容开销、线程安全
-
缺点:读写互斥、并发吞吐量低、容量固定不可动态扩容
-
生产场景:固定并发量、任务波动小、低并发业务线程池
-
面试坑点:初始化必须传容量,无无参构造;一把锁导致读写不能并行
java
// 固定容量为5的有界阻塞队列
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(5);
9.3.2 LinkedBlockingQueue(无界/有界链表阻塞队列)
-
底层结构:单向链表结构,节点动态新增销毁
-
锁机制:两把ReentrantLock(写入锁、读取锁分离,读写并行)
-
容量规则:无参构造默认容量Integer.MAX_VALUE(近乎无界),有参可指定容量
-
排序规则:先进先出FIFO
-
优点:读写锁分离、并发吞吐量高、链表动态扩容、无固定长度限制
-
缺点:无参无界队列极易堆积任务引发OOM;频繁创建节点造成GC压力
-
生产场景:newFixedThreadPool、newSingleThreadExecutor底层队列;常规业务异步任务
-
面试坑点 :生产禁止使用无参构造,必须手动指定容量,防止无限堆积OOM
java
// 手动指定容量,避免无界OOM
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(100);
9.3.3 SynchronousQueue(同步移交队列|无容量)
-
底层结构 :无数组、无链表,容量永久为0
-
存储特性:不存储任何任务,生产者提交任务必须等待消费者消费,一对一移交
-
锁机制:CAS无锁+栈/队列算法,高性能移交
-
模式选择:默认非公平栈模式,可设置公平队列模式
-
优点:无任务堆积、实时响应、吞吐量极高、无内存占用
-
缺点:无缓冲、生产者必须阻塞等待消费
-
生产场景:newCachedThreadPool底层队列;瞬时高并发、短耗时任务、无积压需求
-
面试坑点:不能调用peek()获取元素,永远返回null;无容量,put后必须等待take
java
// 同步移交队列,不存任务,一对一传递
SynchronousQueue<String> queue = new SynchronousQueue<>();
9.3.4 DelayQueue(延时阻塞队列|定时任务)
-
底层结构:优先级队列+可延迟元素,底层基于最小堆
-
元素要求:元素必须实现Delayed接口,重写延时时间方法
-
出队规则:只有元素延时时间到期,才能被取出;未到期则阻塞
-
锁机制:ReentrantLock独占锁
-
优点:天然支持延时任务、优先级排序、无需额外定时线程
-
缺点:排序消耗CPU、元素实现复杂、不适合高频瞬时任务
-
生产场景:订单超时关闭、红包过期、延时重试、缓存失效
-
面试坑点:无到期元素时,take()永久阻塞;不允许存储null元素
java
// 延时队列,元素必须实现Delayed接口
DelayQueue<DelayedTask> delayQueue = new DelayQueue<>();
9.3.5 PriorityBlockingQueue(优先级阻塞队列)
-
底层结构:可变长度数组,最小堆排序算法
-
排序规则:自定义Comparator比较器,优先级高的元素优先出队,无序入队、有序出队
-
容量特性:无界队列,自动扩容,初始容量11
-
锁机制:全局ReentrantLock独占锁
-
优点:支持任务优先级、自动扩容、高优先级任务优先执行
-
缺点:排序耗时、扩容消耗内存、低优先级线程容易饥饿
-
生产场景:消息优先级推送、紧急任务插队、权重排序业务
-
面试坑点:无界队列有OOM风险;非FIFO,打破先进先出规则
java
// 自定义比较器,实现任务优先级排序
PriorityBlockingQueue<Task> queue = new PriorityBlockingQueue<>(11, Comparator.comparing(Task::getLevel));
9.3.6 LinkedTransferQueue(无锁高效转移队列)
-
底层结构:CAS无锁单向链表,JDK1.7新增高性能队列
-
核心特性:融合SynchronousQueue+LinkedBlockingQueue优势,支持批量转移、预占节点
-
关键方法:transfer(),生产者直接把元素转移给消费者,无消费者则阻塞
-
锁机制:全程CAS无锁,无悲观锁开销
-
优点:并发性能天花板、无锁开销、支持批量操作、吞吐量极高
-
缺点:源码复杂、日常业务使用频率低、调试难度大
-
生产场景:超高并发中间件、网关转发、海量异步消息流转
-
面试坑点: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 生产队列选型黄金规则
-
常规业务:优先带容量LinkedBlockingQueue,读写分离、性能均衡;
-
瞬时高并发:SynchronousQueue,无积压、实时移交;
-
定时过期任务:DelayQueue,无需额外定时器;
-
有优先级需求:PriorityBlockingQueue,紧急任务插队;
-
超高并发中间件:LinkedTransferQueue,无锁高性能;
-
固定少量并发:ArrayBlockingQueue,结构简单易维护。
9.4 六大拒绝策略(源码详解+生产场景+代码)
线程池在核心线程已满、阻塞队列已满、最大线程数打满的饱和状态下,新提交任务会触发拒绝策略。JDK原生提供4种拒绝策略,额外扩展2种企业常用自定义策略,合称六大拒绝策略,下面逐个详解底层源码、执行逻辑、优缺点、适用场景,附带实操代码。
9.4.1 AbortPolicy(直接抛异常|JDK默认策略)
-
执行逻辑 :直接抛出RejectedExecutionException运行时异常,中断任务提交,拒绝执行新任务。
-
源码原理:判断线程池非运行状态,直接throw异常,无任何兜底处理。
-
优点:报错直观、快速感知线程池饱和,及时发现并发压力。
-
缺点:直接报错、中断业务,无容错能力,容易导致接口报错。
-
适用场景:后台定时任务、非核心业务、需要严格监控异常的任务。
-
生产禁忌:禁止用于用户直连接口,会直接抛出异常影响用户体验。
java
// 默认拒绝策略:AbortPolicy
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
9.4.2 CallerRunsPolicy(调用者执行策略|主线程兜底)
-
执行逻辑 :线程池饱和后,新任务由提交任务的主线程执行,不丢弃、不报错。
-
源码原理:判断线程池运行状态,调用任务run()方法,直接在调用线程执行。
-
优点:无任务丢失、无异常抛出,简单兜底,保证任务一定执行。
-
缺点:阻塞主线程、拖慢接口响应速度,吞吐量急剧下降。
-
适用场景:任务不允许丢失、对响应耗时不敏感、低优先级业务。
java
// 调用者执行策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
9.4.3 DiscardPolicy(静默丢弃策略|无感知舍弃)
-
执行逻辑 :线程池饱和,直接静默丢弃当前提交的新任务,不报错、无日志、无提醒。
-
源码原理:空实现,拒绝方法内无任何代码,直接舍弃任务。
-
优点:无异常、无阻塞、性能损耗极低。
-
缺点:任务丢失无感知,线上故障难以排查,存在数据遗漏风险。
-
适用场景:可丢弃的日志埋点、统计上报、非核心冗余任务。
java
// 静默丢弃策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy()
);
9.4.4 DiscardOldestPolicy(丢弃最旧任务|留存新任务)
-
执行逻辑 :线程池饱和,丢弃队列头部存活最久、未执行的旧任务,腾出空间执行当前新任务。
-
源码原理:获取队列,poll()删除队首旧任务,再次尝试提交当前新任务。
-
优点:优先保留最新任务,适配时效性强的业务。
-
缺点:旧任务无提醒丢失,任务顺序混乱,不适合有序业务。
-
适用场景:实时性要求高、旧数据无意义的业务(如实时推送、实时监控)。
java
// 丢弃最旧任务策略
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy()
);
9.4.5 自定义拒绝策略(企业通用|个性化兜底)
-
执行逻辑:实现RejectedExecutionHandler接口,重写拒绝方法,自定义兜底逻辑。
-
常用拓展:任务持久化入库、打印告警日志、推送监控报警、重试机制。
-
优点:高度灵活、适配业务、可溯源、无莫名丢失任务。
-
缺点:需要手动编码,开发成本略高。
-
适用场景:绝大多数线上生产业务,企业级标准规范。
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 限流降级策略(高并发专属|大厂方案)
-
执行逻辑:结合令牌桶、漏桶算法,线程池饱和时触发流量降级,返回友好提示、熔断接口。
-
底层原理:整合Sentinel、Resilience4j限流组件,超出阈值直接熔断,保护服务不雪崩。
-
优点:服务熔断降级、防止雪崩、保护核心接口、用户体验友好。
-
缺点:需要引入限流组件,架构复杂度提升。
-
适用场景:秒杀、抢购、网关、高并发流量接口。
9.4.7 六大拒绝策略终极对比+生产选型(必背)
|---------------------|-----------|------------|--------------|
| 拒绝策略 | 核心行为 | 优缺点 | 生产适用场景 |
| AbortPolicy | 直接抛出异常 | 报错直观、影响业务 | 后台定时、非核心任务 |
| CallerRunsPolicy | 主线程执行任务 | 无丢失、阻塞主线程 | 低优先级、不可丢任务 |
| DiscardPolicy | 静默丢弃新任务 | 无报错、任务易丢失 | 日志埋点、冗余统计任务 |
| DiscardOldestPolicy | 丢弃队列最旧任务 | 保留新任务、顺序混乱 | 实时推送、监控时效性任务 |
| 自定义拒绝策略 | 告警+持久化+重试 | 灵活可控、可溯源 | 绝大多数线上业务(推荐) |
| 限流降级策略 | 熔断降级、流量管控 | 防雪崩、架构复杂 | 秒杀、网关、高并发接口 |
9.4.8 面试满分总结(7句背诵话术)
-
JDK原生4种拒绝策略:抛异常、主线程执行、丢弃新任务、丢弃旧任务;
-
AbortPolicy是默认策略,直接抛出拒绝异常;
-
DiscardPolicy静默丢失任务,无任何日志,生产慎用;
-
DiscardOldestPolicy淘汰队列头部旧任务,适配实时业务;
-
CallerRunsPolicy不丢任务,但会阻塞主线程;
-
生产优先自定义拒绝策略,做日志告警+任务持久化;
-
超高并发接口采用限流降级,防止服务雪崩。
9.5 内置四大线程池(生产禁止使用)
JDK通过Executors工具类封装4种便捷线程池,底层全部存在致命缺陷,阿里巴巴开发手册强制禁止生产使用,仅适用于测试、学习、本地简单demo,下面逐个拆解底层参数、源码、优缺点、禁用原因、适用场景。
9.5.1 newFixedThreadPool(固定线程池)
-
创建方式:创建固定线程数量的线程池,线程永久存活
-
底层源码参数:核心线程数=自定义固定值、最大线程数=核心线程数、无空闲回收时间、队列=无界LinkedBlockingQueue
-
执行特点:线程数量恒定,不会扩容、不会回收,任务无限存入队列
-
优点:线程可控、执行有序、无频繁创建销毁线程开销
-
致命缺点 :使用无界队列,高并发下任务无限堆积,内存持续飙升,触发OOM内存溢出
-
生产禁用原因:无任务上限,无法触发拒绝策略,海量任务积压打爆JVM内存
-
适用场景:任务量平稳、并发量极低、测试环境串行批量任务
java
// 固定3条工作线程
ExecutorService fixedPool = Executors.newFixedThreadPool(3);
9.5.2 newSingleThreadExecutor(单一线程池)
-
创建方式:内部仅有1条工作线程,串行执行所有任务
-
底层源码参数:核心线程数=1、最大线程数=1、无空闲回收时间、队列=无界LinkedBlockingQueue
-
执行特点:严格串行执行,任务排队依次执行,不会并发错乱
-
优点:单线程执行,无线程安全竞争,任务执行有序
-
致命缺点 :同样使用无界队列,海量任务积压引发OOM;单线程执行效率极低,吞吐量差
-
生产禁用原因:无任务上限、无拒绝策略,高并发极易内存溢出,且无法利用多核CPU
-
适用场景:简单串行任务、日志顺序打印、单机极简同步任务
java
// 仅有一条工作线程,串行执行
ExecutorService singlePool = Executors.newSingleThreadExecutor();
9.5.3 newCachedThreadPool(缓存线程池)
-
创建方式:无固定线程数,按需创建线程,空闲线程自动回收
-
底层源码参数:核心线程数=0、最大线程数=Integer.MAX_VALUE、空闲存活时间60秒、队列=SynchronousQueue同步移交队列
-
执行特点:任务到来无空闲线程则新建线程,线程空闲60秒自动销毁,实时移交任务无队列堆积
-
优点:响应速度快、瞬时并发能力强、空闲线程自动回收
-
致命缺点 :最大线程数近乎无限,高并发瞬间疯狂创建线程,线程数量打爆操作系统上限,造成CPU飙高、线程栈溢出、服务卡死
-
生产禁用原因:无线程数量限制,恶意流量/突发流量瞬间创建上千条线程,操作系统线程调度崩溃
-
适用场景:大量短耗时、瞬时突发、无压力测试任务
java
// 可无限创建线程,空闲线程60s回收
ExecutorService cachedPool = Executors.newCachedThreadPool();
9.5.4 newScheduledThreadPool(定时线程池)
-
创建方式:支持延迟执行、周期性循环执行的定时线程池
-
底层源码参数:核心线程数=自定义、最大线程数=Integer.MAX_VALUE、队列=延时无界DelayedWorkQueue
-
执行特点:支持延迟执行、固定频率执行、固定间隔执行,适配定时任务
-
优点:自带定时调度能力,无需手动封装延时逻辑
-
致命缺点:最大线程数无上限、队列无界,异常定时任务堆积、线程无限创建,引发OOM+线程溢出
-
生产禁用原因:边界不可控,异常任务持续堆积,内存泄露、线程泛滥风险极高
-
适用场景:本地简单定时测试、非线上业务延时任务
java
// 核心线程数2,无限扩容非核心线程
ScheduledExecutorService scheduledPool = Executors.newScheduledThreadPool(2);
// 延迟3秒执行任务
scheduledPool.schedule(()-> System.out.println("定时任务执行"),3,TimeUnit.SECONDS);
9.5.5 四大内置线程池致命缺陷汇总(面试必背)
-
Fixed/Single:无界阻塞队列,任务无限积压,触发OOM内存溢出;
-
Cached/Scheduled:最大线程数MAX_VALUE,无限创建线程,耗尽系统线程资源;
-
全部无自定义拒绝策略,饱和后无兜底,线上故障无法降级;
-
默认无线程名称、无异常捕获,线上故障无法溯源排查;
-
阿里规范硬性约束:禁止使用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 业务线程数配置公式(生产完整版|面试必考)
常规简易公式(入门背诵,通用基础配置)
-
CPU 密集型:核心线程数 = CPU 核心数 + 1 适用场景 :大量计算、加密解密、循环逻辑、无IO阻塞、CPU持续高占用 设计逻辑:多出1条线程兜底,防止线程偶然阻塞导致CPU空转,最大化利用CPU算力
-
IO 密集型:核心线程数 = CPU 核心数 * 2 适用场景 :数据库查询、Redis缓存、网络请求、文件读写、接口调用 设计逻辑:IO阻塞时线程休眠,多线程可复用CPU,提升吞吐量
进阶精准公式(企业生产调优|核心必背)
通用公式:核心线程数 = CPU核心数 / (1 - 阻塞系数)
-
阻塞系数β:线程阻塞时间 / 线程总执行时间,取值范围 0~1
-
CPU密集型:阻塞系数0~0.2,极少阻塞,线程专注计算
-
普通IO密集型:阻塞系数0.5左右,一半时间阻塞等待IO
-
重度IO密集型:阻塞系数0.8~0.9,大部分时间阻塞(如慢SQL、第三方接口)
9.6.1 公式实战计算案例
-
案例1:8核CPU,CPU密集型任务,阻塞系数0.1 核心线程数 = 8 / (1 - 0.1) ≈ 9,贴合简易公式【核心数+1】
-
案例2:8核CPU,普通IO任务,阻塞系数0.5 核心线程数 = 8 / (1 - 0.5) = 16,贴合简易公式【核心数*2】
-
案例3:8核CPU,重度IO慢接口,阻塞系数0.8 核心线程数 = 8 / (1 - 0.8) = 40,需大幅扩容线程,适配长时间阻塞任务
9.6.2 最大线程数配置规范
-
CPU密集型:最大线程数 = 核心线程数(不扩容,避免CPU上下文切换)
-
普通IO密集型:最大线程数 = 核心线程数 * 1.5 ~ 2(预留扩容余量)
-
重度IO密集型:最大线程数 = 核心线程数 * 2 ~ 3(应对流量峰值)
9.6.3 生产特殊场景优化配置
-
机器配置受限:低配置服务器,核心线程数下调20%,防止CPU打满告警
-
延迟敏感业务:优先调大核心线程数,减少任务排队延迟,牺牲少量CPU换响应速度
-
非核心兜底任务:缩小线程数、加大队列容量,降低资源占用,不抢占核心业务资源
-
多线程池共存:多个业务线程池总和,核心线程总数不超过CPU核心数*5,避免线程泛滥
9.6.4 面试满分总结(6句背诵)
-
简易公式:CPU密集+1,IO密集乘2;
-
精准公式:核心数=CPU核数/(1-阻塞系数);
-
阻塞系数越大、IO阻塞越久,需要线程数越多;
-
CPU密集严控线程数,防止上下文切换;
-
IO密集适当扩容,利用阻塞时间提升吞吐量;
-
线上优先压测调优,公式仅作初始参考基准。
9.7 线程池关闭
-
shutdown ():平缓关闭,执行完队列任务
-
shutdownNow ():强制关闭,中断正在执行任务
9.8 线程池踩坑点
- 坑点1:线程池内部异常静默丢失(高频踩坑) 问题现象 :线程池执行任务抛出异常,无控制台打印、无业务报错,线上悄无声息任务失败,难以排查; 产生原因 :ThreadPoolExecutor内部捕获任务异常,未主动向外抛出,普通无返回值任务异常直接被吞; 解决方案:①任务内部手动try-catch捕获异常并打印日志;②自定义线程工厂设置全局异常处理器UncaughtExceptionHandler;③使用Future接收返回值,get()方法捕获执行异常。
java
// 全局异常处理器,防止线程池异常丢失
new ThreadFactoryBuilder()
.setNameFormat("business-pool-%d")
.setUncaughtExceptionHandler((thread,throwable)->{
System.err.println("线程池任务异常:"+throwable.getMessage());
}).build();
-
坑点2:核心线程无空闲回收,长期占用资源 问题现象 :业务低峰期无任务,核心线程常驻内存,一直占用线程资源,造成资源浪费; 底层原理 :默认配置下allowCoreThreadTimeOut=false,仅非核心线程执行超时回收,核心线程永久存活; 解决方案 :低流量、间歇性业务,开启参数
allowCoreThreadTimeOut(true),让核心线程空闲超时自动销毁,节省服务器资源。 -
坑点3:线程池无监控,线上故障无法溯源 问题现象 :线上任务积压、线程卡死、CPU飙高,无法快速定位线程池状态、堆积任务数、活跃线程数; 监控核心指标 :活跃线程数、完成任务数、队列积压数量、最大并发线程数、拒绝任务次数; 解决方案:自定义监控定时打印线程池指标,接入Prometheus+Grafana可视化监控,队列积压阈值触发告警。
java
// 线程池核心监控指标
System.out.println("活跃线程数:"+pool.getActiveCount());
System.out.println("队列积压数:"+pool.getQueue().size());
System.out.println("已完成任务:"+pool.getCompletedTaskCount());
-
坑点4:定时线程池ScheduledThreadPool任务堆积串行阻塞 问题现象 :单个定时任务执行耗时过长,阻塞后续定时任务,任务执行间隔错乱、叠加积压; 底层原理 :scheduleAtFixedRate&scheduleWithFixedDelay底层单线程串行执行,上一个任务未结束,下一个任务无法执行; 解决方案:耗时定时任务单独开辟子线程执行,拆分任务,避免定时任务内部阻塞。
-
坑点5:线程池混用、业务耦合互相抢占资源 问题现象 :接口同步任务、异步日志、定时任务共用同一个线程池,高并发下核心业务被非核心任务阻塞,接口超时; 核心原则 :业务隔离、池隔离,核心业务、非核心业务、定时任务拆分独立线程池; 优化方案 :拆分核心业务池、异步兜底池、定时任务池,互不抢占线程资源,保障核心接口优先级。
-
坑点6:任务耗时过长,线程长期不释放 问题现象 :线程池内执行慢SQL、第三方超时接口、大文件IO,线程一直被占用,新任务大量积压; 解决方案:①给任务添加超时时间,使用try-catch+超时中断;②第三方接口设置连接超时、读取超时;③耗时任务单独隔离线程池。
-
坑点7:线程池忘记关闭,造成内存泄露 问题现象 :临时线程池、定时线程池使用完毕未关闭,常驻JVM,线程一直存活,内存缓慢泄漏; 适用场景 :临时批量处理任务、一次性异步任务、非全局常驻线程池; 解决方案:临时线程池使用try-finally,finally中执行shutdown平缓关闭,防止线程泄露。
-
坑点8:队列容量设置不合理引发故障 错误配置 :①队列容量过大,任务积压过多导致OOM;②队列容量过小,频繁触发拒绝策略,业务报错; 生产规范:常规业务队列容量设置50~200,瞬时高并发业务结合限流,队列缩小+扩容最大线程数。
-
坑点9:线程池优先级滥用无效 问题现象 :自定义线程优先级,认为高优先级线程优先执行,实际无效果; 底层原理 :Java线程优先级仅为JVM调度建议,操作系统不保证执行顺序,高并发下优先级失效; 避坑方案:不要依赖线程优先级做业务排序,优先级仅用于系统底层调度。
-
坑点10:Lambda任务无法排查堆栈 问题现象 :线程池直接提交Lambda匿名任务,线上线程堆栈无业务类名、无方法名,故障无法定位代码位置; 解决方案:自定义线程名称、拆分业务任务类,禁止大量匿名Lambda任务,方便堆栈排查。
第十部分 JUC 并发容器(完整版|面试+生产)
JUC并发容器位于java.util.concurrent包下,全部是线程安全 集合,专为高并发场景设计;区别于普通集合+Collections.synchronizedxxx包装类,JUC容器采用CAS无锁、分段锁、写时复制等优化,并发吞吐量碾压同步包装类。 核心设计思想:细分锁粒度、减少锁竞争、无锁CAS、读写分离,适配不同并发业务场景。
10.1 JUC容器分类总览
-
并发List:CopyOnWriteArrayList、CopyOnWriteArraySet
-
并发Map:ConcurrentHashMap、ConcurrentSkipListMap
-
并发队列:阻塞队列、非阻塞队列(前文线程池已详解阻塞队列)
-
并发Set:CopyOnWriteArraySet、ConcurrentSkipListSet
简洁:
-
CopyOnWriteArrayList / CopyOnWriteArraySet:写时复制,读极快,写开销大
-
ConcurrentHashMap:1.7 分段锁、1.8 数组 + 链表 + 红黑树,扩容迁移、helpTransfer
-
ConcurrentLinkedQueue:无锁高性能并发队列
-
ConcurrentSkipListMap:有序并发 Map
-
普通集合包装类 synchronizedMap 性能差,不推荐高并发使用
10.2 CopyOnWriteArrayList(写时复制数组列表)
10.2.1 底层原理(面试必考)
写时复制机制 :新增、修改、删除等写操作时,先拷贝原数组生成新数组,在新数组完成写操作,修改完毕将原数组引用指向新数组;读操作全程无锁,直接读取原数组。
10.2.2 核心特性
-
加锁方式:写操作加ReentrantLock独占锁,防止多线程并发写覆盖;读操作无锁。
-
数据一致性:最终一致性,非实时强一致;写操作未完成时,读线程依旧读取旧数组数据。
-
元素特性:允许存储null元素,可重复存储元素。
-
扩容机制:每次写操作拷贝数组,扩容无需单独触发,直接生成指定长度新数组。
10.2.3 优缺点详解
-
优点:读无锁、读取吞吐量极高、并发读性能碾压ArrayList+同步锁;遍历不会抛出并发修改异常。
-
缺点:写操作需要拷贝数组,内存占用翻倍、写开销极大;频繁增删改场景CPU消耗高。
10.2.4 生产适用场景
读多写少场景:配置信息、白名单、本地缓存、常量列表、极少修改的静态数据。
10.2.5 高频易错坑点
-
内存占用高:写操作永久存在新旧双数组,大数据量下极易占用堆内存。
-
数据弱一致性:写操作期间读取旧数据,无法满足实时强一致业务。
-
不适合频繁写:频繁增删会反复拷贝数组,引发频繁GC、CPU飙升。
-
迭代器不可修改:迭代器仅可读,不支持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 核心特性&;适用场景
-
继承写时复制所有特性:读无锁、写拷贝、弱一致性。
-
去重逻辑基于遍历判断,大数据量下查重效率极低。
-
生产场景:少量元素、读多写少、去重静态集合,如权限标识、功能开关集合。
10.3.3 致命缺点
addIfAbsent去重时间复杂度O(n),元素数量超过1000不推荐使用,查重耗时严重。
10.4 ConcurrentHashMap(并发哈希映射|面试重中之重)
JDK最常用并发容器,替代HashMap+同步锁,线程安全、并发性能极高;严格区分JDK1.7、JDK1.8底层实现,面试必背版本差异。
10.4.1 JDK1.7 底层原理
-
数据结构:Segment分段锁 + 数组 + 链表。
-
分段锁机制:默认16个Segment分段,每段独立ReentrantLock,不同分段互不阻塞,并发度=16。
-
扩容逻辑:单段独立扩容,不会全局重哈希,扩容仅影响当前分段。
-
查询时间:哈希冲突严重,链表过长,查询效率偏低。
10.4.2 JDK1.8 底层原理(主流版本)
-
数据结构 :数组 + 链表 + 红黑树,废弃分段锁。
-
加锁方式 :CAS乐观锁 + synchronized内置锁,锁粒度细化到数组桶位。
-
树化阈值:链表长度≥8 && 数组容量≥64,链表转为红黑树;链表长度≤6,树退化为链表。
-
扩容机制:全局扩容,多线程协助迁移(helpTransfer),并发扩容减少阻塞。
-
哈希算法:优化扰动函数,减少哈希碰撞,高低位混合哈希。
10.4.3 JDK1.7 & 1.8 核心区别(必背8条)
-
数据结构:1.7分段锁+链表;1.8数组+链表+红黑树。
-
锁实现:1.7 ReentrantLock;1.8 synchronized+CAS。
-
锁粒度:1.7分段;1.8桶位,粒度更细、并发更高。
-
扩容:1.7单段扩容;1.8多线程协助迁移。
-
树化:1.7无红黑树;1.8链表超长转红黑树。
-
空值:两个版本key、value均不允许为null。
-
初始化:1.7默认容量16*16;1.8默认容量16,负载因子0.75。
-
并发度:1.7固定16;1.8无固定并发度,依托桶位锁。
10.4.4 核心高频考点
-
为什么不用ReentrantLock?:synchronized经过JDK1.6锁优化,偏向锁+轻量级锁性能优于ReentrantLock,低竞争场景开销更低。
-
为什么红黑树阈值是8?:泊松分布,链表长度超过8概率极低,平衡树化开销与查询效率。
-
helpTransfer机制:扩容时,空闲线程协助迁移其他桶位数据,缩短扩容耗时。
-
死链问题:JDK1.7多线程扩容存在循环链表死链,CPU飙高;1.8彻底修复。
10.4.5 生产使用规范
-
初始化指定预估容量,避免频繁扩容:初始容量=预估元素数量/0.75+1。
-
高并发统计场景,优先使用computeIfAbsent原子复合操作。
-
批量遍历采用迭代器遍历,禁止for循环快速失败异常。
10.5 并发有序集合(跳表系列)
10.5.1 ConcurrentSkipListMap(并发有序Map)
-
底层结构:跳表(多层有序链表),无红黑树、无锁CAS实现。
-
排序规则:默认key自然排序,支持自定义比较器。
-
核心优势:有序、高并发、插入删除查询时间复杂度O(logn)。
-
适用场景:高并发有序存储、时间轴排序、区间查询业务。
-
对比TreeMap:TreeMap单线程红黑树;ConcurrentSkipListMap并发跳表,线程安全。
10.5.2 ConcurrentSkipListSet(并发有序Set)
-
底层依托ConcurrentSkipListMap实现,key存储元素,value为固定占位对象。
-
有序、去重、线程安全,适合高并发有序去重场景。
10.6 非阻塞并发队列
10.6.1 ConcurrentLinkedQueue(无锁高性能队列)
-
底层结构:单向链表,全程CAS无锁实现,无悲观锁开销。
-
特性:无界、FIFO先进先出、不允许null元素、高并发吞吐量极高。
-
底层优化:头尾节点松弛更新,减少CAS竞争,提升并发性能。
-
适用场景:超高并发无阻塞消息流转、异步任务排队。
-
坑点:size()时间复杂度O(n),高并发下不准,推荐isEmpty()判断空。
10.6.2 ConcurrentLinkedDeque(无锁双向队列)
-
双向链表结构,支持头尾双向增删,CAS无锁。
-
适合双端进出、高并发消息插队、头尾消费场景。
10.7 同步包装类(不推荐使用)
10.7.1 Collections.synchronizedxxx 原理
-
底层使用对象悲观锁,全部方法串行互斥,读写全部阻塞。
-
锁粒度极大、并发吞吐量极低、性能差。
-
迭代器非线程安全,遍历需手动加锁,否则抛出并发修改异常。
10.7.2 生产禁用场景
高并发业务禁止使用synchronizedMap、synchronizedList,一律替换为JUC原生并发容器。
10.8 JUC并发容器终极选型表(生产直接抄)
|-----------------------|-----------|-------------------|-------------|
| 容器名称 | 底层结构 | 锁机制 | 适用生产场景 |
| CopyOnWriteArrayList | 动态数组 | 写锁读无锁 | 读多写少、静态配置列表 |
| ConcurrentHashMap | 数组+链表+红黑树 | CAS+ synchronized | 通用高并发键值存储 |
| ConcurrentSkipListMap | 跳表 | CAS无锁 | 高并发有序排序业务 |
| ConcurrentLinkedQueue | 单向链表 | CAS无锁 | 超高并发无阻塞消息队列 |
| Synchronized包装类 | 原集合封装 | 全局悲观锁 | 低并发、临时过渡场景 |
10.9 面试满分总结(必背10句)
-
JUC并发容器专为高并发设计,优于同步包装类,锁粒度更细;
-
CopyOnWrite采用写时复制,读无锁,仅适合读多写少;
-
CopyOnWriteArraySet底层依赖List,查重效率低,少量元素使用;
-
ConcurrentHashMap1.7分段锁,1.8桶位锁+红黑树;
-
1.8废弃ReentrantLock,改用synchronized,低竞争性能更优;
-
跳表系列容器天然有序,无锁实现,适合排序业务;
-
ConcurrentLinkedQueue无锁高性能,size()不准优先isEmpty;
-
同步包装类全局加锁,并发吞吐量极低,生产禁用;
-
所有JUC容器均不允许存储null,规避空指针歧义;
-
高并发优先无锁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 包含三大核心衍生类,分别解决父子线程传值、线程池复用传值、不可变上下文问题,生产开发高频使用,区别及底层原理如下:
简洁:
-
InheritableThreadLocal:父子线程数据传递
-
TransmittableThreadLocal:线程池跨线程透传(阿里)
11.5.1 InheritableThreadLocal(JDK原生|父子线程数据透传)
-
核心作用 :原生ThreadLocal仅当前线程可见,此类支持父线程向子线程自动传递数据,创建子线程时拷贝父线程上下文数据。
-
底层原理:重写ThreadLocal的childValue()方法,线程初始化时,JDK主动将父线程ThreadLocalMap数据拷贝至新建子线程Map,属于一次性拷贝。
-
适用场景:一次性新建子线程、简单异步任务、主线程向新建子线程透传用户信息。
-
致命缺陷 :不支持线程池复用。线程池线程长期存活,线程复用后不会重新拷贝父线程数据,出现上下文旧数据残留、串值问题。
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(阿里开源|线程池专用透传)
-
核心作用 :解决InheritableThreadLocal线程池复用串值痛点,完美适配线程池,实现异步线程上下文透传,是企业生产首选。
-
底层原理: 修饰线程池、任务包装,拦截线程池提交任务;
-
任务提交时,捕获主线程上下文快照;
-
任务执行前,将快照赋值给复用线程;
-
任务执行完毕,还原旧上下文,彻底杜绝脏数据残留。
-
依赖引入 :非JDK原生,需引入maven依赖
com.alibaba:transmittable-thread-local。 -
生产场景:Spring异步注解、线程池异步任务、链路追踪TraceId、登录用户上下文透传(互联网公司标配)。
11.5.3 ScopedValue(JDK21+|新一代官方替代方案)
-
核心定位 :JDK21推出,官方换代工具,彻底替代ThreadLocal,解决内存泄漏、手动remove痛点。
-
核心优势: 作用域绑定,代码块执行完毕自动销毁数据,无内存泄漏;
-
无需手动remove,语法简洁安全;
-
天然适配虚拟线程、结构化并发;
-
不可变设计,线程安全、禁止篡改。
-
适用场景:高版本JDK新项目、虚拟线程业务、轻量化上下文透传。
11.5.4 三大衍生类终极对比(面试必背)
|--------------------------|-------------|--------------|------------------|
| 衍生类 | 底层能力 | 优缺点 | 生产选型 |
| ThreadLocal | 单线程数据隔离 | 原生轻量、存在内存泄漏 | 简单单线程业务,用完必删 |
| InheritableThreadLocal | 父子线程一次性传值 | 不支持线程池、复用串值 | 临时新建线程、简单异步 |
| TransmittableThreadLocal | 线程池复用精准透传 | 第三方依赖、稳定成熟 | 主流线上业务、线程池异步(推荐) |
| ScopedValue | 作用域自动回收、无泄漏 | 高版本JDK专属、无泄漏 | JDK21+新项目、虚拟线程 |
11.5.5 面试满分总结(6句背诵)
-
原生ThreadLocal仅单线程隔离,存在内存泄漏,必须手动remove;
-
InheritableThreadLocal支持父子线程传值,禁止用于线程池;
-
TransmittableThreadLocal阿里开源,适配线程池,解决复用串值;
-
TTL底层靠任务包装+上下文快照,实现线程复用数据隔离;
-
ScopedValue是JDK21官方替代,无泄漏、无需手动清除;
-
生产线程池异步透传,优先选用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 实战开发硬性规范(生产必遵守)
-
必须手动清除:所有ThreadLocal使用完毕,finally块执行remove(),杜绝内存泄漏、线程串值;
-
禁止全局static滥用:非通用上下文工具,不要定义为静态常量,缩短生命周期;
-
线程池严格管控:异步任务、线程池任务,执行前后必须清空上下文;
-
存储轻量数据:仅存用户ID、标识、配置等轻量数据,禁止存储大对象;
-
禁止业务赋值null:set(null)不会清除Entry,必须使用remove()。
11.6.8 面试高频追问总结
-
Q:为什么用户上下文一定要用ThreadLocal? A:实现线程隔离,整条请求无参透传,解耦业务代码,避免方法层层传参。
-
Q:线程池使用ThreadLocal最大隐患? A:线程复用导致旧数据残留、上下文串值,必须手动remove清除。
-
Q:MDC日志底层原理? A:基于ThreadLocal实现,单线程日志链路隔离,打印唯一TraceId。
第十二部分 死锁、活锁、饥饿
12.1 死锁四大必要条件
死锁是两个及以上线程互相持有对方所需资源,且互相永久等待、无法主动释放资源的阻塞状态,四大必要条件缺一不可,全部满足才会产生死锁,详细解析如下:
-
互斥条件:临界资源同一时刻仅能被一个线程占用,不可并行持有;已被占用的资源,其他线程必须阻塞等待。
-
请求且保持条件:线程已经持有部分锁/资源,在不释放已有资源的前提下,继续申请其他被占用的资源,不会主动释放已持有资源。
-
不可剥夺条件:线程已获取的资源,无法被其他线程强行抢占、回收;只能由持有线程主动执行完毕后,手动释放资源。
-
循环等待条件:多个线程形成闭环资源依赖,线程A持有资源1等待资源2,线程B持有资源2等待资源1,互相循环等待,无限僵持。
面试满分总结:互斥占用、持有请求、不可抢占、循环等待;破坏任意一条,即可杜绝死锁。
12.2 排查工具
Java 线程死锁、线程卡顿、CPU飙高排查全部依赖JDK自带命令行工具+可视化工具,无需额外部署,线上生产环境高频使用,重点整理jstack、jconsole、jvisualvm三大核心排查工具,附带实操命令、排查步骤、使用场景、面试考点,适配死锁、活锁、线程阻塞排查。
12.2.1 jstack(线上最常用|命令行排查)
核心定位 :JDK自带命令行工具,无图形界面,轻量化、不占用额外内存,生产线上首选排查工具,专门抓取线程堆栈、定位死锁、线程阻塞、死循环。
-
常用实操命令 :
jstack -l 进程PID > thread.log,导出线程堆栈日志,本地分析; -
排查死锁标识 :日志末尾搜索关键字Found one Java-level deadlock,出现该标识判定存在死锁;
-
可排查问题:死锁、线程阻塞、锁等待、线程死循环、休眠线程、线程池积压;
-
线程状态标识:排查重点关注BLOCKED(阻塞抢锁)、WAITING(无限等待)、TIMED_WAITING(限时等待)线程;
-
优缺点:优点:轻量化、不卡顿线上服务、无需停机;缺点:纯文本日志,需人工分析堆栈。
12.2.2 jconsole(简易可视化|轻量监控)
核心定位:JDK自带图形化监控工具,无需安装,操作简单,适合开发、测试环境快速可视化排查,内置线程监控、内存监控、MBean监控。
-
启动方式 :cmd/终端直接输入
jconsole,选择运行中的Java进程一键连接; -
核心功能:实时查看线程数量、线程状态、线程堆栈、检测死锁、监控堆内存使用;
-
死锁检测:内置死锁检测按钮,一键自动扫描线程循环依赖,直观展示死锁线程、锁对象;
-
优缺点:优点:可视化、零代码、上手简单;缺点:占用少量资源,禁止高并发生产环境使用。
12.2.3 jvisualvm(全能可视化|专业排查)
核心定位:JDK官方全能可视化排查工具,功能最全,集线程监控、内存分析、GC分析、性能采样、堆dump分析于一体,是Java并发故障终极排查工具。
-
启动方式 :终端输入
jvisualvm,自动识别本地Java进程; -
线程排查能力:实时监控所有线程运行状态、查看线程堆栈、一键dump线程快照、精准定位死锁、锁竞争、线程卡顿;
-
扩展能力:安装Visual GC插件,可视化查看GC回收过程,排查并发下频繁GC、内存抖动;
-
dump分析:支持堆快照、线程快照保存,离线分析线上疑难故障;
-
优缺点:优点:功能齐全、可视化极强、适合深度排查;缺点:占用资源偏高,生产环境建议采样快照后离线分析。
12.2.4 补充工具(线上进阶排查)
-
jmap:导出堆内存快照,排查死锁引发的内存堆积、对象溢出;
-
jhat:解析堆dump文件,分析大对象、锁对象内存占用;
-
arthas(阿里开源):线上神器,无侵入排查线程、锁、方法耗时,生产高并发故障首选,替代部分JDK原生工具。
12.2.5 线上排查流程(面试+生产标准流程)
-
第一步:top命令查看服务器CPU,定位高占用Java进程PID;
-
第二步:jstack导出线程堆栈,搜索deadlock判定是否死锁;
-
第三步:筛选BLOCKED、WAITING阻塞线程,查看锁依赖关系;
-
第四步:结合代码定位嵌套锁、锁顺序错乱问题;
-
第五步:重启服务临时恢复,优化代码规避死锁。
12.2.6 面试满分总结(6句背诵)
-
排查工具分为命令行、可视化两类,生产优先轻量化命令;
-
jstack线上首选,导出堆栈,搜索deadlock判定死锁;
-
jconsole简易可视化,适合测试环境快速检测死锁;
-
jvisualvm功能最全,做线程、内存、GC深度分析;
-
线上禁止可视化工具常驻,优先快照+离线分析;
-
进阶排查使用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并发编程史上颠覆性优化,彻底解决传统平台线程(内核线程)重量大、创建受限、阻塞开销高的痛点,官方定位:替代平台线程、消灭线程池、简化异步编码。
简洁:
-
轻量级用户线程,开销极小,可海量创建。
-
告别线程池数量限制。
-
底层依托载体线程调度。
13.1.1 核心概念区分(面试必背)
-
平台线程(Platform Thread):传统原生线程,1:1映射操作系统内核线程,线程栈固定大小、内存开销大、创建数量上限极低(单机几千条),阻塞时内核态挂起、开销极高,我们以往使用的Thread、线程池线程均为平台线程。
-
虚拟线程(Virtual Thread) :JVM用户态轻量级线程,不直接绑定内核线程,栈内存动态伸缩、极小内存占用,单机可轻松创建百万、千万级线程,阻塞不会挂起内核线程,无昂贵上下文切换。
-
载体线程(Carrier Thread):JVM内部复用少量平台线程作为载体,负责调度执行虚拟线程,虚拟线程阻塞时,载体线程不会阻塞,转而执行其他就绪虚拟线程,最大化利用CPU。
13.1.2 底层实现原理
-
映射模型 :采用 M:N 映射,多条虚拟线程复用少量载体平台线程,区别于传统线程1:1内核映射;
-
栈内存优化:虚拟线程无固定栈大小,初始仅几百字节,栈内存随业务动态扩容、缩容,闲置时自动释放内存;平台线程默认栈内存1MB,内存占用差距巨大;
-
阻塞优化机制 :虚拟线程遇到阻塞操作(sleep、锁等待、IO请求)时,不会阻塞载体线程,JVM将虚拟线程挂起保存上下文,载体线程调度其他就绪虚拟线程执行,彻底消除阻塞空转浪费;
-
调度方式:JVM自主调度,无需操作系统内核介入,用户态完成线程切换,上下文切换开销几乎可以忽略。
13.1.3 核心特性(满分总结)
-
海量创建无上限:单机支持百万级虚拟线程,无需手动限制线程数量,告别线程池核心参数调优烦恼;
-
极低内存开销:单条虚拟线程内存占用KB级别,对比平台线程1MB栈内存,内存压缩近千倍;
-
阻塞零性能损耗:IO阻塞、sleep等待不占用载体线程,不会造成内核态阻塞,适配大量IO密集型业务;
-
语法完全兼容:虚拟线程继承Thread类,原有线程API全部可用,无需修改老旧业务代码,无缝迁移;
-
无需手动线程池 :官方明确:虚拟线程不需要池化、不建议复用,用完即销毁,简化编码模型;
-
天生守护线程:默认守护线程,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 生产适用&禁用场景
✅ 适用场景(核心落地业务)
-
海量IO密集型接口:HTTP请求、数据库查询、Redis调用、第三方接口调用,阻塞无损耗;
-
批量异步任务:批量文件处理、批量数据同步、定时任务批量执行;
-
网关高吞吐服务:网关转发、流量透传、短连接高频请求;
-
简单异步编码场景:告别线程池参数配置,简化异步业务开发。
❌ 不适用场景(避坑重点)
-
纯CPU密集型任务:大量计算、加密解密、大数据运算,虚拟线程调度开销大于平台线程;
-
本地锁自旋任务:CAS自旋、死循环运算,无法触发虚拟线程挂起优化;
-
依赖线程池隔离的业务:核心线程、非核心线程隔离的老旧复杂业务。
13.1.7 高频易错坑点(面试冷门考点)
-
禁止池化复用虚拟线程:虚拟线程设计初衷为用完即销毁,手动缓存复用会破坏JVM调度机制,性能倒退;
-
不绑定操作系统内核:无法通过top、jstack精准定位虚拟线程,底层无内核线程映射;
-
原生不支持定时调度:暂无定时虚拟线程,延时任务仍需依托平台线程;
-
ThreadLocal仍存在内存泄漏:虚拟线程生命周期短,虽泄漏概率极低,但仍需遵循remove规范;
-
虚拟线程不可修改优先级:优先级固定,无抢占式自定义调度能力。
13.1.8 面试满分8句总结(直接背诵)
-
虚拟线程是JDK21正式推出的轻量级线程,JVM管理、非内核线程;
-
采用M:N映射,复用少量载体线程调度海量虚拟线程;
-
栈内存动态伸缩、开销极低,单机支持百万级线程;
-
IO阻塞不占用载体线程,完美适配IO密集型业务;
-
语法兼容传统Thread,无需修改原有线程业务代码;
-
禁止池化复用,用完即销毁,彻底淘汰传统线程池;
-
CPU密集型业务不推荐使用,调度无优势;
-
未来版本逐步替代平台线程,是Java并发长期演进方向。
13.2 结构化并发 StructuredTaskScope
StructuredTaskScope 是JDK21正式推出、JDK19预览 的结构化并发工具,专门配合虚拟线程使用,用来统一管理一组子任务线程生命周期,解决传统异步编码线程泄露、异常散乱、任务不可控的痛点,是Java并发编码模型的重大革新,官方定义:让子线程生命周期受控于父线程,实现并发任务结构化管理。
简洁:
-
父子线程生命周期绑定,父存子存、父亡子亡。
-
自动异常传播,单一任务失败批量取消。
-
极简API,替代CompletableFuture复杂回调。
13.2.1 诞生背景(传统并发痛点)
-
线程泄露严重:传统线程/线程池异步任务,父线程结束后,子线程仍在后台无意义运行,无法统一回收;
-
异常分散杂乱:多异步任务执行,单个任务异常无法统一捕获,异常散乱、排查困难;
-
回调地狱:CompletableFuture多任务嵌套编排,代码层级臃肿,可读性极差;
-
资源无法联动释放:业务中途报错,已发起的异步任务无法主动取消,造成资源空耗。
13.2.2 核心核心特性(面试必背)
-
生命周期结构化绑定:所有通过当前Scope创建的子线程,生命周期归属于父线程,父线程等待所有子任务执行完毕才会结束,杜绝线程泄露;
-
失败自动传播取消:自定义失败策略,单个子任务异常,自动取消其他未完成任务,快速失败、节省资源;
-
异常聚合统一处理:收集所有子任务异常,统一抛出、统一捕获,规避异常散乱问题;
-
天然适配虚拟线程:配合虚拟线程海量创建、低开销特性,实现高并发简洁编码;
-
无锁、无线程池冗余配置:极简编码,无需手动配置线程池参数,轻量化管理任务。
13.2.3 三大内置策略(核心API)
JDK内置三种任务管控策略,适配不同业务场景,生产按需选用:
-
ShutdownOnFailure(失败即关闭|最常用):任意子任务抛出异常,立刻取消所有正在执行的任务,父线程终止,快速失败,适用于核心任务、强依赖业务;
-
ShutdownOnSuccess(成功即关闭):任意子任务执行成功,立刻取消其他未完成任务,适用于多候选任务、只要一个结果的业务(多渠道查询、重试兜底);
-
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 生产适用场景
-
多接口并行查询:聚合查询用户、订单、支付数据,任意接口失败整体终止;
-
多渠道择优请求:同时请求缓存、数据库、第三方接口,取最快返回结果;
-
批量非核心异步任务:日志、埋点、数据归档,无需强一致性,忽略个别异常;
-
微服务并行调用:微服务多接口聚合,统一管控调用生命周期,减少资源浪费。
13.2.7 面试满分7句总结(直接背诵)
-
StructuredTaskScope是JDK21结构化并发工具,适配虚拟线程;
-
核心思想:父子线程生命周期绑定,结构化管控杜绝线程泄露;
-
内置三大策略:失败终止、成功终止、忽略异常;
-
支持异常聚合、批量取消任务,资源利用率极高;
-
对比CompletableFuture,无回调地狱、代码简洁易维护;
-
依靠try-with-resources自动回收,无需手动关闭线程;
-
高版本JDK优先替代异步编排,是未来并发编码主流。
13.3 作用域值 ScopedValue
新一代线程上下文传递,替代 ThreadLocal,是 JDK21 正式推出、专为虚拟线程设计的上下文传递组件,官方定位:淘汰ThreadLocal、解决线程上下文污染、适配虚拟线程、实现不可变安全上下文透传,彻底修复ThreadLocal历史遗留痛点,是高版本Java并发上下文传递的最优方案。
13.3.1 诞生背景(为什么舍弃ThreadLocal?)
传统ThreadLocal存在大量无法根治的硬伤,尤其适配虚拟线程后弊端被无限放大,JDK推出ScopedValue针对性解决痛点:
-
可修改造成上下文污染:ThreadLocal支持set()重复赋值,多嵌套业务极易篡改上下文数据,引发脏数据;
-
生命周期不可控:ThreadLocal绑定线程,线程池复用、虚拟线程频繁创建销毁,易残留旧数据、内存泄漏;
-
无父子线程隔离管控:InheritableThreadLocal传递数据不可控,大批量子线程继承上下文引发数据混乱;
-
不适配虚拟线程:虚拟线程海量创建,ThreadLocal的Entry哈希表结构极易产生内存碎片、残留垃圾。
13.3.2 ScopedValue 核心核心特性(面试必背)
-
不可变上下文:绑定后数据只读,禁止二次修改,从根源杜绝上下文篡改,线程绝对安全;
-
作用域生命周期:数据仅在指定代码作用域内生效,代码执行完毕自动销毁,无残留、无内存泄漏;
-
天生适配虚拟线程:JDK为虚拟线程量身优化,无哈希表冗余,海量线程下内存开销极低;
-
安全父子传递:结合结构化并发,子线程自动继承父线程上下文,且只读不可篡改;
-
无手动清除要求:依托作用域自动回收,无需手动remove(),规避人为漏删bug;
-
非线程绑定:不永久挂载线程,仅绑定代码执行作用域,执行结束立即释放资源。
13.3.3 核心API 通俗易懂讲解
-
ScopedValue.get():获取当前作用域绑定的上下文数据;
-
ScopedValue.where():绑定键值对,开启作用域,声明上下文数据;
-
where().run():同步执行,作用域内执行业务代码,执行完毕自动销毁上下文;
-
where().call():异步执行,支持有返回值的业务逻辑;
-
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 生产适用场景
-
Web登录上下文透传:存储登录用户、权限信息,全程只读,禁止业务篡改;
-
微服务链路追踪:透传TraceId、SpanId,作用域结束自动清除链路标识;
-
多线程批量任务:批量异步任务共享全局配置、业务标识,无需重复传参;
-
结构化并发专属场景:配合VirtualThread+StructuredTaskScope做异步上下文传递。
13.3.7 高频易错坑点(面试冷门考点)
-
不可二次赋值:同一个作用域内,ScopedValue不可重复绑定数据,直接抛出异常;
-
作用域隔离:外层作用域数据,内层作用域可读取,内层修改不影响外层(隔离性);
-
非作用域不可获取:脱离绑定的代码块,调用get()直接报错,杜绝空残留;
-
不支持null绑定:禁止绑定null数据,从语法层面规避空上下文异常;
-
低版本JDK不兼容:JDK21及以上正式支持,无兼容降级方案。
13.3.8 面试满分7句总结(直接背诵)
-
ScopedValue是JDK21推出的新一代上下文传递工具,替代ThreadLocal;
-
核心特性:只读不可变、作用域管控、自动回收、无内存泄漏;
-
底层基于栈帧存储,区别于ThreadLocal哈希表,开销极低;
-
原生适配虚拟线程与结构化并发,是高版本并发标配;
-
无需手动清除资源,代码执行完毕自动销毁上下文;
-
禁止重复赋值、禁止绑定null,语法层面保证线程安全;
-
企业演进方向:新项目用ScopedValue,老旧项目保留ThreadLocal。
第十四部分 线上高并发实战必学
本章节全部为生产落地硬核实战,剔除空洞理论,全部是线上高频踩坑、企业通用规范、高并发优化手段,适配互联网后端、微服务、分布式项目,所有代码可直接拷贝用于生产,是从面试工程师进阶为资深业务工程师的核心章节。
简洁:
-
多线程上下文透传用户信息、链路 ID
-
并发安全日期工具:禁用 SimpleDateFormat,使用 DateTimeFormatter
-
高并发随机数:ThreadLocalRandom
-
多线程大文件分片读取
-
多线程事务一致性处理
-
异步回调地狱优化
-
多线程结合限流、令牌桶、漏桶算法
-
线上线程池压测调优
14.1 多线程上下文透传(用户/链路ID)
14.1.1 业务痛点
微服务高并发场景下,异步线程、线程池子线程无法直接获取主线程的登录用户、TraceId、请求头;传统ThreadLocal在线程池复用场景下存在上下文污染、内存泄漏、数据串扰问题。
14.1.2 三种透传方案对比
-
ThreadLocal:传统方案,适配低并发、无线程池复用场景,线程池极易数据串号;
-
InheritableThreadLocal:仅支持新建线程传递,线程池复用线程失效,生产基本废弃;
-
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 线上强制规范
-
禁止使用原生ThreadLocal做线程池上下文传递;
-
所有异步接口、线程池任务,执行结束必须手动清空上下文;
-
链路追踪TraceId统一使用TTL透传,保证日志链路完整。
14.2 并发安全日期时间工具(线上高频BUG点)
14.2.1 致命坑点
SimpleDateFormat 线程极度不安全!底层共享calendar对象,多线程并发格式化时间,必定出现时间错乱、年份偏移、直接报错,是线上最常见低级BUG。
14.2.2 生产推荐方案
-
JDK8+ DateTimeFormatter:不可变类、线程绝对安全,无锁高并发;
-
FastDateFormat:旧项目兼容方案,apache工具类,安全高性能;
-
禁止: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 优化细节
-
DateTimeFormatter定义为全局static常量,禁止方法内重复new;
-
LocalDateTime无时区,跨时区业务使用ZonedDateTime;
-
禁止使用静态SimpleDateFormat,并发必崩。
14.3 高并发随机数(规避伪随机卡顿)
14.3.1 痛点分析
-
Random:线程不安全,多线程竞争同一种子,CAS失败空转,并发卡顿;
-
Math.random():底层仍是Random,高并发性能极差;
-
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 线上规范
-
所有高并发随机场景,强制使用ThreadLocalRandom;
-
禁止循环内频繁创建Random实例,造成种子冲突;
-
加密安全随机数使用SecureRandom(低并发、加密场景)。
14.4 多线程大文件分片读取(百万级文件处理)
14.4.1 业务场景
线上日志分析、批量导入、大数据解析、超大CSV/TXT文件,单线程读取速度极慢,IO阻塞严重,采用分片+多线程并行读取提升吞吐量。
14.4.2 核心实现思路
-
获取文件总字节大小,自定义分片区间;
-
线程池分配每段读取起始位置、结束位置;
-
RandomAccessFile随机读写,精准定位文件指针;
-
并行读取、汇总数据,最后合并结果。
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 线上优化方案
-
IO密集型文件读取,线程池核心数设置为 CPU核心数*2;
-
使用try-with-resources自动关闭流,杜绝文件句柄泄露;
-
超大文件禁止一次性加载内存,防止OOM内存溢出。
14.5 多线程事务一致性(线上疑难痛点)
14.5.1 原生痛点
Spring默认事务仅支持单线程事务,多线程异步插入、更新数据,主线程回滚无法控制子线程,极易出现数据不一致、部分成功部分失败。
14.5.2 三种解决方案(生产分级)
-
最终一致性(简单业务):本地事务表+定时补偿、失败重试、幂等校验;
-
编程式事务(中等复杂度):TransactionTemplate手动管控事务,多线程汇总结果统一提交/回滚;
-
分布式事务(复杂业务):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 线上避坑规范
-
禁止在异步线程使用注解式事务(@Transactional),传播机制失效;
-
多线程强一致性业务,禁止使用简单异步,优先同步编排;
-
必须加幂等唯一键,防止重试造成重复脏数据。
14.6 异步回调地狱优化(告别多层嵌套)
14.6.1 传统痛点
原始Future、Thread异步编码,多层依赖业务嵌套,代码层级臃肿、可读性极差,维护成本极高,俗称回调地狱。
14.6.2 三代异步编码演进
-
第一代:Thread+Runnable,无返回、嵌套混乱;
-
第二代:Future,阻塞get()、无法回调;
-
第三代:CompletableFuture,非阻塞、链式编排;
-
第四代: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 线上使用规范
-
异步任务必须指定自定义线程池,禁止使用默认ForkJoinPool;
-
所有异步链路必须加exceptionally异常兜底;
-
高版本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 线上限流规范
-
业务接口优先令牌桶,网关层使用滑动窗口;
-
限流拒绝策略禁止直接抛出异常,优先降级、兜底、排队;
-
多节点分布式限流,使用Redis+Lua保证原子性。
14.8 线上线程池压测调优(资深工程师核心能力)
14.8.1 核心参数黄金配置
-
CPU密集型:核心线程数 = CPU核心数 + 1,减少上下文切换;
-
IO密集型:核心线程数 = CPU核心数 * 2 ~ 5,适配阻塞等待;
-
队列容量:业务接口默认100~500,超大批量任务1000+;
-
拒绝策略:线上禁止AbortPolicy直接抛异常,优先CallerRunsPolicy回退主线程。
14.8.2 线上线程池致命坑
-
禁止使用Executors快捷创建线程池,无边界队列引发OOM;
-
禁止全局共享线程池,业务隔离,拆分支付、订单、日志独立线程池;
-
线程池必须自定义线程工厂,命名规范,方便堆栈排查;
-
定时任务线程池禁止处理耗时业务,造成任务堆积。
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 压测监控指标
-
核心监控:线程活跃数、队列积压数量、任务拒绝次数、平均执行耗时;
-
优化标准:队列积压始终 < 30%,无拒绝任务,CPU利用率稳定在70%左右;
-
调优手段:压测逐步放大并发量,动态修改核心线程数,找到性能拐点。
第十五部分 高频面试查漏补缺(全覆盖)
-
sleep 与 wait 区别
-
为什么放弃使用 stop 终止线程
-
双重检查锁为什么必须加 volatile
-
ConcurrentHashMap 1.7 与 1.8 区别
-
AQS 独占与共享原理
-
线程池参数如何合理配置
-
如何优雅关闭线程池
-
原子类为何不用锁也能保证线程安全
-
ThreadLocal 内存泄漏原因与解决
-
偏向锁失效场景
-
手写生产者消费者三种实现
-
伪共享产生与解决
-
读写锁降级规则
-
虚拟线程使用场景与优势