深入解析Java ReentrantLock:可重入锁的核心机制
·
一、核心概念
1. 什么是“可重入”?
- 同一个线程可以多次获取同一个锁,而不会死锁。
- 每次
lock()成功后,内部计数器(hold count)+1;每次unlock()后 -1。 - 只有当计数器归零时,锁才真正被释放,其他线程才能获取。
✅ 示例:
线程 A 调用lock()→ holdCount=1
再次调用lock()→ holdCount=2
调用unlock()→ holdCount=1
再次unlock()→ holdCount=0 → 锁释放
2. 与 synchronized 的对比
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 可中断等待 | ❌ 不支持 | ✅ 支持(lockInterruptibly()) |
| 超时获取锁 | ❌ 不支持 | ✅ 支持(tryLock(timeout, unit)) |
| 公平性控制 | ❌ 无 | ✅ 可选公平/非公平 |
| 条件变量(Condition) | 只能用 wait/notify |
✅ 多个 Condition 对象 |
| 必须在 finally 中 unlock | ❌ 自动释放 | ✅ 必须手动 unlock(推荐 try-finally) |
二、关键设计:基于 AQS(AbstractQueuedSynchronizer)
ReentrantLock 的核心是内部类 Sync,它继承自 AbstractQueuedSynchronizer(AQS),这是 Java 并发包的基石。
- AQS 使用一个 int 状态(state) 表示锁的持有次数。
- 使用 CLH 队列 管理等待线程(FIFO)。
Sync有两个实现:NonfairSync:非公平锁(默认)FairSync:公平锁
三、公平 vs 非公平
🔸 非公平锁(NonfairSync)
- 允许“插队”:即使有线程在排队,新来的线程如果发现锁空闲,可以直接抢到。
- 性能更高(吞吐量大),但可能导致某些线程长时间等待(饥饿)。
tryLock()总是非公平的,即使你创建的是公平锁!
// 即使 lock 是公平的,这行仍可能“插队”
if (lock.tryLock()) { ... }
🔸 公平锁(FairSync)
- 严格按照 FIFO 顺序分配锁。
- 调用
lock()时会检查:是否有前驱节点?(hasQueuedPredecessors())- 如果有,即使锁空闲,也必须排队。
- 吞吐量较低,但避免饥饿,响应时间更可预测。
⚠️ 注意:公平 ≠ 线程调度公平!操作系统调度仍可能让某个线程连续执行。
四、核心方法解析
1. lock()
- 非公平:先尝试 CAS 抢锁,失败再入队。
- 公平:直接入队,按顺序获取。
2. tryLock()
- 立即返回,不阻塞。
- 无视公平性!即使有线程在等,只要锁空闲就抢。
- 适合“尽力而为”的场景。
3. tryLock(long timeout, TimeUnit unit)
- 带超时的获取,尊重公平性(如果是公平锁,会排队等)。
- 可被中断。
4. lockInterruptibly()
- 可中断的阻塞获取。如果线程被 interrupt,抛出
InterruptedException。
5. unlock()
- 必须由当前持有锁的线程调用,否则抛
IllegalMonitorStateException。 - 内部调用
sync.release(1),减少 hold count。
五、使用最佳实践
✅ 永远用 try-finally 包裹 lock/unlock:
lock.lock();
try {
// 临界区
} finally {
lock.unlock(); // 防止异常导致死锁
}
❌ 错误写法(可能死锁):
lock.lock();
// 如果这里抛异常,unlock 永远不会执行!
doSomething();
lock.unlock();
六、监控与调试方法(用于诊断)
| 方法 | 作用 |
|---|---|
getHoldCount() |
当前线程持有锁的次数 |
isHeldByCurrentThread() |
是否被当前线程持有 |
isLocked() |
是否被任意线程持有 |
hasQueuedThreads() |
是否有线程在等待 |
getQueueLength() |
估算等待线程数 |
getOwner() |
获取当前持有者线程 |
这些方法主要用于 调试、监控、测试,不要用于业务逻辑控制!
七、序列化行为
- 反序列化后的
ReentrantLock总是处于 unlocked 状态。 - 即使你序列化时它是 locked 的,反序列化后也会重置为 0。
- 这和
synchronized一致(对象锁无法跨 JVM 传递)。
八、递归锁上限
- 最多支持 2,147,483,647 次重入(即
Integer.MAX_VALUE)。 - 超出会抛
Error("Maximum lock count exceeded")。 - 实际几乎不可能达到,除非代码有严重 bug(如无限递归加锁)。
总结一句话:
ReentrantLock是一个功能更强、更灵活的 synchronized 替代品,支持中断、超时、公平性、多条件变量,但需要手动管理锁的释放,且默认是非公平的以换取更高性能。
如果你需要精细控制并发行为(比如响应中断、避免死等、多条件等待),就用 ReentrantLock;否则,简单场景用 synchronized 更安全简洁。
更多推荐



所有评论(0)