1. ReentrantLock 核心概念解析
ReentrantLock 是 Java 并发包中一个重要的同步工具,它提供了比 synchronized 更灵活的线程同步机制。与 synchronized 关键字不同,ReentrantLock 是一个显式锁,需要手动获取和释放。
ReentrantLock 的核心特性在于它的"可重入性"——同一个线程可以多次获取同一把锁而不会导致死锁。这种设计使得递归调用和回调场景下的同步控制变得更加自然。在内部实现上,锁会维护一个持有计数(hold count),每次重入时计数加1,释放时计数减1,只有当计数归零时锁才真正释放。
关键点:ReentrantLock 的可重入特性解决了 synchronized 在递归调用场景下的局限性,使得线程可以重复获取已经持有的锁。
2. ReentrantLock 与 synchronized 的对比分析
2.1 功能特性对比
ReentrantLock 相比 synchronized 提供了更多高级功能:
- 可中断的锁获取:lockInterruptibly() 方法允许在等待锁时响应中断
- 超时获取锁:tryLock(long timeout, TimeUnit unit) 可以设置获取锁的超时时间
- 公平性选择:构造函数支持创建公平锁(按请求顺序获取)或非公平锁
- 条件变量支持:通过 newCondition() 可以创建多个条件队列
2.2 性能差异
在低竞争场景下,synchronized 经过 JVM 优化后性能通常优于 ReentrantLock。但在高竞争环境下,ReentrantLock 的非公平模式往往能提供更好的吞吐量。公平模式虽然保证了顺序性,但会带来额外的上下文切换开销。
2.3 使用场景选择
synchronized 适用于:
- 简单的同步需求
- 方法级别的同步
- 不需要高级特性的场景
ReentrantLock 适用于:
- 需要细粒度控制的同步场景
- 需要可中断、超时等高级特性的场景
- 需要多个条件变量的复杂同步逻辑
3. ReentrantLock 的实现原理
3.1 AQS 基础框架
ReentrantLock 的实现依赖于 AbstractQueuedSynchronizer (AQS) 框架。AQS 使用一个 volatile int 类型的 state 变量表示同步状态,并通过一个 FIFO 队列管理获取锁失败的线程。
在 ReentrantLock 中,state 表示锁的重入次数:
- 0 表示锁未被任何线程持有
- 1 表示锁被某个线程持有
-
1 表示锁被同一个线程多次重入
3.2 公平锁与非公平锁
ReentrantLock 提供了两种实现:
- 非公平锁(默认):线程尝试获取锁时直接尝试 CAS 操作,不检查队列
- 公平锁:严格按照 FIFO 顺序获取锁,保证先到先得
公平锁的实现会先检查队列中是否有等待线程,只有在没有等待线程时才会尝试获取锁。这种差异导致了性能上的不同表现。
3.3 锁获取流程
典型的锁获取流程如下:
- 尝试通过 CAS 修改 state 变量
- 如果成功,设置当前线程为锁持有者
- 如果失败,将线程加入等待队列并可能挂起
- 当锁释放时,唤醒队列中的下一个线程
4. ReentrantLock 的最佳实践
4.1 基本使用模式
正确的 ReentrantLock 使用应该遵循以下模式:
java复制ReentrantLock lock = new ReentrantLock();
//...
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
重要提示:必须将 unlock() 放在 finally 块中,确保锁一定会被释放,避免死锁。
4.2 高级特性应用
4.2.1 可中断锁获取
java复制try {
lock.lockInterruptibly();
// 临界区代码
} catch (InterruptedException e) {
// 处理中断
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
4.2.2 超时锁尝试
java复制if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
// 超时处理逻辑
}
4.3 条件变量的使用
ReentrantLock 的条件变量比 Object 的 wait/notify 更灵活:
java复制Condition condition = lock.newCondition();
// 等待线程
lock.lock();
try {
while (!conditionSatisfied) {
condition.await();
}
// 处理条件满足后的逻辑
} finally {
lock.unlock();
}
// 通知线程
lock.lock();
try {
conditionSatisfied = true;
condition.signalAll();
} finally {
lock.unlock();
}
5. 常见问题与性能优化
5.1 死锁预防
虽然 ReentrantLock 本身不会导致死锁,但不正确的使用仍然可能产生死锁。预防措施包括:
- 总是以相同的顺序获取多个锁
- 使用 tryLock 设置超时
- 避免在持有锁时调用外部方法
5.2 性能调优建议
- 在低竞争场景考虑使用 synchronized
- 高竞争场景下使用非公平锁(默认)
- 只在确实需要公平性保证时使用公平锁
- 合理设置锁粒度,避免过大或过小
- 考虑使用读写锁(ReentrantReadWriteLock)替代纯互斥锁
5.3 调试技巧
当遇到锁相关问题时,可以:
- 使用 Thread.dumpStack() 打印堆栈跟踪
- 通过 lock.getHoldCount() 检查重入次数
- 使用 lock.getQueueLength() 监控等待线程数
- 借助 JConsole 或 VisualVM 等工具监控锁状态
6. ReentrantLock 在复杂系统中的应用
6.1 分布式锁的本地实现
虽然 ReentrantLock 是本地锁,但可以结合分布式锁框架实现更复杂的同步逻辑。例如,在分布式系统中,可以用 ReentrantLock 保护本地资源,同时使用 Redis 或 ZooKeeper 实现跨节点同步。
6.2 与线程池的配合使用
当使用线程池时,需要注意:
- 避免在任务中持有锁提交新任务到同一线程池
- 考虑使用 ThreadPoolExecutor 的饱和策略处理锁竞争
- 对于长时间持有锁的任务,考虑使用可中断锁
6.3 在异步编程中的注意事项
在异步/响应式编程中:
- 避免在回调中持有锁
- 考虑使用原子变量或并发集合替代锁
- 如果必须使用锁,确保锁的持有时间尽可能短
7. ReentrantLock 的替代方案
7.1 StampedLock
Java 8 引入的 StampedLock 提供了乐观读模式,在读多写少场景下性能更好。但它不是可重入的,且没有条件变量支持。
7.2 ReadWriteLock
ReentrantReadWriteLock 允许多个读线程同时访问,适用于读多写少的场景。它的写锁是可重入的,但读锁不能升级为写锁。
7.3 并发集合
对于许多同步需求,使用 ConcurrentHashMap、CopyOnWriteArrayList 等并发集合可能是更好的选择,它们内部已经实现了高效的并发控制。
