【面试特集】java并发编程面试题
并发编程面试题
参见实体页:[[jdk8]]、[[jdk21]],概念页:[[virtual-threads]]
一、Java 内存模型(JMM)
Q1: JMM 是什么?解决了什么问题?⭐⭐⭐
答案:
JMM(Java Memory Model)定义了多线程程序中变量的读写规则,屏蔽了不同硬件和操作系统的内存访问差异。
三大问题:
- 可见性:一个线程修改的值,另一个线程不一定能立即看到(CPU 缓存)
- 原子性:复合操作(如 i++)不是原子的
- 有序性:编译器/CPU 会对指令重排序,多线程下可能出错
JMM 的解决方案:
- happens-before 规则:定义操作间的可见性保证
- volatile:禁止重排序 + 强制刷新主内存
- synchronized/Lock:保证原子性 + 可见性 + 有序性
happens-before 核心规则(8条):
- 程序顺序规则:同一线程内,前面的操作 happens-before 后面的操作
- volatile 规则:volatile 写 happens-before 后续的 volatile 读
- 锁规则:unlock happens-before 后续的 lock
- 线程启动规则:
Thread.start()happens-before 线程内任何操作 - 线程终止规则:线程所有操作 happens-before
Thread.join()返回 - 传递性: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() 分三步:
- 分配内存
- 初始化对象
- 将引用指向内存地址
不加 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 的三大缺点:
- ABA 问题:用 AtomicStampedReference 解决
- 自旋开销:竞争激烈时 CPU 空转,可用 LongAdder 替代 AtomicLong
- 只能保证一个变量的原子性:多变量用 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 队列,释放锁,parksignal():将 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:无界队列,任务堆积 OOMnewCachedThreadPool:无限创建线程,线程数爆炸 OOMnewScheduledThreadPool:同 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: 死锁的四个必要条件和如何避免?⭐⭐⭐
四个必要条件:
- 互斥:资源同时只能被一个线程持有
- 持有并等待:线程持有资源的同时等待其他资源
- 不可剥夺:线程持有的资源不能被强制剥夺
- 循环等待:线程间形成资源等待环
避免策略:
- 固定加锁顺序:所有线程按相同顺序获取锁(破坏循环等待)
- 超时获取:
tryLock(timeout)获取失败则释放已持有的锁(破坏持有并等待) - 一次性获取所有资源:原子地获取所有需要的锁(破坏持有并等待)
- 资源排序:给资源编号,按编号顺序加锁
死锁检测:
jstack {pid} | grep -A 20 "deadlock"
# 或使用 jconsole / VisualVM 的线程面板
Q16: 线程安全问题的排查思路?⭐⭐
常见线程安全问题:
- 竞态条件(Race Condition):多线程对共享变量的复合操作(check-then-act)
- 可见性问题:一个线程的修改对其他线程不可见
- 有序性问题:指令重排序导致逻辑错误
排查工具:
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() |
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐



所有评论(0)