2276 字
约 7 分钟
10
S25-J07 · 并发题库讲评:从 JUC 二十章到线程池十四问

适用读者:准备 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. 自测

  1. 并发题的三层骨架是什么?为什么"边界"最常被追问?
  2. synchronized vs Lock,说出三条机制差异与一条版本边界。
  3. 线程池执行流程的四步顺序?无界队列破坏了哪一步?
  4. LongAdder 为什么在高并发写时比 AtomicInteger 快?它的代价是什么?
答案
  1. 结论/机制/边界;追问几乎总落在"什么条件下不成立"。
  2. monitor 自动释放 vs AQS 手动;不可中断 vs 可中断/超时;单条件 vs 多 Condition;边界:JDK 15+ 偏向锁移除。
  3. 核心线程 → 入队 → 扩容至 max → 拒绝;无界队列让第 3 步永不触发,max 失效、任务堆积。
  4. 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 主讲系列;本文只给答题骨架与边界。

S25-J07 · 并发题库讲评:从 JUC 二十章到线程池十四问
https://www.clxhxhhr.top/posts/5588/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。