最后更新:2026-08-11

Java 并发编程速查手册(JDK 8 17 21 版)

本文档面向 Java 高级开发工程师面试,覆盖并发编程核心知识点。内容覆盖 JDK 8、JDK 17、JDK 21 三个 LTS 版本,标注各版本的关键差异和演进。拒绝照本宣科,只讲面试真能用上的东西。


0. 三个 LTS 版本的并发脉络

面试前先搞清这三个版本的分水岭,面试官常问"你用的哪个版本?这个版本有什么并发特性?"

版本发布时间并发里程碑面试定位
JDK 82014.03ConcurrentHashMap 重写(Node + CAS + synchronized)、CompletableFuture、StampedLock、LongAdder、@Contended、ForkJoinPool 完善基础盘:绝大多数公司生产环境还在用/问,所有并发基础概念在此版本成熟
JDK 172021.09偏向锁默认禁用(JDK 15 起)、Thread-Local Handshake、AppCDS、synchronized 内部优化(改用基于 obj 的 C++ 实现)过渡版:偏向锁被废弃,为虚拟线程铺路,底层锁机制做了清理
JDK 212023.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 语义和实现原理

两个语义

  1. 可见性:写操作直接刷回主内存,读操作直接从主内存拿

  2. 有序性:禁止 volatile 读写和前后代码重排

注意!volatile 不保证原子性! volatile int i; i++ 不是线程安全的。

实现原理(反汇编看)

lock addl $0x0, (%rsp)   // x86 架构下 volatile 写会生成这条指令

这条指令做了两件事:

  • 把当前处理器缓存行写回主存(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字节),导致缓存反复失效,性能暴跌。

解决方案

// JDK 8+ 方案
@sun.misc.Contended
public class Counter {
    volatile long count1;
    volatile long count2;
}

或者手动填充:

public class PaddedCounter {
    volatile long count1;
    long pad1, pad2, pad3, pad4, pad5, pad6, pad7; // 填充到 64 字节
    volatile long count2;
}

版本差异(面试加分)

  • @sun.misc.ContendedJDK 8 引入的,但需要 -XX:-RestrictContended 才能生效(否则只对 JDK 内部类起作用)。JDK 8 是第一个支持这个注解的版本。

  • JDK 8 的 LongAdder 内部就用了 @Contended 来隔离 CPU 缓存行,避免伪共享,所以高并发计数场景下 LongAdderAtomicLong 快很多。

  • JDK 17 和 JDK 21 中该注解依然可用,但生产里手动填充或直接用并发容器更常见。


2. synchronized 深度解析

2.1 锁升级全过程

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

升级是单向的,不可降级(JDK 15 之前偏向锁有批量重偏向和批量撤销,但那是类级别的优化)。

无锁

啥锁都没有,谁都能访问。

偏向锁(Biased Locking)

  • Mark Word 记录偏向线程 ID

  • 同一个线程多次进入同步块,直接对比线程 ID 就行,不用 CAS 操作

  • 适合场景:只有单线程访问同步块

JDK 15+ 重要变更:偏向锁默认禁用!

-XX:-UseBiasedLocking  // 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 + 0101
偏向锁threadId (54) + epoch (2) + age (4) + 1 + 0101
轻量级锁指向栈中锁记录的指针 (62) + 0000
重量级锁指向互斥量的指针 (62) + 1010
GC 标记11

面试怎么说: "Java 对象头中的 Mark Word 是 64 位的,它通过低两位的锁标志位来区分当前状态。无锁和偏向锁的标志位都是 01,靠第三位区分。轻量级锁标志位是 00,存的是指向栈中 Lock Record 的指针。重量级锁标志位是 10,存的是指向 Monitor 的指针。锁升级就是 Mark Word 的内容从线程 ID 变成锁记录指针,再变成 Monitor 指针的过程。"

2.3 synchronized vs ReentrantLock 实战选型

维度synchronizedReentrantLock
实现层面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 写的。

两个核心组件

  1. state(volatile int):锁的状态,不同实现含义不同

    • ReentrantLock:0=未锁定,>0=重入次数

    • Semaphore:剩余许可数

    • CountDownLatch:还需要倒计时的次数

  2. CLH 队列(双向链表):等待获取锁的线程排队

Head → Node(线程A) → Node(线程B) → Node(线程C) → Tail
       (waiting)      (waiting)      (waiting)

获取锁流程(以 ReentrantLock 非公平锁为例):

  1. CAS 尝试将 state 从 0 改为 1

  2. 成功 → 设置 exclusiveOwnerThread 为当前线程,完事

  3. 失败 → 判断是不是重入(当前线程已经持有锁)

  4. 重入 → state + 1

  5. 非重入 → 入队(tail 插入),park 等待

  6. 前驱节点释放锁时 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 默认非公平

非公平锁的"二次尝试"

// NonfairSync.tryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 第一次:不管队列,直接 CAS 抢
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    // ...重入逻辑
    return false;
}

这就是为什么非公平锁吞吐量高——新来的线程不一定需要排队,如果锁刚好释放,它直接拿走,省了一次 park/unpark 的开销。

3.4 面试怎么讲 AQS

三步讲法

  1. 先说是什么:"AQS 是 JUC 的基础框架,核心是 volatile state + CLH 双向队列。"

  2. 再说怎么工作:"线程先 CAS 尝试修改 state 来获取锁,失败就加入 CLH 队列。队列里每个节点会检查前驱是否是 head,是的话再次尝试获取,不是就 park 等待。释放锁时 unpark 后继节点。"

  3. 最后说用到哪:"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 个参数)

public ThreadPoolExecutor(
    int corePoolSize,        // 核心线程数(即使空闲也不回收,除非设了 allowCoreThreadTimeOut)
    int maximumPoolSize,     // 最大线程数(核心+非核心)
    long keepAliveTime,      // 非核心线程空闲存活时间
    TimeUnit unit,           // 时间单位
    BlockingQueue<Runnable> workQueue,  // 任务队列
    ThreadFactory threadFactory,        // 线程工厂(自定义线程名)
    RejectedExecutionHandler handler    // 拒绝策略
)

任务提交流程(面试高频):

提交任务 → 核心线程数满了?→ 没满,创建核心线程执行
                    ↓ 满了
              队列满了?→ 没满,放入队列
                    ↓ 满了
              最大线程数满了?→ 没满,创建非核心线程执行
                    ↓ 满了
              执行拒绝策略

4.2 四种拒绝策略

策略行为适用场景
AbortPolicy(默认)抛 RejectedExecutionException需要感知被拒绝
CallerRunsPolicy让提交任务的线程自己执行降速但不丢任务
DiscardPolicy静默丢弃可以容忍丢失(如日志采集)
DiscardOldestPolicy丢弃队列最老的任务只关心最新数据

自定义拒绝策略(生产中常用):

public class MyRejectionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 1. 记录告警
        log.warn("线程池已满!队列积压: {}", executor.getQueue().size());
        // 2. 持久化任务到 MQ/DB
        taskPersistenceService.save(r);
        // 3. 或者降级处理
        fallbackService.handle(r);
    }
}

4.3 线程池大小设定

类型公式例子
CPU 密集型N(cpu) + 14核 → 5 线程
IO 密集型N(cpu) × 2 或 N(cpu) / (1 - 阻塞系数)4核,阻塞系数 0.9 → 40 线程
混合型拆成两个线程池

2026 年更靠谱的做法

别死套公式!生产环境要用压测数据来调优。

  1. 先按公式给个初始值

  2. 压测时观察:CPU 使用率、队列积压、响应时间

  3. 逐步调整到最优

版本差异

  • 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
// 定时上报(集成 Micrometer)
@Scheduled(fixedRate = 10000)
public void monitor() {
    log.info("线程池状态: active={}, poolSize={}, queueSize={}, completed={}, rejected={}",
        pool.getActiveCount(), pool.getPoolSize(), 
        pool.getQueue().size(), pool.getCompletedTaskCount(),
        rejectedCounter.get());
}

4.5 生产事故案例

事故:线程池使用不当导致雪崩

背景:订单服务调用支付网关,用的线程池做异步调用。

问题:

  1. 支付网关响应变慢(从 200ms 涨到 2s)

  2. 线程池队列迅速积压(用的无界 LinkedBlockingQueue)

  3. 内存暴涨 → GC 频繁 → STW 时间变长

  4. 所有请求变慢,雪崩

排查链路:

告警:订单接口 RT 飙升 → 看监控发现 GC 频繁 → dump 堆内存
→ 发现线程池队列里堆了 50 万个任务 → 查线程池配置
→ 用了 newFixedThreadPool(底层 LinkedBlockingQueue 无界)
→ 下游慢导致任务处理不过来,队列无限增长

修复方案

// 错误写法(千万别这么写)
ExecutorService pool = Executors.newFixedThreadPool(20); // 无界队列!

// 正确写法
ThreadPoolExecutor pool = new ThreadPoolExecutor(
    20, 50, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(1000),  // 有界队列
    new ThreadFactoryBuilder().setNameFormat("pay-call-%d").build(),
    new CallerRunsPolicy()  // 满了让调用线程自己干,降速
);

口诀:生产中严禁使用 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 使用方式

// 方式 1:直接创建
Thread vt = Thread.ofVirtual().start(() -> {
    System.out.println("Hello from virtual thread!");
});

// 方式 2:工厂方法(最常用)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    IntStream.range(0, 100_000).forEach(i -> 
        executor.submit(() -> {
            Thread.sleep(Duration.ofSeconds(1));
            return i;
        })
    );
}

// 方式 3:自定义虚拟线程工厂
ThreadFactory factory = Thread.ofVirtual().name("vt-", 0).factory();
Thread vt = factory.newThread(() -> doWork());

5.4 与 Spring Boot 3.x 集成

Spring Boot 3.2+ 原生支持虚拟线程

# application.yml
spring:
  threads:
    virtual:
      enabled: true  # 一行配置,Tomcat、WebFlux 全部切到虚拟线程

效果:

  • 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

// 坏例子:synchronized 导致 pinning
synchronized (lock) {
    httpClient.send(request); // 阻塞 IO,虚拟线程被 pin 住了!
}

// 好例子:用 ReentrantLock 代替
reentrantLock.lock();
try {
    httpClient.send(request); // 虚拟线程可以正常卸载
} finally {
    reentrantLock.unlock();
}

检测方法

-Djdk.tracePinnedThreads=full  // 打印 pinning 堆栈
-Djdk.tracePinnedThreads=short // 打印简短信息

ThreadLocal 影响: 虚拟线程大量创建时,ThreadLocal 的内存开销不容忽视。JDK 21 提供了 ScopedValue(Preview)作为替代:

⚠️ 版本限制:ScopedValue 是 JDK 21 的 Preview 特性,JDK 8 和 JDK 17 中不可用。JDK 8/17 场景下还是用 ThreadLocal,但记得用完 remove()

// 传统 ThreadLocal(JDK 8 / 17 / 21 都可用)
static final ThreadLocal<User> USER = new ThreadLocal<>();

// ScopedValue(仅 JDK 21 Preview)
static final ScopedValue<User> USER = ScopedValue.newInstance();

ScopedValue.runWhere(USER, currentUser, () -> {
    // 在这个作用域内 USER.get() 返回 currentUser
    doWork();
});
// 作用域结束,自动清理,不会内存泄漏

5.6 性能基准测试数据(参考)

场景平台线程池(200线程)虚拟线程提升
10 万并发 HTTP 请求OOM / 队列爆炸正常运行
1 万并发 DB 查询RT p99 = 5sRT p99 = 200ms25x
内存占用(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(桶头)
数据结构分段数组 + 链表数组 + 链表 + 红黑树
锁方式ReentrantLockCAS + synchronized
并发度16(可配置)理论无限(等于桶数)

put 操作关键流程

  1. 计算 hash,定位桶

  2. 桶为空 → CAS 插入

  3. 桶不为空 → synchronized 锁住桶头节点,遍历链表/红黑树插入

  4. 链表长度 ≥ 8 → 转红黑树

  5. 元素总数超过阈值 → 扩容(多线程协助扩容)

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 对比

维度ConcurrentHashMapHashtableCollections.synchronizedMap
CAS + synchronized(桶级)整表 synchronized整表 synchronized
性能
nullkey/value 都不允许都不允许取决于底层 Map
迭代弱一致性(不抛 CME)fail-fastfail-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

维度ABQLBQ
一把锁(put/take 互斥)两把锁(put/take 分离)
吞吐略低略高
内存预分配,无 GC动态分配 Node,有 GC
边界必须指定容量可选(默认 Integer.MAX_VALUE = 几乎无界)

生产建议:优先用 ArrayBlockingQueue,因为有界能防止 OOM。LinkedBlockingQueue 不设界的话,下游慢了会无限积压。


7. 并发工具类实战

7.1 CountDownLatch CyclicBarrier Semaphore 对比

维度CountDownLatchCyclicBarrierSemaphore
作用等 N 个操作完成N 个线程互相等到齐限流(控制并发数)
可重用不可(一次性的)可(reset 后重来)可(release/acquire)
计数器只能减只能减(到 0 后自动重置)可增可减
典型场景并行任务汇总结果多线程分阶段同步点数据库连接池限流
谁先执行主线程 await()所有线程 await()任意线程 acquire()

CountDownLatch 实战

// 场景:汇总 3 个服务的结果
CountDownLatch latch = new CountDownLatch(3);
List<Future<Result>> futures = new ArrayList<>();

for (Service svc : services) {
    futures.add(executor.submit(() -> {
        try {
            return svc.query();
        } finally {
            latch.countDown();
        }
    }));
}

latch.await(5, TimeUnit.SECONDS); // 带超时的等
Result merged = mergeResults(futures);

CyclicBarrier 实战

// 场景:分布式压测,所有线程同时起跑
CyclicBarrier barrier = new CyclicBarrier(100, () -> {
    System.out.println("所有线程就绪,开始!");
});

for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        prepareData();
        barrier.await(); // 等 100 个都到这
        startPressTest(); // 同时开跑
    });
}

Semaphore 实战

// 场景:限制同时访问数据库的并发数
Semaphore semaphore = new Semaphore(10); // 最多 10 个并发

public void queryDB() throws InterruptedException {
    semaphore.acquire();
    try {
        return jdbcTemplate.query(sql);
    } finally {
        semaphore.release();
    }
}

7.2 CompletableFuture 异步编程

版本背景:CompletableFuture 是 JDK 8 引入的,是 JDK 8 在异步编程领域最重要的贡献。它把 JDK 7 及以前的 Future(只能阻塞获取结果)升级为可组合、可链式的异步编程模型。所以面试官问 CompletableFuture,其实是考 JDK 8 的知识点。

链式调用

CompletableFuture.supplyAsync(() -> getUser(userId))          // 获取用户
    .thenApplyAsync(user -> getOrders(user))                   // 获取订单
    .thenApply(orders -> calculateTotal(orders))               // 计算总额
    .exceptionally(ex -> {                                     // 异常处理
        log.error("查询失败", ex);
        return 0.0;
    });

组合操作

// 并行查两个服务,结果合并
CompletableFuture<User> userFuture = CompletableFuture.supplyAsync(() -> getUser(id));
CompletableFuture<List<Order>> ordersFuture = CompletableFuture.supplyAsync(() -> getOrders(id));

CompletableFuture<String> result = userFuture.thenCombine(ordersFuture, (user, orders) -> {
    return user.getName() + " 共有 " + orders.size() + " 个订单";
});

// allOf:等所有完成
CompletableFuture<Void> all = CompletableFuture.allOf(future1, future2, future3);

// anyOf:只要一个完成
CompletableFuture<Object> first = CompletableFuture.anyOf(future1, future2, future3);

异常处理三件套

future.exceptionally(ex -> defaultValue);           // 兜底
future.whenComplete((result, ex) -> { ... });       // 不改变结果,只做收尾
future.handle((result, ex) -> {                     // 可以修改结果
    if (ex != null) return fallback;
    return result;
});

7.3 Structured Concurrency(JDK 21 Preview)

⚠️ 版本限制:Structured Concurrency 是 JDK 21 专属的 Preview 特性,JDK 8 和 JDK 17 都没有。如果在 JDK 8/17 环境下无法使用,需要手动实现(用 CompletableFuture + 异常传播)。面试时如果公司还在用 JDK 8/17,不要主动提这个特性做方案,但可以展示"我了解新特性"。

解决的痛点:多线程场景下,一个子任务异常了,其他子任务还在跑,浪费资源。

// JDK 21 结构化并发
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Subtask<User> userTask = scope.fork(() -> getUser(userId));
    Subtask<List<Order>> ordersTask = scope.fork(() -> getOrders(userId));
    
    scope.join();            // 等所有子任务完成
    scope.throwIfFailed();   // 任一子任务异常则抛出
    
    // 都成功了
    return new UserProfile(userTask.get(), ordersTask.get());
}
// scope 关闭时,如果还有子任务在跑,会自动关闭(ShutdownOnFailure 策略)

核心思想:把并发任务当作一个整体,同生共死。

  • ShutdownOnFailure:任一失败,关闭其他

  • ShutdownOnSuccess:任一成功,关闭其他

  • 可以和虚拟线程完美搭配

面试怎么说: "Structured Concurrency 是 JDK 21 的预览特性,它把并发任务的生命周期绑定到一个 scope 上。好处是代码结构清晰——你能从代码块直接看出哪些任务是并行的。异常处理也更优雅,一个子任务失败,scope 自动关闭其他子任务,不会出现僵尸任务。"


8. 死锁排查实战

8.1 死锁的 4 个必要条件

条件含义破法
互斥资源同一时刻只能被一个线程持有用可重入的并发容器替代
持有并等待持有资源的同时请求新资源一次性申请所有资源
不可抢占资源只能主动释放用 tryLock + 超时
循环等待A 等 B、B 等 A 形成环统一加锁顺序

破任意一个条件就能避免死锁。 生产中最常用的方法:

  1. 统一加锁顺序(破循环等待)

  2. tryLock + 超时(破持有并等待 + 不可抢占)

8.2 jstack 排查死锁

# 找到 Java 进程 PID
jps -l

# dump 线程栈
jstack <PID> > thread_dump.txt

jstack 会自动检测死锁并输出:

Found one Java-level deadlock:
=============================
"Thread-A":
  waiting to lock monitor 0x00007f... (object 0x000000..., a java.lang.Object),
  which is held by "Thread-B"
"Thread-B":
  waiting to lock monitor 0x00007f... (object 0x000000..., a java.lang.Object),
  which is held by "Thread-A"

8.3 Arthas 排查

# 启动 Arthas
java -jar arthas-boot.jar

# 查看线程状态
thread

# 查看死锁(直接定位)
thread -b

# 查看特定线程堆栈
thread <线程ID>

# 查看 CPU 占用最高的线程
thread -n 3

8.4 生产案例

事故:转账导致的死锁

// 有问题的代码
public void transfer(Account from, Account to, BigDecimal amount) {
    synchronized (from) {           // 先锁 from
        synchronized (to) {         // 再锁 to
            from.debit(amount);
            to.credit(amount);
        }
    }
}

// 线程 1:transfer(A, B, 100)  → 锁 A,然后锁 B
// 线程 2:transfer(B, A, 200)  → 锁 B,然后锁 A
// 死锁!

修复方案:按 ID 排序加锁

public void transfer(Account from, Account to, BigDecimal amount) {
    Account first = from.getId() < to.getId() ? from : to;
    Account second = from.getId() < to.getId() ? to : from;
    
    synchronized (first) {
        synchronized (second) {
            from.debit(amount);
            to.credit(amount);
        }
    }
}

排查链路

告警:接口超时 → 查监控发现线程池满 → 查线程状态(大量 BLOCKED)
→ jstack 发现死锁 → 定位代码行号 → 发现转账方法加锁顺序不一致
→ 修复方案:按 Account ID 排序加锁
→ 加监控:thread -b 定期检测

9. 面试高频问答

Q1:线程和进程的区别?

初中级回答:进程是资源分配的单位,线程是 CPU 调度的单位。进程之间内存隔离,线程之间共享内存。

高级补充:进程有独立的虚拟地址空间,线程共享进程的资源(堆、方法区、文件描述符等),但每个线程有自己的栈和程序计数器。线程切换的代价比进程切换小(不用切换页表),但线程间需要同步机制来保证数据一致性。

Q2:线程有哪些状态?

标准回答(6 个)

状态含义
NEW创建了还没 start
RUNNABLE就绪+运行中(JVM 层面)
BLOCKED等锁
WAITINGwait()/join()/park()
TIMED_WAITINGsleep(time)/wait(time)
TERMINATED执行完毕

高级加分:在 JDK 21 里,虚拟线程还有一个 PINNED 状态(被钉在载体线程上无法卸载),通过 thread.state() 或者 JFR 事件可以看到。

版本差异:JDK 8 和 JDK 17 的线程状态只有上述 6 个,没有 PINNED 状态。PINNED 是虚拟线程(JDK 21)特有的概念。

Q3:sleep 和 wait 的区别?

维度sleepwait
所属Thread 的方法Object 的方法
释放锁不释放释放
唤醒时间到了自动醒notify/notifyAll
使用位置任意必须在 synchronized 块内

Q4:volatile 能保证原子性吗?

不能。 volatile 保证可见性和有序性。i++ 这种复合操作不是原子的(读-改-写三步),需要 AtomicIntegerLongAdder

Q5:ThreadLocal 原理?内存泄漏怎么解决?

原理:每个 Thread 内部有一个 ThreadLocalMap,key 是 ThreadLocal 的弱引用,value 是强引用。

内存泄漏:线程复用(线程池)时,如果 ThreadLocal 用完不清理,value 一直存在。

解决:用完必须 remove(),放在 finally 里。

try {
    threadLocal.set(value);
    doWork();
} finally {
    threadLocal.remove(); // 必须!
}

虚拟线程场景下 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 原子指令。

三个问题

  1. ABA 问题 → AtomicStampedReference(加版本号)

  2. 自旋开销 → 长时间 CAS 失败浪费 CPU

  3. 只能保证一个变量的原子性 → 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 并发核心变化

  1. ConcurrentHashMap 重写:从 Segment 分段锁改为 Node + CAS + synchronized 桶级锁,并发度大幅提升

  2. CompletableFuture 引入:可组合的异步编程模型,终结了 Future 只能阻塞的尴尬

  3. StampedLock 引入:比 ReentrantReadWriteLock 更高效的乐观读锁

  4. LongAdder / LongAccumulator 引入:高并发计数场景下性能碾压 AtomicLong

  5. @sun.misc.Contended 注解:解决伪共享问题

  6. ForkJoinPool 完善:新增 commonPool()execute() 等异步方法

  7. synchronized 锁升级机制成熟:偏向锁 → 轻量级锁 → 重量级锁,四级升级路径

"JDK 8 是并发编程的集大成者,现在绝大多数并发基础概念(ConcurrentHashMap 的桶级锁、CompletableFuture 的链式调用、LongAdder 的 Striped 思想)都在这个版本定型。面试 JDK 8 版本时,能把 ConcurrentHashMap 的 put 流程完整讲清楚,就已经展现了扎实的功底。"

JDK 17 并发相关变化

  1. 偏向锁默认禁用(JDK 15 起,JDK 17 延续):废弃偏向锁,简化 JVM 锁实现

  2. synchronized 内部实现重构:改用基于对象头的 C++ 实现,更简洁、更易维护

  3. Thread-Local Handshake(JEP 312):改进的线程握手机制,减少全局安全点,提升 JVM 响应性

  4. AppCDS(Application Class-Data Sharing):改进应用启动性能(间接影响并发场景的启动延迟)

  5. Removed RMI Activation / Applet API:清理了遗留并发代码

"JDK 17 不是并发特性的爆发版本,但它的意义在于清理——废弃偏向锁、重构 synchronized 实现,这些都是为后续虚拟线程的引入做准备。如果你面试时说自己用 JDK 17,面试官可能会问:'JDK 17 和 JDK 8 相比,synchronized 有什么变化?'要答得出偏向锁被废弃了。"

JDK 21 并发核心变化

  1. 虚拟线程正式发布(JEP 444):轻量级 M:N 调度,IO 密集型场景性能飞跃

  2. Structured Concurrency Preview(结构化并发):并发任务同生共死,异常处理更优雅

  3. ScopedValue Preview(替代 ThreadLocal):解决虚拟线程场景下 ThreadLocal 的内存膨胀

  4. 偏向锁默认禁用(JDK 15 开始,21 早已生效)

  5. ForkJoinPool 优化:作为虚拟线程的默认调度器,性能提升

  6. 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 生成,请遵循相关法律法规及《人工智能生成合成内容标识办法》使用与传播。

技术面试笔记 / 02_并发编程速查手册_2026 0 0 cosolar
2026-08-30T02:14:22.235218212Z 2026-08-30T02:17:12.399969551Z