一、核心概念

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 更安全简洁。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐