最后更新:2026-08-11
Java 并发编程速查手册(JDK 8 17 21 版)
本文档面向 Java 高级开发工程师面试,覆盖并发编程核心知识点。内容覆盖 JDK 8、JDK 17、JDK 21 三个 LTS 版本,标注各版本的关键差异和演进。拒绝照本宣科,只讲面试真能用上的东西。
0. 三个 LTS 版本的并发脉络
面试前先搞清这三个版本的分水岭,面试官常问"你用的哪个版本?这个版本有什么并发特性?"
| 版本 | 发布时间 | 并发里程碑 | 面试定位 |
|---|---|---|---|
| JDK 8 | 2014.03 | ConcurrentHashMap 重写(Node + CAS + synchronized)、CompletableFuture、StampedLock、LongAdder、@Contended、ForkJoinPool 完善 | 基础盘:绝大多数公司生产环境还在用/问,所有并发基础概念在此版本成熟 |
| JDK 17 | 2021.09 | 偏向锁默认禁用(JDK 15 起)、Thread-Local Handshake、AppCDS、synchronized 内部优化(改用基于 obj 的 C++ 实现) | 过渡版:偏向锁被废弃,为虚拟线程铺路,底层锁机制做了清理 |
| JDK 21 | 2023.09 | 虚拟线程正式发布(JEP 444)、Structured Concurrency(Preview)、ScopedValue(Preview)、AQS 性能优化 | 新趋势:面试高频话题,代表 Java 并发模型的未来方向 |
面试策略:
如果你用 JDK 8,面试官会觉得你"基本功扎实,但可能对新特性不敏感"
如果你用 JDK 17,面试官会觉得你"跟上了节奏"
如果你用 JDK 21,面试官会重点问虚拟线程,但也会考察你对 JDK 8 基础的理解
最佳策略:JDK 8 的基础不能丢,JDK 21 的新特性要能讲透,JDK 17 作为过渡版本能说出关键变化即可
1. Java 内存模型(JMM)
1.1 先搞懂为什么要搞内存模型
简单说:CPU 太快了,内存跟不上。所以 CPU 内部搞了多级缓存(L1/L2/L3),每个核有自己的缓存。这就引出一个问题——线程 A 在核 1 上改了个变量,线程 B 在核 2 上能看到吗?
答案:不一定。JMM 就是来解决这个问题的。
JMM 的核心思路:不直接管物理内存,而是定义一套 happens-before 规则。你按规则来,我就保证可见性。
1.2 Happens-Before 规则(面试必背)
| 规则 | 大白话 | 例子 |
|---|---|---|
| 程序顺序规则 | 同一个线程里,前面的操作 HB 后面的 | a = 1; b = 2; 保证 a=1 先于 b=2 执行(但 JVM 可以重排,只要结果对) |
| volatile 规则 | volatile 写 HB 后面读 | 线程 A 写 flag = true,线程 B 读到 flag == true 时,A 之前写的所有变量 B 都能看到 |
| 锁规则 | 解锁 HB 加锁 | A 释放锁之后,B 拿到同一把锁时,A 在锁里改的东西 B 全能看到 |
| 线程启动规则 | Thread.start() HB 线程里的操作 | 主线程设好变量后 start 新线程,新线程一定能看到 |
| 线程终止规则 | 线程里的操作 HB 其他线程发现它终止 | thread.join() 返回后,join 的线程里的修改全可见 |
| 传递性 | A HB B,B HB C,则 A HB C | 这是整个规则体系的胶水 |
| 中断规则 | interrupt() 调用 HB 被中断线程检测到中断 | — |
| 终结器规则 | 构造函数 HB finalize() | — |
口诀:「顺、挥、锁、启、终、传、中、终」(顺序、volatile、锁、启动、终止、传递、中断、终结器)
1.3 volatile 语义和实现原理
两个语义
可见性:写操作直接刷回主内存,读操作直接从主内存拿
有序性:禁止 volatile 读写和前后代码重排
注意!volatile 不保证原子性! volatile int i; i++ 不是线程安全的。
实现原理(反汇编看)
这条指令做了两件事:
把当前处理器缓存行写回主存(Lock 语义)
让其他 CPU 缓存失效(MESI 协议的 Invalidate 操作)
内存屏障(Memory Barrier)
volatile 的有序性保证是通过插入内存屏障实现的:
| 屏障类型 | 作用 | volatile 写后插入 | volatile 读前插入 |
|---|---|---|---|
| LoadLoad | 禁止读和后面的读重排 | — | ✓ |
| LoadStore | 禁止读和后面的写重排 | — | ✓ |
| StoreStore | 禁止写和后面的写重排 | ✓ | — |
| StoreLoad | 禁止写和后面的读重排(最重的屏障) | ✓ | — |
插入策略:
volatile 写:前面插 StoreStore,后面插 StoreLoad
volatile 读:后面插 LoadLoad + LoadStore
面试怎么说: "volatile 的底层是通过内存屏障实现的。写操作前后会插入 StoreStore 和 StoreLoad 屏障,读操作后面会插入 LoadLoad 和 LoadStore 屏障。在 x86 架构上,volatile 写会额外生成 lock 前缀指令,利用 MESI 协议让其他 CPU 缓存失效。volatile 保证可见性和有序性,但不保证原子性,因为复合操作(比如 i++)不是单条指令能完成的。"
1.4 假共享(False Sharing)
生产场景:两个线程频繁修改不同变量,但变量在同一个缓存行(64字节),导致缓存反复失效,性能暴跌。
解决方案:
或者手动填充:
版本差异(面试加分):
@sun.misc.Contended是 JDK 8 引入的,但需要-XX:-RestrictContended才能生效(否则只对 JDK 内部类起作用)。JDK 8 是第一个支持这个注解的版本。JDK 8 的
LongAdder内部就用了@Contended来隔离 CPU 缓存行,避免伪共享,所以高并发计数场景下LongAdder比AtomicLong快很多。JDK 17 和 JDK 21 中该注解依然可用,但生产里手动填充或直接用并发容器更常见。
2. synchronized 深度解析
2.1 锁升级全过程
升级是单向的,不可降级(JDK 15 之前偏向锁有批量重偏向和批量撤销,但那是类级别的优化)。
无锁
啥锁都没有,谁都能访问。
偏向锁(Biased Locking)
Mark Word 记录偏向线程 ID
同一个线程多次进入同步块,直接对比线程 ID 就行,不用 CAS 操作
适合场景:只有单线程访问同步块
JDK 15+ 重要变更:偏向锁默认禁用!
原因:HotSpot 维护偏向锁的代码复杂度很高,但实际受益场景越来越少。禁用后简化了代码,也为 Project Valhalla(值类型)铺路。
版本差异(JDK 8 17 21 锁升级对比):
版本 偏向锁 锁升级路径 说明 JDK 8 默认开启 无锁 → 偏向锁 → 轻量级锁 → 重量级锁 完整四级升级,偏向锁是 JDK 8 的默认行为,面试 JDK 8 版本时锁升级是必考 JDK 17 默认禁用(JDK 15 起) 无锁 → 轻量级锁 → 重量级锁 偏向锁废弃,升级路径少了偏向锁这一级 JDK 21 默认禁用 无锁 → 轻量级锁 → 重量级锁 与 JDK 17 一致,虚拟线程场景下同步块更轻量 面试要点:面试官如果问锁升级,你要先确认对方关注的版本。JDK 8 答"四级升级",JDK 17+ 答"三级升级(偏向锁已废弃)"。很多人答错,是因为把 JDK 8 的旧知识套在所有版本上。
JDK 17 的 synchronized 底层实现变化:JDK 17 里 synchronized 改用基于对象头的 C++ 实现重构(JEP 里程碑之一),代码更简洁、更易维护,但对外行为不变。面试时提一句"JDK 17 对 synchronized 内部做了清理,废弃了偏向锁,为后续虚拟线程铺路"是加分项。
轻量级锁
竞争出现了,锁膨胀
线程在栈帧里创建 Lock Record,通过 CAS 把 Mark Word 指向 Lock Record
自旋等待:如果竞争线程很快释放,自旋能拿到锁,不用阻塞
适合场景:交替执行、竞争不激烈
重量级锁
自旋失败,膨胀为重量级锁
底层依赖操作系统的 Mutex Lock,线程挂起/唤醒需要用户态和内核态切换
适合场景:竞争激烈
2.2 对象头 Mark Word 结构(64 位 JVM)
| 状态 | Mark Word 存储内容 | 锁标志位 |
|---|---|---|
| 无锁 | hashCode (31) + age (4) + 0 + 01 | 01 |
| 偏向锁 | threadId (54) + epoch (2) + age (4) + 1 + 01 | 01 |
| 轻量级锁 | 指向栈中锁记录的指针 (62) + 00 | 00 |
| 重量级锁 | 指向互斥量的指针 (62) + 10 | 10 |
| GC 标记 | — | 11 |
面试怎么说: "Java 对象头中的 Mark Word 是 64 位的,它通过低两位的锁标志位来区分当前状态。无锁和偏向锁的标志位都是 01,靠第三位区分。轻量级锁标志位是 00,存的是指向栈中 Lock Record 的指针。重量级锁标志位是 10,存的是指向 Monitor 的指针。锁升级就是 Mark Word 的内容从线程 ID 变成锁记录指针,再变成 Monitor 指针的过程。"
2.3 synchronized vs ReentrantLock 实战选型
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM 内置(monitorenter/monitorexit) | JDK API(AQS) |
| 锁释放 | 自动释放(退出代码块) | 必须 finally 手动释放 |
| 可中断 | 不可中断 | lockInterruptibly() 可中断 |
| 超时 | 不支持 | tryLock(time) 支持 |
| 公平性 | 非公平 | 可选公平/非公平 |
| 条件变量 | 只有一个 wait/notify | 多个 Condition |
| 性能 | JDK 6+ 优化后差不多 | 差不多 |
| 可读性 | 简洁 | 灵活但啰嗦 |
实际选型原则:
能不用锁就不用锁(先想无锁方案)
简单场景用 synchronized(代码简洁,JVM 会自动优化)
需要 tryLock、超时、中断、多条件队列时用 ReentrantLock
JDK 21+ 虚拟线程场景下,尽量用 ReentrantLock(避免 pinning 问题,后面会讲)
版本差异:
JDK 8:两者性能经过 JDK 6+ 优化后几乎持平,选型主要看功能需求。JDK 8 里 synchronized 有偏向锁加持,单线程访问场景反而更快。
JDK 17:与 JDK 8 基本一致,synchronized 内部实现优化后性能更稳定,依旧推荐简单场景用 synchronized。
JDK 21:虚拟线程场景下,synchronized 会导致 pinning(线程被钉在载体线程上),此时优先用 ReentrantLock。这是版本差异的典型例子——同一个选择,在不同版本下结论不同。
面试怎么说: "实际开发中,简单同步用 synchronized,因为它代码更简洁,JVM 也能自动优化。需要高级特性比如超时、可中断、公平锁的时候用 ReentrantLock。但要注意 ReentrantLock 必须在 finally 里释放锁。在 JDK 21 虚拟线程场景下,建议用 ReentrantLock 代替 synchronized,因为 synchronized 会导致虚拟线程 pinning 到载体线程上。"
3. AQS 框架详解
3.1 核心原理
AQS(AbstractQueuedSynchronizer)是 JUC 包的基石,Doug Lea 写的。
两个核心组件:
state(volatile int):锁的状态,不同实现含义不同
ReentrantLock:0=未锁定,>0=重入次数
Semaphore:剩余许可数
CountDownLatch:还需要倒计时的次数
CLH 队列(双向链表):等待获取锁的线程排队
获取锁流程(以 ReentrantLock 非公平锁为例):
CAS 尝试将 state 从 0 改为 1
成功 → 设置 exclusiveOwnerThread 为当前线程,完事
失败 → 判断是不是重入(当前线程已经持有锁)
重入 → state + 1
非重入 → 入队(tail 插入),park 等待
前驱节点释放锁时 unpark 唤醒 → 再次尝试获取
3.2 各组件的 AQS 实现差异
| 组件 | state 含义 | 共享/独占 | 核心逻辑 |
|---|---|---|---|
| ReentrantLock | 重入次数 | 独占 | tryAcquire / tryRelease |
| CountDownLatch | 倒计时值 | 共享 | countDown() → state 减 1,减到 0 唤醒所有等待线程 |
| Semaphore | 剩余许可 | 共享 | acquire() → CAS 减 1,release() → CAS 加 1 |
| ReentrantReadWriteLock | 高 16 位读锁 / 低 16 位写锁 | 读共享 / 写独占 | 设计最复杂 |
3.3 公平锁 vs 非公平锁
| 维度 | 公平锁 | 非公平锁 |
|---|---|---|
| 获取策略 | 先排队,严格按 FIFO | 上来就抢,抢不到再排队 |
| 吞吐 | 较低(线程频繁挂起唤醒) | 较高(减少上下文切换) |
| 饥饿 | 不会 | 可能(但概率极低) |
| 默认 | 否 | ReentrantLock 默认非公平 |
非公平锁的"二次尝试":
这就是为什么非公平锁吞吐量高——新来的线程不一定需要排队,如果锁刚好释放,它直接拿走,省了一次 park/unpark 的开销。
3.4 面试怎么讲 AQS
三步讲法:
先说是什么:"AQS 是 JUC 的基础框架,核心是 volatile state + CLH 双向队列。"
再说怎么工作:"线程先 CAS 尝试修改 state 来获取锁,失败就加入 CLH 队列。队列里每个节点会检查前驱是否是 head,是的话再次尝试获取,不是就 park 等待。释放锁时 unpark 后继节点。"
最后说用到哪:"ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock 都是基于 AQS 实现的,区别在于 state 的含义和获取策略不同。"
面试加分点:
CLH 队列的节点有 EXCLUSIVE 和 SHARED 两种模式
条件队列(Condition Queue)和同步队列(Sync Queue)是分开的,signal 时从条件队列移到同步队列
JDK 19+ 对 AQS 做了一些性能优化,减少了 volatile 写
版本差异:JDK 8:AQS 是 JDK 8 并发包的基石,ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 等工具类在 JDK 8 已全部成熟。面试 JDK 8 版本时,AQS 原理是必考点。
JDK 17:AQS 实现与 JDK 8 基本一致,没有重大变化。JDK 17 主要是在 JVM 层面做优化,AQS 框架本身稳定。
JDK 21:AQS 在 JDK 19+ 做了 volatile 写优化,非公平锁的 CAS 路径性能提升。虚拟线程场景下,ReentrantLock 的使用更频繁(代替 synchronized 避免 pinning),所以 AQS 的稳定性至关重要。
4. 线程池全攻略
4.1 核心参数详解(7 个参数)
任务提交流程(面试高频):
4.2 四种拒绝策略
| 策略 | 行为 | 适用场景 |
|---|---|---|
| AbortPolicy(默认) | 抛 RejectedExecutionException | 需要感知被拒绝 |
| CallerRunsPolicy | 让提交任务的线程自己执行 | 降速但不丢任务 |
| DiscardPolicy | 静默丢弃 | 可以容忍丢失(如日志采集) |
| DiscardOldestPolicy | 丢弃队列最老的任务 | 只关心最新数据 |
自定义拒绝策略(生产中常用):
4.3 线程池大小设定
| 类型 | 公式 | 例子 |
|---|---|---|
| CPU 密集型 | N(cpu) + 1 | 4核 → 5 线程 |
| IO 密集型 | N(cpu) × 2 或 N(cpu) / (1 - 阻塞系数) | 4核,阻塞系数 0.9 → 40 线程 |
| 混合型 | 拆成两个线程池 | — |
2026 年更靠谱的做法:
别死套公式!生产环境要用压测数据来调优。
先按公式给个初始值
压测时观察:CPU 使用率、队列积压、响应时间
逐步调整到最优
版本差异:
JDK 8:线程池基础框架已完全成熟,
ForkJoinPool在 JDK 8 中完善(异步执行、managedBlock 等)。Executors工厂方法全量可用,但生产上严禁使用(无界队列风险)。JDK 17:与 JDK 8 基本一致,
ForkJoinPool在 JDK 17 中作为虚拟线程的调度器做了底层优化(但 JDK 17 本身没有虚拟线程)。JDK 21:虚拟线程引入了新思路——IO 密集型任务不再需要线程池,直接用
Executors.newVirtualThreadPerTaskExecutor()或Thread.ofVirtual()。但 CPU 密集型任务仍然需要传统线程池。JDK 21 的线程池面试题,不能只背 7 个参数,还要和虚拟线程做对比。(详见第 5 章)
4.4 线程池监控方案
生产必做! 不然出了问题你都不知道:
| 监控指标 | 获取方式 | 告警阈值 |
|---|---|---|
| 活跃线程数 | getActiveCount() | 接近 maximumPoolSize |
| 队列积压 | getQueue().size() | 超过容量 80% |
| 已完成任务 | getCompletedTaskCount() | 持续为 0 说明任务堆积 |
| 拒绝次数 | 自定义拒绝策略统计 | > 0 就告警 |
| 线程池生命周期 | Spring Actuator / Micrometer | — |
4.5 生产事故案例
事故:线程池使用不当导致雪崩
背景:订单服务调用支付网关,用的线程池做异步调用。
问题:
支付网关响应变慢(从 200ms 涨到 2s)
线程池队列迅速积压(用的无界 LinkedBlockingQueue)
内存暴涨 → GC 频繁 → STW 时间变长
所有请求变慢,雪崩
排查链路:
修复方案:
口诀:生产中严禁使用 Executors 的工厂方法创建线程池,必须手动 new ThreadPoolExecutor!
4.6 JDK 21 虚拟线程 vs 传统线程池
这个太重要了,面试必问,放到下一章详细讲。
5. 虚拟线程(Virtual Threads)专题
5.1 核心概念
虚拟线程是 JDK 21 正式发布的特性(JEP 444),从 JDK 15 开始孵化(Preview)。
核心思想:平台线程(平台线程 = OS 线程)很贵(默认 1MB 栈空间),虚拟线程很便宜(初始只有几百字节)。你可以轻松创建几百万个虚拟线程。
关键概念:
载体线程(Carrier Thread):虚拟线程运行的平台线程,由 ForkJoinPool 提供
挂载(Mount):虚拟线程被分配到载体线程上执行
卸载(Unmount):虚拟线程遇到阻塞操作时,从载体线程上摘下来
调度器(Scheduler):管理虚拟线程到载体线程的映射
5.2 和平台线程的对比
| 维度 | 平台线程(Platform Thread) | 虚拟线程(Virtual Thread) |
|---|---|---|
| 对应关系 | 1:1 对应 OS 线程 | M:N 多路复用(M 个虚拟线程跑在 N 个载体线程上) |
| 内存 | ~1MB 栈 | 初始几百字节,按需增长 |
| 创建成本 | 高(内核态操作) | 低(用户态操作) |
| 创建数量 | 几千个就顶天了 | 轻松百万级 |
| 阻塞代价 | 高(占住 OS 线程) | 低(卸载就行,载体线程继续干别的) |
| 调度 | OS 调度 | JVM 调度 |
| 线程池 | 必须用(复用线程) | 不需要!每个任务一个虚拟线程 |
| 适用场景 | CPU 密集型 / 需要精细控制 | IO 密集型(HTTP调用、DB查询) |
5.3 使用方式
5.4 与 Spring Boot 3.x 集成
Spring Boot 3.2+ 原生支持虚拟线程:
效果:
Tomcat 的每个请求用虚拟线程处理
@Async方法用虚拟线程执行CompletableFuture.supplyAsync()默认用虚拟线程
注意事项:
JDBC 驱动需要兼容(MySQL Connector/J 8.0.33+、PostgreSQL 42.7.0+ 已支持)
Hibernate 6.3+ 支持
如果用了自定义线程池做 IO,可以考虑替换为虚拟线程
5.5 Pinning 问题(synchronized 的坑)
这是虚拟线程最大的限制!
当虚拟线程在 synchronized 块内执行阻塞操作时,无法卸载,会一直占着载体线程,这叫 Pinning:
检测方法:
ThreadLocal 影响:
虚拟线程大量创建时,ThreadLocal 的内存开销不容忽视。JDK 21 提供了 ScopedValue(Preview)作为替代:
⚠️ 版本限制:ScopedValue 是 JDK 21 的 Preview 特性,JDK 8 和 JDK 17 中不可用。JDK 8/17 场景下还是用 ThreadLocal,但记得用完
remove()。
5.6 性能基准测试数据(参考)
| 场景 | 平台线程池(200线程) | 虚拟线程 | 提升 |
|---|---|---|---|
| 10 万并发 HTTP 请求 | OOM / 队列爆炸 | 正常运行 | — |
| 1 万并发 DB 查询 | RT p99 = 5s | RT p99 = 200ms | 25x |
| 内存占用(10万并发) | ~100GB(不可能) | ~500MB | — |
| CPU 密集型计算 | 略优(无调度开销) | 略低 | — |
面试怎么说: "虚拟线程的核心价值是把阻塞操作的代价从'占住 OS 线程'变成'卸载虚拟线程'。对于 IO 密集型应用,这意味着可以用同步的写法获得异步的性能,不用再写回调地狱或者 Reactive 代码。但它不是万能的,CPU 密集型任务还是该用平台线程池。另外要注意 synchronized 的 pinning 问题,生产环境建议全面替换为 ReentrantLock。"
6. Concurrent 集合深度
6.1 ConcurrentHashMap:JDK 8+ 实现
核心设计:数组 + 链表 + 红黑树,用 CAS + synchronized 保证并发安全。
和 JDK 7 的区别:
| 维度 | JDK 7(Segment 分段锁) | JDK 8+(Node 级锁) |
|---|---|---|
| 锁粒度 | Segment 级别(默认 16 段) | 单个 Node(桶头) |
| 数据结构 | 分段数组 + 链表 | 数组 + 链表 + 红黑树 |
| 锁方式 | ReentrantLock | CAS + synchronized |
| 并发度 | 16(可配置) | 理论无限(等于桶数) |
put 操作关键流程:
计算 hash,定位桶
桶为空 → CAS 插入
桶不为空 → synchronized 锁住桶头节点,遍历链表/红黑树插入
链表长度 ≥ 8 → 转红黑树
元素总数超过阈值 → 扩容(多线程协助扩容)
size() 怎么算的?
JDK 8 用 baseCount + CounterCell[] 的方案(类似 LongAdder),避免全局锁。
版本差异:
JDK 8:ConcurrentHashMap 的重写版本,彻底抛弃了 JDK 7 的 Segment 分段锁,改为 Node 数组 + CAS + synchronized 桶级锁。这是 JDK 8 并发中最重要的改动之一,面试必问。
JDK 17:与 JDK 8 实现一致,没有重大变化。JDK 17 保留了 JDK 8 的完整设计。
JDK 21:与 JDK 8 实现一致。虚拟线程场景下 ConcurrentHashMap 的 synchronized 桶级锁可能引起 pinning,但通常影响不大(锁持有时间极短)。
6.2 三种并发 Map 对比
| 维度 | ConcurrentHashMap | Hashtable | Collections.synchronizedMap |
|---|---|---|---|
| 锁 | CAS + synchronized(桶级) | 整表 synchronized | 整表 synchronized |
| 性能 | 高 | 低 | 低 |
| null | key/value 都不允许 | 都不允许 | 取决于底层 Map |
| 迭代 | 弱一致性(不抛 CME) | fail-fast | fail-fast |
| JDK 8+ | 推荐 | 不推荐 | 不推荐 |
口诀:2026 年了,ConcurrentHashMap 一把梭,其他两个别用了。
6.3 CopyOnWriteArrayList
核心原理:写时复制。每次修改(add/set/remove)都复制一份新数组,修改完替换引用。
优点:读操作无锁,迭代器不会抛 ConcurrentModificationException 缺点:写操作内存开销大(复制整个数组),数据一致性是最终一致
适用场景:读多写少、数据量不大、允许最终一致
事件监听器列表
配置缓存
黑名单/白名单
不适用场景:数据量大、写操作频繁
版本差异:
JDK 8:CopyOnWriteArrayList 在 JDK 8 中已成熟,是 JDK 8 并发集合的标配之一。
JDK 17 / JDK 21:实现与 JDK 8 一致。虚拟线程场景下,如果读多写少且数据量不大,CopyOnWriteArrayList 仍然适用,但要注意写时的复制成本。
6.4 BlockingQueue 家族选型
| 队列 | 底层结构 | 有界 | 公平性 | 适用场景 |
|---|---|---|---|---|
| ArrayBlockingQueue | 数组 | 有界(构造时指定) | 非公平 | 通用生产者-消费者 |
| LinkedBlockingQueue | 链表 | 可选有界 | 非公平 | 通用,吞吐量略高(两把锁) |
| SynchronousQueue | 无存储 | — | 可选 | 直接交接,线程池 Executors.newCachedThreadPool() 用的 |
| PriorityBlockingQueue | 堆 | 无界 | — | 优先级任务 |
| DelayQueue | 堆 | 无界 | — | 延时任务(订单超时关闭) |
ArrayBlockingQueue vs LinkedBlockingQueue:
| 维度 | ABQ | LBQ |
|---|---|---|
| 锁 | 一把锁(put/take 互斥) | 两把锁(put/take 分离) |
| 吞吐 | 略低 | 略高 |
| 内存 | 预分配,无 GC | 动态分配 Node,有 GC |
| 边界 | 必须指定容量 | 可选(默认 Integer.MAX_VALUE = 几乎无界) |
生产建议:优先用 ArrayBlockingQueue,因为有界能防止 OOM。LinkedBlockingQueue 不设界的话,下游慢了会无限积压。
7. 并发工具类实战
7.1 CountDownLatch CyclicBarrier Semaphore 对比
| 维度 | CountDownLatch | CyclicBarrier | Semaphore |
|---|---|---|---|
| 作用 | 等 N 个操作完成 | N 个线程互相等到齐 | 限流(控制并发数) |
| 可重用 | 不可(一次性的) | 可(reset 后重来) | 可(release/acquire) |
| 计数器 | 只能减 | 只能减(到 0 后自动重置) | 可增可减 |
| 典型场景 | 并行任务汇总结果 | 多线程分阶段同步点 | 数据库连接池限流 |
| 谁先执行 | 主线程 await() | 所有线程 await() | 任意线程 acquire() |
CountDownLatch 实战:
CyclicBarrier 实战:
Semaphore 实战:
7.2 CompletableFuture 异步编程
版本背景:CompletableFuture 是 JDK 8 引入的,是 JDK 8 在异步编程领域最重要的贡献。它把 JDK 7 及以前的 Future(只能阻塞获取结果)升级为可组合、可链式的异步编程模型。所以面试官问 CompletableFuture,其实是考 JDK 8 的知识点。
链式调用:
组合操作:
异常处理三件套:
7.3 Structured Concurrency(JDK 21 Preview)
⚠️ 版本限制:Structured Concurrency 是 JDK 21 专属的 Preview 特性,JDK 8 和 JDK 17 都没有。如果在 JDK 8/17 环境下无法使用,需要手动实现(用 CompletableFuture + 异常传播)。面试时如果公司还在用 JDK 8/17,不要主动提这个特性做方案,但可以展示"我了解新特性"。
解决的痛点:多线程场景下,一个子任务异常了,其他子任务还在跑,浪费资源。
核心思想:把并发任务当作一个整体,同生共死。
ShutdownOnFailure:任一失败,关闭其他ShutdownOnSuccess:任一成功,关闭其他可以和虚拟线程完美搭配
面试怎么说: "Structured Concurrency 是 JDK 21 的预览特性,它把并发任务的生命周期绑定到一个 scope 上。好处是代码结构清晰——你能从代码块直接看出哪些任务是并行的。异常处理也更优雅,一个子任务失败,scope 自动关闭其他子任务,不会出现僵尸任务。"
8. 死锁排查实战
8.1 死锁的 4 个必要条件
| 条件 | 含义 | 破法 |
|---|---|---|
| 互斥 | 资源同一时刻只能被一个线程持有 | 用可重入的并发容器替代 |
| 持有并等待 | 持有资源的同时请求新资源 | 一次性申请所有资源 |
| 不可抢占 | 资源只能主动释放 | 用 tryLock + 超时 |
| 循环等待 | A 等 B、B 等 A 形成环 | 统一加锁顺序 |
破任意一个条件就能避免死锁。 生产中最常用的方法:
统一加锁顺序(破循环等待)
tryLock + 超时(破持有并等待 + 不可抢占)
8.2 jstack 排查死锁
jstack 会自动检测死锁并输出:
8.3 Arthas 排查
8.4 生产案例
事故:转账导致的死锁
修复方案:按 ID 排序加锁
排查链路:
9. 面试高频问答
Q1:线程和进程的区别?
初中级回答:进程是资源分配的单位,线程是 CPU 调度的单位。进程之间内存隔离,线程之间共享内存。
高级补充:进程有独立的虚拟地址空间,线程共享进程的资源(堆、方法区、文件描述符等),但每个线程有自己的栈和程序计数器。线程切换的代价比进程切换小(不用切换页表),但线程间需要同步机制来保证数据一致性。
Q2:线程有哪些状态?
标准回答(6 个):
| 状态 | 含义 |
|---|---|
| NEW | 创建了还没 start |
| RUNNABLE | 就绪+运行中(JVM 层面) |
| BLOCKED | 等锁 |
| WAITING | wait()/join()/park() |
| TIMED_WAITING | sleep(time)/wait(time) |
| TERMINATED | 执行完毕 |
高级加分:在 JDK 21 里,虚拟线程还有一个 PINNED 状态(被钉在载体线程上无法卸载),通过
thread.state()或者 JFR 事件可以看到。版本差异:JDK 8 和 JDK 17 的线程状态只有上述 6 个,没有 PINNED 状态。PINNED 是虚拟线程(JDK 21)特有的概念。
Q3:sleep 和 wait 的区别?
| 维度 | sleep | wait |
|---|---|---|
| 所属 | Thread 的方法 | Object 的方法 |
| 释放锁 | 不释放 | 释放 |
| 唤醒 | 时间到了自动醒 | notify/notifyAll |
| 使用位置 | 任意 | 必须在 synchronized 块内 |
Q4:volatile 能保证原子性吗?
不能。 volatile 保证可见性和有序性。i++ 这种复合操作不是原子的(读-改-写三步),需要 AtomicInteger 或 LongAdder。
Q5:ThreadLocal 原理?内存泄漏怎么解决?
原理:每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是强引用。
内存泄漏:线程复用(线程池)时,如果 ThreadLocal 用完不清理,value 一直存在。
解决:用完必须 remove(),放在 finally 里。
虚拟线程场景下 ThreadLocal 的注意事项:虚拟线程可以创建几百万个,每个都有 ThreadLocal 副本的话,内存爆炸。JDK 21 的 ScopedValue 是更好的替代方案。
版本差异:
JDK 8:ThreadLocal 是唯一的选择,没有替代方案。务必在 finally 里
remove()。JDK 17:与 JDK 8 一致,ThreadLocal 仍然是标准方案。
JDK 21:虚拟线程场景下推荐用 ScopedValue(Preview)替代 ThreadLocal,避免内存膨胀。但生产环境谨慎使用 Preview 特性。
Q6:什么是 CAS?有什么问题?
CAS(Compare And Swap):比较内存值和预期值,相同则更新为新值。是一条 CPU 原子指令。
三个问题:
ABA 问题 →
AtomicStampedReference(加版本号)自旋开销 → 长时间 CAS 失败浪费 CPU
只能保证一个变量的原子性 →
AtomicReference包装多个变量
Q7:什么是 AQS?
参考第 3 章。核心:volatile state + CLH 队列。
Q8:synchronized 和 ReentrantLock 的区别?
参考第 2.3 节。关键:ReentrantLock 支持超时、可中断、公平锁、多条件变量。JDK 21 虚拟线程场景下,建议用 ReentrantLock 避免 pinning。
Q9:ConcurrentHashMap 怎么保证线程安全?
JDK 8+:数组 + 链表 + 红黑树。put 时桶为空用 CAS,桶不为空用 synchronized 锁住桶头节点。粒度从 JDK 7 的 Segment 级细化到 Node 级。
Q10:线程池核心参数?
参考第 4.1 节。7 个参数,记住任务提交流程。
Q11:线程池拒绝策略?
AbortPolicy(默认,抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(丢弃)、DiscardOldestPolicy(丢最老的)。生产一般自定义。
Q12:线程池大小怎么定?
CPU 密集型 N+1,IO 密集型 2N 或 N/(1-阻塞系数)。但生产靠压测,别死套公式。
Q13:什么是死锁?怎么排查?
参考第 8 章。4 个必要条件,jstack 和 Arthas 排查。
Q14:什么是虚拟线程?
参考第 5 章。核心:轻量级线程,M:N 调度,IO 密集型场景性能碾压平台线程。
Q15:CountDownLatch 和 CyclicBarrier 的区别?
CountDownLatch 一次性、一个或多个线程等另外 N 个线程。CyclicBarrier 可重用、N 个线程互相等。
Q16:CompletableFuture 了解吗?
参考第 7.2 节。链式调用、组合、异常处理,替代回调地狱。
Q17:什么是线程安全?
多线程环境下,不用额外同步,程序行为依然正确。
实现方式:
不可变(final、String)
栈封闭(局部变量)
线程本地(ThreadLocal)
原子类(AtomicInteger)
锁(synchronized、Lock)
并发集合(ConcurrentHashMap)
Q18:什么是伪共享?怎么解决?
参考第 1.4 节。缓存在同一行的不同变量互相影响,用 @Contended 或填充解决。
Q19:Java 中锁有哪些分类?
| 分类 | 锁 |
|---|---|
| 悲观锁/乐观锁 | synchronized / CAS |
| 公平锁/非公平锁 | ReentrantLock(true/false) |
| 可重入锁 | synchronized、ReentrantLock |
| 读写锁 | ReentrantReadWriteLock、StampedLock(JDK 8+) |
| 偏向锁/轻量级锁/重量级锁 | synchronized 的三种状态(JDK 8 存在偏向锁,JDK 17+ 废弃) |
Q20:JDK 8 17 21 三个版本在并发层面分别有哪些变化?
JDK 8 并发核心变化
ConcurrentHashMap 重写:从 Segment 分段锁改为 Node + CAS + synchronized 桶级锁,并发度大幅提升
CompletableFuture 引入:可组合的异步编程模型,终结了 Future 只能阻塞的尴尬
StampedLock 引入:比 ReentrantReadWriteLock 更高效的乐观读锁
LongAdder / LongAccumulator 引入:高并发计数场景下性能碾压 AtomicLong
@sun.misc.Contended 注解:解决伪共享问题
ForkJoinPool 完善:新增
commonPool()、execute()等异步方法synchronized 锁升级机制成熟:偏向锁 → 轻量级锁 → 重量级锁,四级升级路径
"JDK 8 是并发编程的集大成者,现在绝大多数并发基础概念(ConcurrentHashMap 的桶级锁、CompletableFuture 的链式调用、LongAdder 的 Striped 思想)都在这个版本定型。面试 JDK 8 版本时,能把 ConcurrentHashMap 的 put 流程完整讲清楚,就已经展现了扎实的功底。"
JDK 17 并发相关变化
偏向锁默认禁用(JDK 15 起,JDK 17 延续):废弃偏向锁,简化 JVM 锁实现
synchronized 内部实现重构:改用基于对象头的 C++ 实现,更简洁、更易维护
Thread-Local Handshake(JEP 312):改进的线程握手机制,减少全局安全点,提升 JVM 响应性
AppCDS(Application Class-Data Sharing):改进应用启动性能(间接影响并发场景的启动延迟)
Removed RMI Activation / Applet API:清理了遗留并发代码
"JDK 17 不是并发特性的爆发版本,但它的意义在于清理——废弃偏向锁、重构 synchronized 实现,这些都是为后续虚拟线程的引入做准备。如果你面试时说自己用 JDK 17,面试官可能会问:'JDK 17 和 JDK 8 相比,synchronized 有什么变化?'要答得出偏向锁被废弃了。"
JDK 21 并发核心变化
虚拟线程正式发布(JEP 444):轻量级 M:N 调度,IO 密集型场景性能飞跃
Structured Concurrency Preview(结构化并发):并发任务同生共死,异常处理更优雅
ScopedValue Preview(替代 ThreadLocal):解决虚拟线程场景下 ThreadLocal 的内存膨胀
偏向锁默认禁用(JDK 15 开始,21 早已生效)
ForkJoinPool 优化:作为虚拟线程的默认调度器,性能提升
ReentrantLock 改进:非公平锁的 CAS 路径优化
面试怎么说: "JDK 8 把并发基础打牢了,JDK 17 把遗留问题清理干净了,JDK 21 则带来了并发编程的新范式——虚拟线程。这三个版本代表了 Java 并发编程的过去、现在和未来。JDK 8 的基础知识(ConcurrentHashMap、AQS、线程池)是面试的基石,JDK 21 的新特性(虚拟线程、结构化并发)是面试的加分项,JDK 17 作为过渡版本能说出关键变化(偏向锁废弃)就足够了。"
附录:面试 Checklist
最后,面试前过一遍这个清单:
JMM 的 happens-before 规则能说出 5 条以上
volatile 的语义和底层实现(内存屏障 + MESI)
synchronized 锁升级过程 + 版本差异(JDK 8 四级升级 / JDK 17+ 偏向锁废弃)
AQS 原理(state + CLH 队列),能讲清楚 ReentrantLock 的获取流程
线程池 7 个参数 + 任务提交流程 + 拒绝策略
线程池大小怎么定(不要只背公式,要提压测)
虚拟线程原理 + 和平台线程区别 + pinning 问题(JDK 21)
ConcurrentHashMap JDK 8+ 实现(Node 级锁,对比 JDK 7 Segment)
CompletableFuture 引入自 JDK 8,链式调用和异常处理
JDK 8 的 StampedLock / LongAdder 使用场景
JDK 17 的偏向锁废弃 + synchronized 内部重构
JDK 21 的 Structured Concurrency / ScopedValue(Preview)
BlockingQueue 家族选型
CountDownLatch CyclicBarrier Semaphore 使用场景
死锁排查:jstack / Arthas
生产事故案例能讲出排查链路
能讲清「JDK 8 打基础 JDK 17 清理 JDK 21 新范式」的版本演进脉络
本文档持续更新,如有错漏欢迎指正。祝面试顺利!🎯
本内容由 Coze AI 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。