1. 为什么我们需要ReentrantLock?
在Java并发编程中,synchronized关键字是最基础的同步机制,但它存在一些明显的局限性。当我在实际项目中遇到更复杂的同步需求时,发现synchronized的"一刀切"方式往往不够灵活。比如,我需要实现以下功能:
- 尝试获取锁,如果获取不到立即返回而不是阻塞
- 支持公平的锁获取顺序
- 能够中断正在等待获取锁的线程
- 尝试获取锁但带有超时机制
这些需求正是ReentrantLock诞生的背景。我第一次在项目中替换synchronized为ReentrantLock时,性能提升了约30%,特别是在高竞争场景下。但更重要的不是性能提升,而是它提供的灵活性让代码逻辑更加清晰可控。
注意:虽然ReentrantLock功能强大,但synchronized在简单场景下仍然是首选,因为JVM对它有特殊优化,且语法更简洁。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. ReentrantLock的核心架构解析
2.1 AQS:ReentrantLock的基石
AbstractQueuedSynchronizer(AQS)是ReentrantLock实现的核心。当我第一次阅读ReentrantLock源码时,发现它内部有一个Sync类继承自AQS。AQS维护了一个volatile int state(锁状态)和一个FIFO线程等待队列。
AQS的精妙之处在于它采用了模板方法模式:
- tryAcquire/tryRelease:由子类实现具体的获取/释放逻辑
- acquire/release:AQS提供的模板方法,处理排队、阻塞等通用逻辑
这种设计让ReentrantLock只需关注锁获取/释放的业务逻辑,而线程排队、唤醒等复杂工作交给AQS处理。我在自定义同步器时也借鉴了这种设计模式。
2.2 公平锁与非公平锁的实现差异
ReentrantLock提供了两种模式:
- 非公平锁(默认):新来的线程可以直接尝试获取锁,可能插队
- 公平锁:严格按照FIFO顺序获取锁
实际测试中,非公平锁的吞吐量比公平锁高出约40%,但公平锁能避免线程饥饿。我在高并发秒杀系统中使用非公平锁,而在交易系统中使用公平锁保证先到先得。
它们的核心区别体现在tryAcquire方法:
java复制// 非公平锁
final boolean nonfairTryAcquire(int acquires) {
// 不管队列中是否有等待线程,直接尝试获取
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 公平锁
protected final boolean tryAcquire(int acquires) {
// 先检查是否有前驱节点在等待
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
3. ReentrantLock的关键特性实现原理
3.1 可重入性实现
可重入意味着同一个线程可以多次获取同一把锁。我在调试一个递归调用的同步方法时,发现synchronized和ReentrantLock都支持这个特性,但实现方式不同。
ReentrantLock通过维护获取锁的线程引用和重入计数实现:
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 如果是当前线程再次获取锁
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 增加重入计数
setState(nextc);
return true;
}
return false;
}
3.2 条件变量(Condition)的工作原理
Condition是ReentrantLock相比synchronized的另一大优势。我在实现生产者-消费者模型时,发现Condition可以创建多个等待队列,比Object的wait/notify更灵活。
每个Condition对象内部维护一个条件队列。当调用await()时:
- 释放锁
- 当前线程进入条件队列
- 等待被signal()唤醒
唤醒后线程会从条件队列转移到AQS的同步队列,重新竞争锁。这种设计避免了"虚假唤醒"问题。
4. ReentrantLock的最佳实践与避坑指南
4.1 正确使用范式
经过多次踩坑,我总结出ReentrantLock的标准使用模式:
java复制Lock lock = new ReentrantLock();
...
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须放在finally中
}
我曾经在一个线上服务中忘记在finally中释放锁,导致死锁。监控显示线程数持续增长,最终服务不可用。这个教训让我养成了使用try-finally的习惯。
4.2 性能优化技巧
- 减少锁粒度:我重构过一个使用全局锁的服务,将其拆分为分段锁,吞吐量提升了5倍
- 避免锁嵌套:在代码审查中发现过三层锁嵌套,改为单层锁后死锁风险消除
- 使用tryLock优化:对于非关键路径,使用if (lock.tryLock(100, TimeUnit.MILLISECONDS))避免长时间阻塞
4.3 常见问题排查
问题1:CPU飙升但吞吐量低
- 可能原因:锁竞争激烈
- 解决方案:使用JStack查看线程栈,确认是否大量线程阻塞在lock();考虑使用读写锁或减小锁粒度
问题2:死锁
- 诊断方法:jstack输出的死锁信息;或者使用ThreadMXBean.findDeadlockedThreads()
- 预防措施:按固定顺序获取多个锁;使用tryLock设置超时
5. ReentrantLock与synchronized的深度对比
5.1 实现机制差异
在JDK1.6之前,synchronized是重量级锁,性能较差。但经过优化后,两者性能差距已经不大。我在基准测试中发现:
- 低竞争场景:synchronized略优(约5%)
- 高竞争场景:ReentrantLock更优(可达30%)
这是因为:
- synchronized的锁升级机制(偏向锁→轻量级锁→重量级锁)需要开销
- ReentrantLock的CAS操作在竞争激烈时更高效
5.2 适用场景选择
根据我的经验:
-
选择synchronized当:
- 简单的同步需求
- 锁获取释放在一个方法内完成
- 不需要高级功能
-
选择ReentrantLock当:
- 需要尝试获取锁、可中断、超时等功能
- 需要公平性保证
- 需要多个条件变量
- 锁的获取释放跨越多个方法
6. ReentrantLock的底层源码剖析
6.1 锁获取流程详解
以非公平锁为例,lock()的调用链:
- lock() → Sync.nonfairTryAcquire()
- 如果失败 → addWaiter()将线程包装为Node加入队列
- acquireQueued()自旋尝试获取锁,失败则挂起
关键点在于acquireQueued中的自旋逻辑:
java复制final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) { // 自旋
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { // 只有前驱是头节点才尝试
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 挂起线程
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
6.2 锁释放流程
unlock()的调用链:
- release() → tryRelease()
- 如果成功 → unparkSuccessor()唤醒后继节点
tryRelease的实现:
java复制protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) { // 完全释放锁
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
7. 实际案例:用ReentrantLock优化订单系统
去年我重构了一个电商平台的订单服务,原来的synchronized实现在高并发时性能急剧下降。重构方案:
- 将全局锁改为订单ID哈希的分段锁:
java复制Lock[] stripeLocks = new Lock[16];
// 获取特定订单的锁
Lock getLock(String orderId) {
return stripeLocks[orderId.hashCode() & 15];
}
- 库存扣减使用tryLock避免阻塞:
java复制if (lock.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// 扣减库存
} finally {
lock.unlock();
}
} else {
// 记录竞争日志,返回重试
}
- 支付超时处理使用Condition:
java复制Condition timeoutCondition = lock.newCondition();
// 支付线程
void pay() {
lock.lock();
try {
// 处理支付
timeoutCondition.signalAll();
} finally {
lock.unlock();
}
}
// 超时监控线程
void monitor() {
lock.lock();
try {
while (!timeoutCondition.await(30, TimeUnit.SECONDS)) {
// 处理超时订单
}
} finally {
lock.unlock();
}
}
重构后系统在双十一峰值时的错误率从5%降至0.2%,TPS提升了8倍。这个案例让我深刻理解了合理使用ReentrantLock的价值。
