最后更新: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.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+ 重要变更:偏向锁默认禁用!

-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++ 这种复合操作不是原子的(读-改-写三步),需要 AtomicInteger 或 LongAdder。

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