知

模块三 · 并发与 JVM

第 5 / 11 节20 分钟阅读型

并发全景:从线程池到锁到无锁

并发题的核心不是背 API,而是理解"共享、可见、有序、互斥"四件事。这一节按线程池、锁、JMM、CAS、并发工具的顺序讲。

读完这一节,你能做到

  1. 说清线程池七个参数与四种拒绝策略的取舍。
  2. 解释 synchronized 的锁升级与 ReentrantLock 的差异。
  3. 用 happens-before 解释 volatile 的可见性与有序性。
  4. 判断 CAS 的 ABA 问题与自旋开销。

一、线程池:七个参数

new ThreadPoolExecutor(
  corePoolSize, maximumPoolSize, keepAliveTime, unit,
  workQueue, threadFactory, handler
);

执行顺序是:核心线程 → 队列 → 非核心线程 → 拒绝策略。很多人以为"线程满了才排队",实际是"核心满了先排队,队列满了才扩线程"。

拒绝策略行为适用
AbortPolicy抛 RejectedExecutionException默认,快速失败
CallerRunsPolicy调用方线程执行降速,避免丢任务
DiscardPolicy静默丢弃不重要任务
DiscardOldestPolicy丢最老的,再尝试提交实时性优先

线程数估算:

  • CPU 密集:约 CPU 核数 + 1。
  • IO 密集:约 CPU 核数 × (1 + 等待时间/计算时间)。
  • 最终要以压测为准,公式只是起点。

队列不要用无界队列:任务堆积会 OOM,且 maximumPoolSize 永远不会生效。

二、锁:synchronized 与 ReentrantLock

synchronized 是 JVM 层面的内置锁,自动加锁/释放,异常也会释放。锁升级路径:

无锁 → 偏向锁 → 轻量级锁(CAS 自旋)→ 重量级锁(操作系统互斥量)

注意:偏向锁在 JDK 15 被废弃、JDK 17 默认关闭,面试时要说明版本差异。

ReentrantLock 是 AQS 实现的显式锁,能力更强:

能力synchronizedReentrantLock
自动释放是否,必须 finally unlock
公平锁否可选
可中断否lockInterruptibly
超时尝试否tryLock(timeout)
条件队列单个 wait/notify多个 Condition

AQS 的核心是 state + CLH 双向队列:获取失败入队,释放时唤醒后继;公平与非公平的差别在于是否允许插队。

三、JMM 与 volatile

Java 内存模型规定:每个线程有工作内存,共享变量在主内存。没有同步时,一个线程的修改对另一个线程不一定可见。

volatile 保证两件事:

  • 可见性:写立即刷新到主内存,读从主内存读。
  • 有序性:禁止指令重排(通过内存屏障)。

它不保证原子性:i++ 仍然是读-改-写三步,多线程下会丢更新。

happens-before 规则是判断可见性的依据,常用的几条:

  • 程序顺序:同一线程内,前面的操作先于后面的。
  • 监视器锁:解锁先于后续加锁。
  • volatile:写先于后续读。
  • 线程启动/终止:start 先于线程内操作,线程内操作先于 join 返回。
  • 传递性:A 先于 B、B 先于 C,则 A 先于 C。

四、CAS 与原子类

CAS 是"比较并交换":只有当前值等于期望值时才更新。AtomicInteger 用 Unsafe 的 CAS 实现无锁自增。

三个问题:

  • ABA:值从 A 变 B 又变 A,CAS 察觉不到。用 AtomicStampedReference 加版本号。
  • 自旋开销:竞争激烈时 CPU 空转,必要时退化为锁。
  • 只能保证一个变量:多变量用 AtomicReference 或锁。

LongAdder 用分段累加降低热点,读的时候再汇总,适合高并发计数;AtomicLong 适合低竞争。

五、并发工具

工具作用
CountDownLatch等待 N 个任务完成,一次性
CyclicBarrier等齐再一起走,可循环
Semaphore控制并发数量
CompletableFuture异步编排与组合

CompletableFuture 的三个常用组合:thenCompose(串行依赖)、thenCombine(并行合并)、allOf(等待全部)。异常用 exceptionally 或 handle 处理。

六、常见坑

  1. 用无界队列,导致任务堆积与 OOM。
  2. 线程池用完不 shutdown,JVM 无法退出。
  3. 在 synchronized 块里做远程调用,锁持有时间过长。
  4. 用 volatile 修饰计数器,误以为原子。
  5. 线程池里抛出的异常被吞掉,任务静默失败。

七、动手任务:自写 → AI 写 → 对比 → 取舍

四步不跳:① 自己写(限时,关掉补全)→ ② 让 AI 写同一题 → ③ 逐条比对 → ④ 说出保留谁的写法。 完整动作与记录模板见《AI 协作与代码评审》。

① 自己写

  1. 写一个固定大小线程池,对比 CallerRunsPolicy 与 AbortPolicy 的行为。
  2. 用两个线程演示 volatile 可见性,再去掉 volatile 复现死循环。
  3. 用 CompletableFuture 并行调用三个接口并合并结果。

② 让 AI 写同一题

把第 2 题(线程池配置)交给 AI,补一句 说明队列容量、拒绝策略与为什么这么选。

③ 对比要盯的点

  • 它用了无界队列吗?那等于放弃背压,任务会堆到 OOM。
  • ThreadLocal 用完它 remove() 了吗?线程池复用会导致串数据。
  • CompletableFuture 默认用哪个线程池?它有没有说清为什么不建议默认。

④ 一句话取舍

它这套配置在流量翻三倍时,第一个撑不住的是哪一环?

八、自测

  1. 线程池的执行顺序是什么?
  2. 四种拒绝策略分别适合什么场景?
  3. synchronized 的锁升级路径?
  4. volatile 保证什么、不保证什么?
  5. CAS 的 ABA 问题怎么解决?

参考答案要点:核心→队列→非核心→拒绝;快速失败/降速/丢弃/丢老;无锁→偏向→轻量→重量;可见性与有序性,不保证原子;版本号。

附录:高频追问

问:为什么阿里规约禁止用 Executors 创建线程池? 固定/缓存线程池用无界队列,可能堆积 OOM;应显式指定队列与拒绝策略。

问:线程池核心线程会回收吗? 默认不回收;allowCoreThreadTimeOut(true) 可以让核心线程也超时回收。

问:synchronized 和 Lock 怎么选? 简单同步用 synchronized;需要公平、超时、可中断、多条件队列时用 Lock。

问:什么是锁消除与锁粗化? JIT 发现锁对象不会逃逸就消除;相邻的多次加锁合并成一次,减少开销。

问:ThreadLocal 会内存泄漏吗? 会。key 是弱引用,value 是强引用;线程池里用完必须 remove。

问:ConcurrentHashMap 的 size 准确吗? JDK 8 用 CounterCell 分段计数,size 是近似值,并发下不保证精确。

问:AQS 的 state 一定是 1 吗? 不一定。ReentrantLock 用它表示重入次数,Semaphore 表示许可数,CountDownLatch 表示剩余计数。

问:死锁的四个必要条件? 互斥、占有且等待、不可抢占、循环等待;破坏任一即可。

问:如何排查死锁? jstack 会直接给出死锁报告,或用 Arthas 的 thread 命令。

问:乐观锁与悲观锁的区别? 乐观锁用版本号/CAS,冲突少时性能好;悲观锁先加锁,冲突多时更稳。

线程池监控

生产环境至少监控四个数:活跃线程数、队列长度、拒绝次数、任务耗时。队列持续增长说明消费能力不足;拒绝次数上升说明需要扩容或限流。

Spring 的 ThreadPoolTaskExecutor 可以暴露指标,接入 Micrometer 后在 Prometheus 里看趋势。

锁的选择清单

  • 简单互斥:synchronized。
  • 需要超时/可中断/公平:ReentrantLock。
  • 读多写少:ReentrantReadWriteLock 或 StampedLock。
  • 高并发计数:LongAdder。
  • 跨进程:Redis/ZooKeeper 分布式锁。

先问“临界区多大”,再选锁;临界区越小,锁竞争越少。

线程池的关闭

shutdown 不再接新任务,等已提交的跑完;shutdownNow 中断工作线程并返回未执行的任务。关闭后要 awaitTermination 等待,否则 JVM 可能提前退出或任务被丢。临界区越小,锁越轻;先缩临界区。先缩。

补遗:ThreadLocal 与内存泄漏

ThreadLocal 为每个线程维护独立副本,底层是 Thread 的 ThreadLocalMap,key 是弱引用、value 是强引用。线程池里线程长期存活,用完不 remove() 就会泄漏,还会串数据。InheritableThreadLocal 可传给子线程,但线程池下不可靠;异步场景用 TransmittableThreadLocal 或显式传参。

补遗:阻塞队列与 CompletableFuture

常用队列:ArrayBlockingQueue(有界数组)、LinkedBlockingQueue(链表,默认无界要当心)、SynchronousQueue(不存储)、PriorityBlockingQueue(优先级)、DelayQueue(延迟)。

CompletableFuture 的常用组合:thenApply(转换)、thenCompose(串行依赖)、thenCombine(并行合并)、allOf/anyOf、exceptionally/handle。默认用 ForkJoinPool.commonPool,IO 密集要传自定义线程池,否则会拖垮公共池。

补遗:虚拟线程

JDK 21 的虚拟线程由 JVM 调度,阻塞时自动让出载体线程,适合高并发 IO。它不适合 CPU 密集,也不需要池化(创建很便宜)。注意 synchronized 阻塞可能钉住载体线程,必要时换 ReentrantLock。

配套题目

★ 越多越该先背;点题目可看完整答案与深入版。

提示:读完可以顺手做几道对应题目,检验是否真的理解