适用读者:准备 Java 后端面试(一面到三面)的人;想把手里的"并发八股"变成可追问的体系的人。
前置:S04(Java 并发)、S25-J01(题库设计)、J05(原理速查)。
覆盖旧文:2773–2775、3016–3035(JUC 教程 20 章)、3131–3145(线程池教程 14 章)→ 38 篇 / 约 38 万字符。
扩展点:E-S25-021(并发答题三层骨架)、E-S25-022(Lock/AQS 与 synchronized 的分界)、E-S25-023(线程池七参数与执行流程的追问链)。
下一篇:S25-J08 · JVM 调优题库。
1. 一句话结论
并发题不问"是什么",问"为什么这样设计、什么条件下失效"。 标准答法是三层:结论 → 机制 → 边界(何时不适用)。只答结论的人,会被第二个追问打回原形。
出处:JUC 教程 20 章(
A-3016–A-3035)、线程池教程 14 章(A-3131–A-3145),本系列按"答题骨架"重新组织。
2. 答题三层骨架(E-S25-021)
第一层 结论:一句话给答案(10 秒内)
第二层 机制:靠什么实现(数据结构/关键类/状态机)
第三层 边界:什么条件下失效、代价是什么、怎么验证
| 题 | 只答一层(常见) | 三层答法 |
|---|---|---|
synchronized 和 Lock 区别 |
"Lock 更灵活" | 结论:synchronized 是 JVM 关键字自动释放;机制:monitor + 对象头 + 偏向/轻量/重量锁升级;边界:JDK 15+ 已废弃偏向锁,低竞争下 synchronized 常更快,需要可中断/超时/多条件才用 Lock |
ThreadLocal 会不会内存泄漏 |
"会,记得 remove" | 结论:线程池里必须 remove;机制:Thread→ThreadLocalMap,key 弱引用、value 强引用,线程长活则 value 悬空;边界:InheritableThreadLocal 在池化线程下传播不可靠,改用显式传参或 TransmittableThreadLocal |
volatile 能不能保证原子性 |
"不能" | 结论:只保可见性与有序性;机制:内存屏障 + 禁止重排,i++ 是读改写三步;边界:long/double 的读写原子性在 64 位 JVM 已保证,别拿旧教材当标准答案 |
判据:面试官追问的第二句,几乎总落在"边界"上。 提前准备边界,比多背十个类名有用。
3. JUC 全景:一张图定位所有问题
| 层 | 组件 | 高频问法 |
|---|---|---|
| 锁 | ReentrantLock、ReentrantReadWriteLock、StampedLock |
公平/非公平、可重入怎么实现 |
| 同步器 | AQS(state + CLH 队列)、Condition |
AQS 的独占/共享模式、await/signal 与 wait/notify 差别 |
| 原子类 | AtomicInteger、LongAdder、AtomicReference、ABA |
CAS 三问题(ABA、自旋开销、只能保一个变量) |
| 容器 | ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue |
1.7 分段锁 → 1.8 CAS+synchronized 的演进理由 |
| 任务 | Future、CompletableFuture、ForkJoinPool |
组合式异步、工作窃取、commonPool 阻塞风险 |
| 工具 | CountDownLatch、CyclicBarrier、Semaphore、Phaser |
四者语义差别与选型 |
| 线程封闭 | ThreadLocal、不可变对象 |
池化线程下的清理 |
3.1 synchronized 与 Lock 的分界(E-S25-022)
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 获取方式 | 关键字,自动释放 | 手动 lock()/unlock()(必须 try/finally) |
| 可中断 | 否 | lockInterruptibly() |
| 超时 | 否 | tryLock(timeout) |
| 公平性 | 非公平 | 可公平(代价:吞吐下降) |
| 条件变量 | 单一(wait/notify) |
多个 Condition |
| 实现层 | JVM monitor + 锁升级 | Java 层 AQS + CAS |
边界(必答):JDK 15(JEP 374)起偏向锁默认禁用并最终移除,所以"轻量级锁/偏向锁优化"的旧答案需要加时间限定;低竞争场景 synchronized 往往更快,因为 JVM 可做锁消除/粗化。
4. 线程池:七参数与执行流程(E-S25-023)
new ThreadPoolExecutor(
corePoolSize, maximumPoolSize, keepAliveTime, unit,
workQueue, threadFactory, handler)
执行流程(必背顺序,答错即扣分):
1. 线程数 < core → 新建核心线程执行
2. 否则尝试入队 → 入队成功即返回
3. 队列满 → 建非核心线程(直到 max)
4. 仍失败 → 拒绝策略
追问链(每一层都可能是下一问):
| 追问 | 应答要点 |
|---|---|
| 为什么先入队再扩容? | 避免频繁创建销毁线程;代价是队列选错会掩盖容量问题 |
| 无界队列会怎样? | maximumPoolSize 失效,任务堆积 → OOM;LinkedBlockingQueue 默认容量 Integer.MAX_VALUE |
| 四种拒绝策略怎么选? | Abort(要报错)/ CallerRuns(要反压)/ Discard(可丢)/ DiscardOldest(可丢旧) |
| 线程数怎么定? | CPU 密集 ≈ N+1;IO 密集 ≈ N×(1+等待/计算);必须实测,别用公式当结论 |
| 核心线程会回收吗? | 默认不回收;allowCoreThreadTimeOut(true) 才回收 |
| 池化线程里 ThreadLocal 怎么办? | 必须 finally 里 remove() |
| 怎么监控? | getActiveCount/getQueue().size()/getCompletedTaskCount + 自定义 RejectedExecutionHandler 计数 |
为什么禁用 Executors? |
newFixedThreadPool/newSingleThreadExecutor 用无界队列;newCachedThreadPool 的 max 是 Integer.MAX_VALUE — 阿里规约的来源 |
源码级答法(ThreadPoolExecutor.execute)——把 ctl 的高 3 位(状态)与低 29 位(线程数)说清,比背流程更能体现深度:
ctl = (状态 << 29) | 线程数 // RUNNING/SHUTDOWN/STOP/TIDYING/TERMINATED
5. 高频题的"边界答案"速查
| 题 | 常见错答 | 边界补充 |
|---|---|---|
ConcurrentHashMap 为什么不允许 null |
"设计如此" | 并发下无法区分"没查到"与"值是 null",且 computeIfAbsent 语义会歧义 |
CopyOnWriteArrayList 适用场景 |
"读多写少" | 写少且元素少;每次写全量复制,大列表写放大严重 |
ForkJoin 一定更快吗 |
"分治更快" | 任务切分粒度太小反而更慢;commonPool 被阻塞任务占满会拖垮全局 |
CompletableFuture 用默认线程池 |
"方便" | ForkJoinPool.commonPool() 并行度 = CPU-1,IO 任务会饿死;应传自定义 Executor |
Semaphore 能替代限流吗 |
"能" | 只是单机信号量;分布式限流要 Redis/Lua 或网关层 |
AQS 为什么用 CLH 变体 |
"FIFO 公平" | 实际是虚拟双向队列 + 状态位,非严格 FIFO;非公平模式允许插队 |
6. 对照实验:i++ 的三种写法
// JDK 17,单机 8 核;每项跑 10 轮取均值(量级示意,非精确基准)
int i = 0; // ① 裸 int: 约 2.0 亿次丢更新
AtomicInteger ai; // ② CAS: 正确但高竞争下自旋开销大
LongAdder la; // ③ 分段累加: 高并发写吞吐最高,sum() 非强一致快照
实测口径:本机未跑百万级基准,上表是机制推断(依据 LongAdder 的 Cell 分段与 AtomicInteger 的 CAS 重试)。要发表结论请自行用 JMH 测;面试时说"量级"而不是"精确倍数"更稳。
7. 排障:并发问题的现场手法
| 现象 | 手法 |
|---|---|
| CPU 打满 | top -Hp <pid> → printf %x 转 16 进制 → jstack <pid> | grep -A20 <nid> 找热点栈 |
| 线程卡死/死锁 | jstack 搜 Found one Java-level deadlock;或 jcmd <pid> Thread.print |
| 线程数暴涨 | 查是否有池外 new Thread;jstack | grep "pool-" | wc -l |
| 内存只涨不降 | jmap -histo:live;重点看 ThreadLocal 的 value、ThreadPoolExecutor 队列 |
8. 反例:三种"背了也答不对"的答法
| 答法 | 为什么错 |
|---|---|
| "锁升级:偏向 → 轻量 → 重量" | 没提 JDK 15+ 偏向锁移除,时间点缺失 |
| "线程数 = CPU 核数 × 2" | 无前提;IO 等待比、任务类型、GC 停顿都会改变结论 |
| "volatile 保证可见性和原子性" | 原子性说错了;i++ 反例一句就能推翻 |
9. 自测
- 并发题的三层骨架是什么?为什么"边界"最常被追问?
synchronizedvsLock,说出三条机制差异与一条版本边界。- 线程池执行流程的四步顺序?无界队列破坏了哪一步?
LongAdder为什么在高并发写时比AtomicInteger快?它的代价是什么?
答案
- 结论/机制/边界;追问几乎总落在"什么条件下不成立"。
- monitor 自动释放 vs AQS 手动;不可中断 vs 可中断/超时;单条件 vs 多 Condition;边界:JDK 15+ 偏向锁移除。
- 核心线程 → 入队 → 扩容至 max → 拒绝;无界队列让第 3 步永不触发,max 失效、任务堆积。
- Cell 分段减少 CAS 冲突;代价是
sum()非强一致快照。
10. 下一篇
S25-J08 · JVM 调优题库 —— 内存结构、垃圾收集器选型、诊断四件套(jps/jstat/jmap/jstack)、三套调优实战(JDK 8/17/21、大内存、高并发)与监控体系。
本篇属于系列《S25 · 求职与面试》。旧文来源:JUC 教程 20 章(A-3016…3035)、线程池教程 14 章(A-3131…3145)等 38 篇;扩展点 E-S25-021/022/023。技术细节点以 duplicate_ref 指回 S04/S11 主讲系列;本文只给答题骨架与边界。