4. 并发编程与锁优化
并发编程是分布式系统高并发场景的基石,锁优化直接决定系统吞吐与稳定性。本章覆盖 synchronized、ReentrantLock、AQS、线程池、CAS 五大核心,从锁机制原理到线程池调优,再到分布式锁实战,构建完整并发知识体系。
| 核心概念 | 一句话定位 | 面试权重 |
|---|---|---|
| synchronized | JVM 内置锁,自动加锁/释放,支持锁升级 | ⭐⭐⭐⭐⭐ |
| ReentrantLock | API 级别可重入锁,支持公平/非公平、中断、Condition | ⭐⭐⭐⭐⭐ |
| AQS | 抽象队列同步器,CLH 变体 + state + CAS,模板方法模式 | ⭐⭐⭐⭐⭐ |
| 线程池 | 管理线程生命周期,七大参数 + 四步执行流程 | ⭐⭐⭐⭐⭐ |
| CAS | 乐观锁核心,硬件级原子操作,存在 ABA 问题 | ⭐⭐⭐⭐ |
Q1:synchronized 和 ReentrantLock 有什么区别?实际开发中如何选择?
回答框架:从 语法 → 功能特性 → 底层实现 → 性能 → 选型建议 五个维度对比。
| 维度 | synchronized | ReentrantLock |
|---|---|---|
| 语法 | JVM 关键字,自动加锁/释放 | API 接口,lock/unlock 配对 |
| 公平性 | 非公平 | 支持公平/非公平 |
| 中断响应 | 不支持 | 支持 lockInterruptibly |
| 条件变量 | wait/notify 单一条件 | 多个 Condition 对象 |
| 底层实现 | 锁升级(偏向→轻量→重量) | AQS + CAS + LockSupport |
| 性能(JDK 8+) | 优化后与 Lock 接近 | 高并发下略优 |
📝 示例回答:synchronized 是 JVM 内置锁,自动释放,支持锁升级(偏向锁→轻量锁→重量锁);ReentrantLock 是 API 级锁,需手动释放,提供可中断、公平锁、多 Condition 等高级功能。简单场景选 synchronized,需要超时中断或公平锁时选 ReentrantLock。
🔍 可能追问:
- synchronized 的锁升级过程具体是怎样的?偏向锁是否一定比轻量锁快?
- ReentrantLock 的公平锁与非公平锁在性能上有什么区别?
- JDK 6 之后 synchronized 做了哪些优化?
Q2:请讲一下 AQS 的实现原理,如何基于 AQS 实现一个自定义同步器?
回答框架:核心组件(state + CLH 队列 + CAS)→ 模板方法模式 → 自定义实现步骤 → 代码示例。
- state:volatile 修饰的同步状态,通过 CAS 更新
- CLH 队列:双向链表,线程获取锁失败时入队等待
- 模板方法:acquire / release 定义骨架,tryAcquire / tryRelease 由子类实现
- 自定义步骤:继承 AbstractQueuedSynchronizer,重写 tryAcquire / tryRelease(独占)或 tryAcquireShared / tryReleaseShared(共享)
📝 示例回答:AQS 基于 volatile state 和 CLH 变体队列实现,通过 CAS 更新 state,失败则入队等待。自定义同步器需重写 tryAcquire/tryRelease(独占模式)或 tryAcquireShared/tryReleaseShared(共享模式)。例如实现一个互斥锁,只需将 state 0→1 视为获取锁,1→0 视为释放锁。
🔍 可能追问:
- AQS 如何解决线程阻塞与唤醒的性能问题?为什么要用 CLH 队列而不是其他队列?
- 共享模式(如 CountDownLatch、Semaphore)与独占模式(如 ReentrantLock)的区别是什么?
- AQS 中的 Node 节点状态(CANCELLED / SIGNAL / CONDITION)分别代表什么?
Q3:线程池的核心参数有哪些?提交一个任务后线程池的执行流程是什么?如何合理配置线程数?
回答框架:七大参数 → 四步执行流程 → 配置策略 → 监控与调优。
七大参数速记:corePoolSize · maxPoolSize · keepAliveTime · unit · workQueue · threadFactory · rejectHandler
执行流程:
- ① 核心线程数未满 → 创建核心线程执行
- ② 核心线程已满 → 任务入阻塞队列
- ③ 队列已满 → 创建非核心线程(不超过 maxPoolSize)
- ④ 线程数已达 maxPoolSize → 执行拒绝策略
配置策略:CPU 密集型 → N+1 | IO 密集型 → 2N+1 | 混合型 → 拆分后分别配置
📝 示例回答:七大参数:corePoolSize、maxPoolSize、keepAliveTime、unit、workQueue、threadFactory、rejectHandler。流程:先 corePoolSize,再入队列,再到 maxPoolSize,最后拒绝策略。CPU 密集型配置 N+1,IO 密集型配置 2N+1(N 为 CPU 核数),避免过度创建线程导致上下文频繁切换。
🔍 可能追问:
- 拒绝策略有哪些?AbortPolicy / CallerRunsPolicy / DiscardPolicy / DiscardOldestPolicy 分别适用什么场景?
- 如何动态调整线程池参数?使用 ThreadPoolExecutor 的 setCorePoolSize / setMaximumPoolSize 方法。
- 线程池的监控指标有哪些?activeCount / completedTaskCount / largestPoolSize / queue.size 等。
📌 项目案例:高并发秒杀系统的锁优化实践
核心难点与解决方案
| 核心难点 | 解决方案 | 效果 |
|---|---|---|
| 单 SKU 库存扣减并发冲突 | Redis 分段锁 + 本地缓存预热,将单一 key 拆分为 8 个 segment | 锁冲突降低 85% |
| 数据库行锁竞争 | 异步批量写入 + 最终一致性补偿,下单时只写 Redis,MQ 异步落库 | DB 锁等待下降 92% |
| 热点 SKU 导致的线程池阻塞 | 独立线程池隔离热点 SKU 处理,拒绝策略降级为快速失败并返回提示 | 整体吞吐提升 3 倍 |
面试高频追问
- 为什么选择 Redis 分段锁而不是 Redlock?—— 分段锁降低单 key 压力,Redlock 偏重跨节点一致性,秒杀场景可接受短暂超卖。
- 分段锁的 segment 数量如何确定?—— 根据 SKU 热度动态调整,热 SKU 分段多,冷 SKU 分段少,避免资源浪费。
- 本地缓存与 Redis 如何保证一致性?—— 采用「先写 Redis,再异步失效本地缓存」策略,配合版本号校验,容忍秒级不一致。
- 如果 Redis 宕机如何兜底?—— 本地熔断降级,直接读取数据库库存并加分布式写锁,保证系统不崩溃。
可迁移回答话术
“在秒杀场景中,我们通过 分段锁 + 本地缓存预热 降低锁粒度,将单 SKU 的库存拆分为 8 个 Redis segment,每个 segment 独立加锁,锁冲突率降低了 85%。同时用 Caffeine 本地缓存 承载 90% 的读请求,Redis 只处理写请求和缓存同步,最终 TP99 从 2s 降至 200ms。这套方案的核心思路是 『读写分离 + 分段隔离』,可迁移到任何高并发库存/余额类场景。”
💡 迁移提示:面试其他项目时,只要涉及「高并发写 + 热点资源竞争」,都可以复用「分段+本地缓存+异步落库」的话术结构,重点突出「锁粒度量化对比」和「TP99 改善幅度」两个关键数据。
📖 本章核心脉络
- 锁机制选型: synchronized 适用于简单同步,ReentrantLock 适用于高级并发场景(公平、中断、多条件)
- AQS 原理: 理解 state + CLH 队列 + 模板方法,是自定义同步器的基础
- 线程池调优: 七大参数 + 四步流程 + 拒绝策略,结合业务类型配置线程数
- 锁优化实战: 分段锁降低粒度、锁消除/锁粗化、CAS 无锁化、读写分离
- 分布式锁: Redis 分段锁 + 本地缓存 + 异步落库,兼顾性能与一致性