并发内功-如何确定加什么类型的锁?
前言
在 Java 并发编程(JUC)的学习中,我们往往容易陷入 API 的泥潭:掌握了 synchronized 的原理,背熟了 AQS 的源码,但在面对具体的业务场景时,却依然犹豫不决——“这里到底该用哪种锁?为什么?”
在 JDK 1.8 时代,锁的性能差异已不再像早期那么巨大,选型的核心依据从单纯的“性能”转向了**“场景匹配度”与“代码维护性”**。
本文将摒弃枯燥的 API 罗列,直接提供一套从实战出发的并发工具选型决策指南。
🚀 0. 精简速查版 (TL;DR)
如果你时间紧迫,请直接参考这份选型决策树。在面对线程安全问题时,按顺序自问:
- 是数据容器(List, Map, Queue)吗?
- KV 存储 →
ConcurrentHashMap(首选) - 需要排序的 KV →
ConcurrentSkipListMap - 读多写极少(如白名单) →
CopyOnWriteArrayList - 生产消费队列 →
ArrayBlockingQueue(必须有界)
- KV 存储 →
- 是单个变量的简单操作(如 +1,赋值)吗?
- 极高并发统计(如接口限流计数) →
LongAdder(JDK 1.8+) - 需要精确值或 CAS (如序列号) →
Atomic系列
- 极高并发统计(如接口限流计数) →
- 读写比例是否非常悬殊(读 >> 写)?
- 追求极致读性能且能容忍代码复杂 →
StampedLock(乐观读) - 常规业务,追求稳健 →
ReentrantReadWriteLock
- 追求极致读性能且能容忍代码复杂 →
- 需要高级流程控制(超时、中断、多条件等待)吗?
- 是 →
ReentrantLock+Condition
- 是 →
- 默认选项(终点):
- 上述都不满足 →
synchronized(最简单、最安全、自动挡)
- 上述都不满足 →
1. 基准线:synchronized 的现代重生
观点:不要因为“它是重量级锁”而不敢用。在 JDK 1.8+ 中,它是默认首选。
为什么选它?
早期的 synchronized 确实性能较差,但在 JDK 1.6 之后,JVM 引入了偏向锁、轻量级锁(自旋)、重量级锁的自动升级机制。
- 低竞争时:成本极低,仅涉及对象头 CAS,无需操作系统介入。
- 防呆设计:JVM 保证无论正常返回还是抛出异常,锁一定会被释放。这避免了开发人员忘记
unlock导致的死锁灾难。
适用场景
- 普通业务同步:如单例模式(DCL)、简单的方法级互斥。
- 不想引入复杂逻辑:团队成员水平参差不齐,追求代码的简洁性和可维护性。
口诀:官方原生是默认,自动挡位最省心。若无特殊高级求,synchronized 是冠军。
2. 手动挡:ReentrantLock 的精准控制
观点:当 synchronized 的“死等”机制无法满足需求时,才使用它。
核心痛点与解决方案
synchronized 是霸道的,一旦进入等待队列,要么拿到锁,要么一直死等,不可中断。ReentrantLock 提供了三种核心能力:
- 快速失败 (Fail-Fast):
tryLock()。尝试拿一下,拿不到就放弃或去做别的事,适合定时任务或自我保护机制。 - 可中断响应:
lockInterruptibly()。允许在等待锁的过程中响应Thread.interrupt(),适合需要“取消按钮”的交互场景。 - 多路通知:配合
Condition可以实现精准唤醒(如只唤醒消费者,不唤醒生产者),这是实现复杂阻塞队列的基础。
⚠️ 致命风险
必须养成肌肉记忆:锁的释放必须放在 finally 块中。
1 | lock.lock(); |
3. 性能利器:读写锁的权衡 (RRW vs Stamped)
观点:只有在“读多写少”(建议比例 > 10:1)的场景下,读写锁才有意义。
3.1 ReentrantReadWriteLock (RRW)
- 特性:读读共享,读写互斥,写写互斥。
- 缺点:存在“写锁饥饿”问题。如果读请求源源不断,写线程可能永远抢不到锁。
- 场景:配置中心缓存、商品详情页(偶尔修改,大量查看)。
3.2 StampedLock (JDK 1.8 新宠)
为了解决 RRW 的性能瓶颈,JDK 8 引入了 StampedLock,它支持乐观读 (Optimistic Read)。
- 机制:先假设没人改(不加锁),读完后验证一下“版本戳”。如果验证失败(说明中间有人改了),再升级为悲观读锁。
- 代价:代码非常复杂,且不可重入,使用不当容易死锁。
- 场景:对吞吐量极其敏感的系统(如金融高频交易、中间件底层)。
4. 无锁编程:Atomic 与 LongAdder
观点:如果只是简单的数值更新,没必要动用“锁”这种重型武器。
4.1 Atomic 家族 (AtomicInteger/AtomicLong)
- 原理:CAS (Compare-And-Swap) 指令。
- 场景:精确的序列号生成、状态标志位切换。
4.2 LongAdder (JDK 1.8 黑科技)
- 痛点:高并发下,大量线程竞争同一个 Atomic 变量,CAS 失败率极高,导致 CPU 空转自旋。
- 原理:空间换时间。将一个变量拆分成一个数组(Cell[]),不同线程去加数组中不同的格子,最后求和。
- 限制:
sum()返回的是最终一致性结果,且只能做加减统计,不能做 CAS 比较。 - 场景:接口限流计数器、系统指标监控(Metrics)。
5. 容器选型:避坑指南
不要再使用 Vector 或 Hashtable,也不要乱用 Collections.synchronizedList。
| 容器类型 | 推荐组件 | 选型关键点 |
|---|---|---|
| Map | ConcurrentHashMap |
默认首选。JDK 1.8 优化为 CAS + synchronized (Node锁),性能极高。 |
| List | CopyOnWriteArrayList |
仅限读多写极少(如白名单)。写操作会复制整个数组,频繁写会造成 GC 灾难。 |
| Queue | ArrayBlockingQueue |
生产环境首选。必须有界!防止消费者宕机导致队列积压引发 OOM。 |
| Queue | LinkedBlockingQueue |
吞吐量虽高,但默认构造函数是无界的(Integer.MAX_VALUE)。使用时必须手动指定容量。 |
🎯 总结:架构师的思维
技术选型没有银弹,只有 Trade-off(权衡)。
- Simplicity First:能用简单方案(synchronized/Atomic)解决的,绝不引入复杂方案。
- Protect System:使用队列必须有界,使用锁要考虑超时和释放。
- Know Your Data:清楚你的业务是读多写少,还是写多读少?是要求绝对精确,还是最终一致?
希望这篇博文能帮你建立起一套完整的并发工具选型框架。下次面对复杂的线程安全问题时,不再迷茫,能够自信地做出最合理的架构决策。


