面试基础----ReentrantLock vs Synchronized
ReentrantLock vs Synchronized:源码级解析与高并发场景下的锁博弈
引言:多线程编程中的锁为何重要?
- 业务背景:北京互联网大厂的高并发场景(如电商秒杀、支付交易、实时推荐系统)对线程安全和性能的极致要求。
- 锁的核心作用:解决竞态条件(Race Condition)、保证可见性(Visibility)和有序性(Ordering)。
- 痛点直击:错误选锁可能引发性能瓶颈(如线程阻塞、上下文切换)、死锁风险,甚至系统雪崩。
一、Synchronized 的底层实现:JVM 的锁优化艺术
1.1 对象头与 Mark Word(源码级解析)
- 对象内存布局:以 HotSpot JVM 为例,对象头中的
Mark Word
(32/64位)存储锁状态、GC 分代年龄等信息。 - 锁状态标志位:通过
markOop.hpp
(C++ 源码)中的mark bit
区分无锁、偏向锁、轻量级锁、重量级锁。 - 代码示例:
// 对象头示例(简化) class ObjectHeader { MarkWord mark; // 锁状态存储 // ... }
1.2 锁升级过程:偏向锁 → 轻量级锁 → 重量级锁
- 偏向锁(Biased Locking):
- 目标:减少无竞争场景下的锁开销。
- 实现:通过
BiasedLocking::revoke_and_rebias
(HotSpot 源码)处理线程 ID 偏向。 - 适用场景:单线程重复进入同步块(如订单状态机)。
- 轻量级锁(Lightweight Locking):
- CAS 自旋:通过
Atomic::cmpxchg_ptr
(JVM 原子操作)尝试获取锁。 - 栈帧锁记录(Lock Record):存储对象头的拷贝,用于锁释放时的恢复。
- CAS 自旋:通过
- 重量级锁(Heavyweight Locking):
- 操作系统互斥量(Mutex):通过
ObjectMonitor
(objectMonitor.hpp
)实现,涉及线程阻塞和唤醒。 - 性能代价:上下文切换开销(约 1-10μs)。
- 操作系统互斥量(Mutex):通过
1.3 锁粗化与锁消除(JIT 优化)
- 锁粗化(Lock Coarsening):合并相邻同步块(如循环内的同步操作)。
- 锁消除(Lock Elimination):逃逸分析(Escape Analysis)确定锁对象无竞争时直接移除锁。
二、ReentrantLock 的底层实现:AQS 的队列化控制
2.1 AQS(AbstractQueuedSynchronizer)核心机制
- CLH 队列(Craig, Landin, Hagersten):双向链表实现线程排队,源码见
AbstractQueuedSynchronizer.Node
。 - 状态变量(state):通过
volatile int state
控制锁的获取与释放。 - 关键方法:
acquire(int arg)
:尝试获取锁,失败则加入队列阻塞。release(int arg)
:释放锁并唤醒后继节点。
2.2 公平锁 vs 非公平锁(源码对比)
- 公平锁(FairSync):
protected final boolean tryAcquire(int acquires) { if (hasQueuedPredecessors()) // 检查队列中是否有等待线程 return false; // ... CAS 操作获取锁 }
- 非公平锁(NonfairSync):
final void lock() { if (compareAndSetState(0, 1)) // 直接尝试插队 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }
- 业务场景选择:
- 公平锁:避免线程饥饿(如金融交易订单处理)。
- 非公平锁:提高吞吐量(如日志异步写入)。
2.3 Condition 的精准控制
- 等待/通知机制:通过
ConditionObject
实现(对比Object.wait()
/notify()
)。 - 典型应用:生产者-消费者模型中的精准唤醒。
ReentrantLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); Condition notEmpty = lock.newCondition();
三、性能对比:JMH 基准测试与业务场景适配
3.1 吞吐量对比(JMH 基准测试)
- 低竞争场景:
synchronized
因偏向锁优化表现更优。 - 高竞争场景:
ReentrantLock
的非公平锁模式吞吐量更高(减少线程切换)。
3.2 响应时间对比
- ReentrantLock 的优势:
tryLock()
:支持超时等待,避免死锁。lockInterruptibly()
:响应线程中断。
3.3 适用场景总结
场景 | 推荐锁 | 理由 |
---|---|---|
低竞争、简单同步 | synchronized | JVM 自动优化,代码简洁 |
高并发、需要超时/中断 | ReentrantLock | 灵活控制,避免线程阻塞 |
分布式锁本地降级 | synchronized | 减少外部依赖,降低复杂度 |
四、实际应用中的选择建议与避坑指南
4.1 大厂业务场景实战
- 场景 1:秒杀系统库存扣减
- 选择:
ReentrantLock
+tryLock(10ms)
- 原因:防止线程长时间阻塞导致请求堆积。
- 选择:
- 场景 2:配置中心热更新
- 选择:
synchronized
- 原因:更新频率低,偏向锁优化足够高效。
- 选择:
4.2 常见坑与解决方案
- 坑 1:锁粒度过粗
- 现象:全局锁导致性能瓶颈。
- 解决:细粒度锁(如 ConcurrentHashMap 分段锁)。
- 坑 2:死锁
- 诊断:
jstack
分析线程栈,检查锁依赖链。 - 预防:统一锁获取顺序,使用
tryLock
超时机制。
- 诊断:
4.3 优化建议
- 监控:通过 APM 工具(如 Arthas)监控锁竞争(
monitor
命令)。 - 代码规范:避免在锁内执行耗时操作(如 IO 操作)。
五、总结:最佳实践与未来展望
- synchronized 优势:简单、自动优化、JVM 原生支持。
- ReentrantLock 优势:灵活、可中断、支持公平性。
- 终极选择:
- 80% 场景:优先使用
synchronized
(KISS 原则)。 - 20% 复杂场景:选择
ReentrantLock
(灵活控制)。
- 80% 场景:优先使用
- 未来趋势:无锁编程(CAS、LongAdder)、协程(虚拟线程)的崛起。
通过源码级解析与业务场景结合,开发者可在大厂高并发环境下精准选择锁机制,平衡性能与复杂度。