并发编程面试题

参见实体页:[[jdk8]]、[[jdk21]],概念页:[[virtual-threads]]


一、Java 内存模型(JMM)

Q1: JMM 是什么?解决了什么问题?⭐⭐⭐

答案:
JMM(Java Memory Model)定义了多线程程序中变量的读写规则,屏蔽了不同硬件和操作系统的内存访问差异。

三大问题:

  • 可见性:一个线程修改的值,另一个线程不一定能立即看到(CPU 缓存)
  • 原子性:复合操作(如 i++)不是原子的
  • 有序性:编译器/CPU 会对指令重排序,多线程下可能出错

JMM 的解决方案:

  • happens-before 规则:定义操作间的可见性保证
  • volatile:禁止重排序 + 强制刷新主内存
  • synchronized/Lock:保证原子性 + 可见性 + 有序性

happens-before 核心规则(8条):

  1. 程序顺序规则:同一线程内,前面的操作 happens-before 后面的操作
  2. volatile 规则:volatile 写 happens-before 后续的 volatile 读
  3. 锁规则:unlock happens-before 后续的 lock
  4. 线程启动规则:Thread.start() happens-before 线程内任何操作
  5. 线程终止规则:线程所有操作 happens-before Thread.join() 返回
  6. 传递性:A hb B,B hb C → A hb C

二、synchronized 深度

Q2: synchronized 的锁升级过程?⭐⭐⭐

答案:

无锁 → 偏向锁 → 轻量级锁 → 重量级锁
状态 触发条件 Mark Word 内容 性能
无锁 初始状态 hashCode + 分代年龄 最高
偏向锁 只有一个线程访问 线程ID + epoch 接近无锁
轻量级锁 多线程交替访问(无竞争) 指向栈帧锁记录的指针 CAS 自旋
重量级锁 多线程同时竞争 指向 Monitor 对象的指针 OS Mutex,线程挂起

偏向锁细节:

  • JDK 15 起默认禁用(-XX:-UseBiasedLocking),JDK 18 彻底移除
  • 偏向锁撤销需要 STW,高并发场景反而有性能损耗

轻量级锁细节:

  • 线程在栈帧创建 Lock Record,CAS 将 Mark Word 替换为指向 Lock Record 的指针
  • 自旋超过阈值(默认 10 次,或自适应自旋)→ 膨胀为重量级锁

追问:synchronized 修饰静态方法和实例方法的区别?

  • 实例方法:锁是 this 对象
  • 静态方法:锁是 Class 对象(类级别锁)
  • 两者不互斥,可以同时被不同线程持有

Q3: synchronized 和 ReentrantLock 的区别?⭐⭐⭐

维度 synchronized ReentrantLock
实现 JVM 内置(monitorenter/exit) JDK 实现(AQS)
锁释放 自动(异常也会释放) 必须手动 unlock()(finally)
可中断 不支持 lockInterruptibly()
超时获取 不支持 tryLock(timeout)
公平锁 不支持 new ReentrantLock(true)
条件变量 wait/notify(一个) Condition(多个)
锁绑定多条件 不支持 支持,精确唤醒

何时选 ReentrantLock:

  • 需要可中断等待(防止死锁)
  • 需要超时获取锁
  • 需要多个等待队列(生产者-消费者多条件)
  • 需要公平锁(防止线程饥饿)

三、volatile 深度

Q4: volatile 的原理?双重检查锁为什么需要 volatile?⭐⭐⭐

原理:

  • 可见性:写操作后插入 StoreLoad 屏障,强制刷新到主内存;读操作前插入 LoadLoad 屏障
  • 禁止重排序:通过 4 种内存屏障(LoadLoad/StoreStore/LoadStore/StoreLoad)

双重检查锁(DCL)单例:

public class Singleton {
    // 必须加 volatile!
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {                    // 第一次检查(无锁)
            synchronized (Singleton.class) {
                if (instance == null) {            // 第二次检查(有锁)
                    instance = new Singleton();    // 问题在这里
                }
            }
        }
        return instance;
    }
}

为什么必须加 volatile:
new Singleton() 分三步:

  1. 分配内存
  2. 初始化对象
  3. 将引用指向内存地址

不加 volatile,步骤 2 和 3 可能被重排序为 1→3→2。
线程 A 执行到步骤 3(引用非 null 但对象未初始化),线程 B 第一次检查通过,拿到未初始化的对象。


四、CAS 与原子类

Q5: CAS 的原理和 ABA 问题?⭐⭐⭐

CAS(Compare And Swap):

  • 原子操作:比较内存值与期望值,相等则更新为新值,否则重试(自旋)
  • 底层:CPU 的 CMPXCHG 指令,硬件级原子性
  • Java 中通过 Unsafe.compareAndSwapXxx() 调用

ABA 问题:

  • 值从 A → B → A,CAS 认为没有变化,但实际已被修改过
  • 场景:链表头节点被删除再插入,CAS 成功但链表结构已变

解决方案:

// AtomicStampedReference:加版本号
AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
int[] stampHolder = new int[1];
String val = ref.get(stampHolder);  // 同时获取值和版本号
ref.compareAndSet("A", "B", stampHolder[0], stampHolder[0] + 1);

// AtomicMarkableReference:加布尔标记(只关心是否被修改过)
AtomicMarkableReference<String> markRef = new AtomicMarkableReference<>("A", false);

CAS 的三大缺点:

  1. ABA 问题:用 AtomicStampedReference 解决
  2. 自旋开销:竞争激烈时 CPU 空转,可用 LongAdder 替代 AtomicLong
  3. 只能保证一个变量的原子性:多变量用 AtomicReference 包装对象,或用锁

Q6: LongAdder 为什么比 AtomicLong 快?⭐⭐

AtomicLong 的问题:
高并发下多线程竞争同一个 value,CAS 失败率高,大量自旋浪费 CPU。

LongAdder 的设计:

  • 分段思想:维护一个 base 值 + Cell[] 数组
  • 无竞争时:直接 CAS 更新 base
  • 有竞争时:每个线程映射到一个 Cell,各自累加
  • 读取时:sum() = base + Σ Cell[i].value
线程1 → Cell[0] += 1
线程2 → Cell[1] += 1   (各自操作,无竞争)
线程3 → Cell[2] += 1
sum() = base + Cell[0] + Cell[1] + Cell[2]

缺点: sum() 不是强一致的,读取时其他线程可能还在写入。
适合统计计数(如 QPS),不适合需要精确值的场景(用 AtomicLong)。


五、AQS 深度

Q7: AQS 的原理?ReentrantLock 如何基于 AQS 实现?⭐⭐⭐

AQS 核心结构:

state(int):同步状态
CLH 变体队列:双向链表,Node{thread, waitStatus, prev, next}

waitStatus 取值:

含义
0 初始状态
CANCELLED(1) 线程已取消等待
SIGNAL(-1) 后继节点需要被唤醒
CONDITION(-2) 节点在条件队列中等待
PROPAGATE(-3) 共享模式下传播唤醒

ReentrantLock 加锁流程(非公平锁):

lock()
  → CAS(state, 0, 1) 成功 → 设置 exclusiveOwnerThread = 当前线程
  → 失败 → acquire(1)
      → tryAcquire():再次 CAS,或检查是否重入(state++)
      → 失败 → addWaiter(Node.EXCLUSIVE) 入队
      → acquireQueued():自旋检查前驱是否为 head,是则再次 tryAcquire
      → 失败 → LockSupport.park() 挂起

ReentrantLock 解锁流程:

unlock() → release(1)
  → tryRelease():state--,为 0 时清除 exclusiveOwnerThread
  → unparkSuccessor():唤醒队列中第一个有效的等待节点

Condition 原理:

  • await():将当前节点从 CLH 队列移到 Condition 队列,释放锁,park
  • signal():将 Condition 队列头节点移回 CLH 队列,等待重新竞争锁

Q8: Semaphore、CountDownLatch、CyclicBarrier 的区别?⭐⭐

工具 计数方向 可重用 典型场景
Semaphore 许可证(acquire/release) 限流、连接池
CountDownLatch 倒计时(countDown/await) 等待多个任务完成
CyclicBarrier 等待所有线程到达屏障 分阶段并行计算
// CountDownLatch:主线程等待 3 个子任务完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
    executor.submit(() -> { doWork(); latch.countDown(); });
}
latch.await();  // 主线程阻塞直到 count=0

// CyclicBarrier:3 个线程都到达后一起继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("all ready"));
// 每个线程调用 barrier.await(),第 3 个到达时触发 barrierAction,然后全部释放

六、线程池深度

Q9: 线程池的核心参数和工作流程?⭐⭐⭐

核心参数:

  • corePoolSize:核心线程数,常驻(即使空闲)
  • maximumPoolSize:最大线程数
  • keepAliveTime:非核心线程空闲存活时间
  • workQueue:任务队列
  • threadFactory:线程工厂(命名、优先级、daemon)
  • rejectedExecutionHandler:拒绝策略

工作流程:

提交任务
  → 当前线程数 < corePoolSize?→ 创建核心线程执行
  → 队列未满?→ 加入队列等待
  → 当前线程数 < maximumPoolSize?→ 创建非核心线程执行
  → 执行拒绝策略

队列选型:

队列 特点 适用场景
LinkedBlockingQueue(无界) 无限堆积,可能 OOM 不推荐
LinkedBlockingQueue(有界) 有背压 I/O 密集型
ArrayBlockingQueue 有界,公平可选 需要公平性
SynchronousQueue 不存储,直接交付 CPU 密集型,快速响应
PriorityBlockingQueue 按优先级出队 任务有优先级

拒绝策略:

  • AbortPolicy(默认):抛 RejectedExecutionException
  • CallerRunsPolicy:由提交线程执行(降速保护,防止 OOM)
  • DiscardPolicy:静默丢弃
  • DiscardOldestPolicy:丢弃队列最老的任务

Q10: 如何合理配置线程池大小?⭐⭐⭐

理论公式(Little’s Law):

最优线程数 = CPU 核数 × (1 + 等待时间/计算时间)
任务类型 推荐 corePoolSize 推荐队列
CPU 密集型 CPU 核数 + 1 SynchronousQueue 或小有界队列
I/O 密集型 CPU 核数 × 2 有界 LinkedBlockingQueue
混合型 根据 I/O 比例调整 有界队列

实际配置(8 核,I/O 密集型):

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    16, 32,
    60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000),
    new ThreadFactoryBuilder().setNameFormat("biz-pool-%d").build(),
    new ThreadPoolExecutor.CallerRunsPolicy()
);
// 允许核心线程超时回收(低峰期节省资源)
executor.allowCoreThreadTimeOut(true);

追问:为什么不推荐 Executors 工厂方法?

  • newFixedThreadPool/newSingleThreadExecutor:无界队列,任务堆积 OOM
  • newCachedThreadPool:无限创建线程,线程数爆炸 OOM
  • newScheduledThreadPool:同 newFixedThreadPool 问题

Q11: 线程池的监控和动态调参?⭐⭐

监控指标:

executor.getPoolSize()           // 当前线程数
executor.getActiveCount()        // 活跃线程数
executor.getQueue().size()       // 队列积压数
executor.getCompletedTaskCount() // 已完成任务数
executor.getTaskCount()          // 总提交任务数
// 队列使用率 = queue.size() / (queue.size() + queue.remainingCapacity())

动态调参(JDK 原生支持):

// ThreadPoolExecutor 支持运行时修改参数
executor.setCorePoolSize(newCore);
executor.setMaximumPoolSize(newMax);
// 注意:先调大 max 再调大 core;先调小 core 再调小 max

线上 CPU 飙升排查:

top -c                          # 找高 CPU 的 PID
top -H -p {pid}                 # 找高 CPU 的线程 TID
printf "%x
" {tid}             # TID 转十六进制
jstack {pid} | grep -A 30 {hex} # 找对应线程栈

常见原因:死循环、频繁 Full GC、正则回溯、JSON 循环引用


七、ThreadLocal 深度

Q12: ThreadLocal 的原理和内存泄漏?⭐⭐⭐

原理:

Thread
  └── ThreadLocalMap(Entry[],开放地址法解决冲突)
        └── Entry{key=ThreadLocal(弱引用), value=值(强引用)}

ThreadLocal.get() 实际执行:

Thread t = Thread.currentThread();
ThreadLocalMap map = t.threadLocals;
ThreadLocalMap.Entry e = map.getEntry(this);  // this = ThreadLocal 实例
return e.value;

内存泄漏原因:

  • Entry 的 key 是 ThreadLocal 的弱引用,GC 后 key 变为 null
  • 但 value 是强引用,线程池中线程不销毁,value 永远无法回收
  • 形成:Thread → ThreadLocalMap → Entry{null, value} 的强引用链

解决:

try {
    threadLocal.set(value);
    // ... 业务逻辑
} finally {
    threadLocal.remove();  // 必须!尤其在线程池中
}

追问:ThreadLocalMap 为什么用弱引用作 key?

  • 如果用强引用:ThreadLocal 对象即使没有外部引用,也因 ThreadLocalMap 持有而无法 GC,造成更严重的泄漏
  • 用弱引用:ThreadLocal 无外部引用时可以被 GC,key 变 null,下次 get/set/remove 时会清理 null key 的 Entry(expungeStaleEntry)

InheritableThreadLocal:

  • 子线程可以继承父线程的 ThreadLocal 值(Thread 构造时复制父线程的 inheritableThreadLocals)
  • 线程池中不适用(线程复用,不会重新构造)→ 用 TransmittableThreadLocal(TTL)

八、并发集合

Q13: ConcurrentHashMap 的原理(JDK 8)?⭐⭐⭐

结构: 数组 + 链表/红黑树,不再使用分段锁(Segment)

put 流程:

1. 计算 hash,定位桶位置
2. 桶为空 → CAS 插入(无锁)
3. 桶头节点 hash == MOVED(-1) → 协助扩容(helpTransfer)
4. 否则 → synchronized(桶头节点) 加锁,链表/树插入
5. 链表长度 ≥ 8 且数组长度 ≥ 64 → 转红黑树

size() 的实现:

  • 维护 baseCount + CounterCell[](类似 LongAdder)
  • 无竞争时 CAS 更新 baseCount,有竞争时更新 CounterCell
  • size() = baseCount + Σ CounterCell[i](非强一致)

扩容(transfer):

  • 多线程协助扩容:每个线程负责迁移一段桶(stride = max(n/8/NCPU, 16))
  • 迁移完的桶放置 ForwardingNode(hash=MOVED),其他线程 put 时检测到会协助

追问:JDK 7 和 JDK 8 的区别?

  • JDK 7:Segment 分段锁(16个),锁粒度是 Segment
  • JDK 8:synchronized + CAS,锁粒度是单个桶,并发度更高

Q14: BlockingQueue 的实现原理?⭐⭐

ArrayBlockingQueue(有界,数组):

  • 一把 ReentrantLock + 两个 Condition(notEmpty、notFull)
  • put:队列满时 notFull.await();take:队列空时 notEmpty.await()

LinkedBlockingQueue(可有界,链表):

  • 两把锁:putLock(生产者)+ takeLock(消费者),读写分离,吞吐量更高
  • 原子计数器 AtomicInteger count 协调两把锁

SynchronousQueue:

  • 不存储元素,put 必须等待 take,直接交付
  • 公平模式:FIFO 队列;非公平模式:LIFO 栈

九、死锁与并发问题排查

Q15: 死锁的四个必要条件和如何避免?⭐⭐⭐

四个必要条件:

  1. 互斥:资源同时只能被一个线程持有
  2. 持有并等待:线程持有资源的同时等待其他资源
  3. 不可剥夺:线程持有的资源不能被强制剥夺
  4. 循环等待:线程间形成资源等待环

避免策略:

  • 固定加锁顺序:所有线程按相同顺序获取锁(破坏循环等待)
  • 超时获取tryLock(timeout) 获取失败则释放已持有的锁(破坏持有并等待)
  • 一次性获取所有资源:原子地获取所有需要的锁(破坏持有并等待)
  • 资源排序:给资源编号,按编号顺序加锁

死锁检测:

jstack {pid} | grep -A 20 "deadlock"
# 或使用 jconsole / VisualVM 的线程面板

Q16: 线程安全问题的排查思路?⭐⭐

常见线程安全问题:

  1. 竞态条件(Race Condition):多线程对共享变量的复合操作(check-then-act)
  2. 可见性问题:一个线程的修改对其他线程不可见
  3. 有序性问题:指令重排序导致逻辑错误

排查工具:

  • jstack:线程栈快照,检测死锁和线程状态
  • jmap -histo:堆对象统计,检测内存泄漏
  • Arthas watch/trace:动态追踪方法调用
  • ThreadSanitizer(TSan):C/C++ 工具,Java 可用 -Djdk.attach.allowAttachSelf=true + async-profiler

十、JDK 21 虚拟线程与并发新特性

Q17: 虚拟线程(Virtual Threads)是什么?解决了什么问题?⭐⭐⭐

详见概念页:[[virtual-threads]]

传统平台线程的问题:

  • 每个线程对应一个 OS 线程,栈内存约 1MB
  • 线程数受限(通常几千),I/O 阻塞时线程空转浪费资源
  • 线程池模型复杂,需要手动调参

虚拟线程:

  • JDK 21 正式发布(Project Loom)
  • 由 JVM 调度,挂载在少量平台线程(carrier thread)上
  • I/O 阻塞时自动卸载(unmount),不占用 carrier thread
  • 可创建数百万个虚拟线程
// 创建虚拟线程
Thread vt = Thread.ofVirtual().start(() -> {
    // I/O 阻塞时自动让出 carrier thread
    var result = httpClient.send(request, bodyHandler);
});

// 虚拟线程池(每个任务一个虚拟线程)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
    for (int i = 0; i < 100_000; i++) {
        executor.submit(() -> handleRequest());
    }
}

注意事项:

  • 不适合 CPU 密集型任务(无法让出 carrier thread)
  • synchronized 块内阻塞会 pin 住 carrier thread(JDK 21 已部分修复,JDK 24 完全修复)
  • 推荐用 ReentrantLock 替代 synchronized(虚拟线程场景)
  • ThreadLocal 在虚拟线程中仍可用,但推荐用 ScopedValue(JDK 21 Preview)

Q18: StructuredConcurrency(结构化并发)是什么?⭐⭐

JDK 21 Preview,JDK 25 正式:

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> user = scope.fork(() -> fetchUser(id));
    Future<Order> order = scope.fork(() -> fetchOrder(id));
    scope.join();           // 等待所有子任务
    scope.throwIfFailed();  // 任一失败则抛出
    return new Response(user.resultNow(), order.resultNow());
}
// scope 关闭时自动取消未完成的子任务

优势:

  • 子任务生命周期绑定到父任务(不会泄漏)
  • 任一子任务失败自动取消其他子任务
  • 比 CompletableFuture 更清晰的错误处理

十一、应用场景总结

场景 推荐方案 关键点
简单互斥 synchronized 代码简洁,JVM 优化好
需要超时/可中断 ReentrantLock tryLock + finally unlock
高并发计数 LongAdder 分段累加,sum() 非强一致
精确原子操作 AtomicXxx CAS,适合低竞争
线程间通信 BlockingQueue 生产者-消费者模式
并发 Map ConcurrentHashMap 桶级锁,高并发读写
限流/信号量 Semaphore 控制并发访问数量
等待多任务 CountDownLatch 一次性,不可重置
分阶段协调 CyclicBarrier 可重用,支持 barrierAction
I/O 密集型高并发 虚拟线程(JDK 21) 百万级并发,简化线程池
线程隔离数据 ThreadLocal 用完必须 remove()

更多推荐