前言

在 Java 并发编程(JUC)的学习中,我们往往容易陷入 API 的泥潭:掌握了 synchronized 的原理,背熟了 AQS 的源码,但在面对具体的业务场景时,却依然犹豫不决——“这里到底该用哪种锁?为什么?”

在 JDK 1.8 时代,锁的性能差异已不再像早期那么巨大,选型的核心依据从单纯的“性能”转向了**“场景匹配度”“代码维护性”**。

本文将摒弃枯燥的 API 罗列,直接提供一套从实战出发的并发工具选型决策指南


🚀 0. 精简速查版 (TL;DR)

如果你时间紧迫,请直接参考这份选型决策树。在面对线程安全问题时,按顺序自问:

  1. 是数据容器(List, Map, Queue)吗?
    • KV 存储 → ConcurrentHashMap (首选)
    • 需要排序的 KV → ConcurrentSkipListMap
    • 读多写极少(如白名单) → CopyOnWriteArrayList
    • 生产消费队列 → ArrayBlockingQueue (必须有界)
  2. 是单个变量的简单操作(如 +1,赋值)吗?
    • 极高并发统计(如接口限流计数) → LongAdder (JDK 1.8+)
    • 需要精确值或 CAS (如序列号) → Atomic 系列
  3. 读写比例是否非常悬殊(读 >> 写)?
    • 追求极致读性能且能容忍代码复杂 → StampedLock (乐观读)
    • 常规业务,追求稳健 → ReentrantReadWriteLock
  4. 需要高级流程控制(超时、中断、多条件等待)吗?
    • 是 → ReentrantLock + Condition
  5. 默认选项(终点):
    • 上述都不满足 → synchronized (最简单、最安全、自动挡)

1. 基准线:synchronized 的现代重生

观点:不要因为“它是重量级锁”而不敢用。在 JDK 1.8+ 中,它是默认首选。

为什么选它?

早期的 synchronized 确实性能较差,但在 JDK 1.6 之后,JVM 引入了偏向锁、轻量级锁(自旋)、重量级锁的自动升级机制。

  • 低竞争时:成本极低,仅涉及对象头 CAS,无需操作系统介入。
  • 防呆设计:JVM 保证无论正常返回还是抛出异常,锁一定会被释放。这避免了开发人员忘记 unlock 导致的死锁灾难。

适用场景

  • 普通业务同步:如单例模式(DCL)、简单的方法级互斥。
  • 不想引入复杂逻辑:团队成员水平参差不齐,追求代码的简洁性和可维护性。

口诀:官方原生是默认,自动挡位最省心。若无特殊高级求,synchronized 是冠军。


2. 手动挡:ReentrantLock 的精准控制

观点:当 synchronized 的“死等”机制无法满足需求时,才使用它。

核心痛点与解决方案

synchronized 是霸道的,一旦进入等待队列,要么拿到锁,要么一直死等,不可中断。ReentrantLock 提供了三种核心能力:

  1. 快速失败 (Fail-Fast)tryLock()。尝试拿一下,拿不到就放弃或去做别的事,适合定时任务或自我保护机制。
  2. 可中断响应lockInterruptibly()。允许在等待锁的过程中响应 Thread.interrupt(),适合需要“取消按钮”的交互场景。
  3. 多路通知:配合 Condition 可以实现精准唤醒(如只唤醒消费者,不唤醒生产者),这是实现复杂阻塞队列的基础。

⚠️ 致命风险

必须养成肌肉记忆:锁的释放必须放在 finally 块中。

1
2
3
4
5
6
lock.lock();
try {
// 业务逻辑
} finally {
lock.unlock(); // 必须在这里释放!
}

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. 容器选型:避坑指南

不要再使用 VectorHashtable,也不要乱用 Collections.synchronizedList

容器类型 推荐组件 选型关键点
Map ConcurrentHashMap 默认首选。JDK 1.8 优化为 CAS + synchronized (Node锁),性能极高。
List CopyOnWriteArrayList 仅限读多写极少(如白名单)。写操作会复制整个数组,频繁写会造成 GC 灾难。
Queue ArrayBlockingQueue 生产环境首选必须有界!防止消费者宕机导致队列积压引发 OOM。
Queue LinkedBlockingQueue 吞吐量虽高,但默认构造函数是无界的(Integer.MAX_VALUE)。使用时必须手动指定容量

🎯 总结:架构师的思维

技术选型没有银弹,只有 Trade-off(权衡)。

  1. Simplicity First:能用简单方案(synchronized/Atomic)解决的,绝不引入复杂方案。
  2. Protect System:使用队列必须有界,使用锁要考虑超时和释放。
  3. Know Your Data:清楚你的业务是读多写少,还是写多读少?是要求绝对精确,还是最终一致?

希望这篇博文能帮你建立起一套完整的并发工具选型框架。下次面对复杂的线程安全问题时,不再迷茫,能够自信地做出最合理的架构决策。