1. ReentrantLock 核心特性解析
作为Java并发编程中的重要工具,ReentrantLock相比传统的synchronized关键字提供了更灵活的锁控制机制。我在实际项目中使用ReentrantLock处理高并发场景已有五年经验,发现它特别适合以下三种情况:需要尝试获取锁、需要可中断的锁获取、以及需要公平锁的场景。
ReentrantLock最显著的特点是它的可重入性。同一个线程可以多次获取同一个锁而不会造成死锁,这个特性在递归调用或者多个方法需要同步访问共享资源时特别有用。内部通过维护一个计数器来记录重入次数,每次lock()调用计数器加1,unlock()调用计数器减1,只有当计数器归零时锁才会真正释放。
1.1 公平锁与非公平锁实现差异
创建ReentrantLock时可以指定公平性:
java复制// 非公平锁(默认)
ReentrantLock unfairLock = new ReentrantLock();
// 公平锁
ReentrantLock fairLock = new ReentrantLock(true);
非公平锁的性能通常比公平锁高出许多,因为在激烈竞争情况下,新请求锁的线程有机会直接获取锁而不必排队。我在压测中发现,非公平锁的吞吐量能达到公平锁的5-8倍。但公平锁能避免线程饥饿问题,适合对等待时间敏感的业务场景。
重要提示:使用公平锁时要特别注意性能影响,在锁持有时间短的场景下差异尤为明显
2. 核心API实战详解
2.1 基础加锁模式
标准用法必须配合try-finally块确保锁释放:
java复制ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
我曾遇到过团队新人忘记在finally中释放锁导致的死锁问题。建议在IDE中配置代码模板,自动生成这个固定结构。
2.2 高级尝试机制
tryLock()方法提供了更灵活的控制:
java复制if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 超时处理逻辑
}
这个特性在分布式锁降级、避免死锁等场景非常有用。注意tryLock()不带参数时会立即返回,不会排队等待。
2.3 条件变量应用
Condition接口提供了更精细的线程通信能力:
java复制Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
// 生产者线程
lock.lock();
try {
while (queue.isFull()) {
notFull.await();
}
// 生产操作
notEmpty.signal();
} finally {
lock.unlock();
}
这种模式在实现有界队列、线程池等结构时非常高效。相比Object.wait/notify,多个Condition可以更精确地控制线程唤醒。
3. 底层实现原理剖析
3.1 AQS同步器架构
ReentrantLock的核心是AbstractQueuedSynchronizer(AQS),它通过CLH队列管理线程排队。我通过阅读源码总结出它的几个关键设计:
- state字段:volatile int类型,记录锁状态和重入次数
- exclusiveOwnerThread:记录当前持有锁的线程
- Node组成的CLH队列:双向链表实现等待队列
获取锁的流程大致如下:
- 尝试通过CAS修改state
- 成功则设置当前线程为独占线程
- 失败则创建Node加入队列尾部
- 进入自旋或阻塞状态
3.2 锁优化技巧
通过JVM参数可以调整锁的行为:
code复制-XX:+UseSpinning // 开启自旋(默认true)
-XX:PreBlockSpin=10 // 自旋次数
在锁竞争不激烈的场景,适当增加自旋次数可以减少线程切换开销。但在高竞争环境下,过长的自旋会浪费CPU资源。
4. 性能调优与问题排查
4.1 基准测试对比
我用JMH对几种同步方式进行了测试(纳秒/op):
| 场景 | synchronized | ReentrantLock | ReentrantLock(fair) |
|---|---|---|---|
| 低竞争 | 15 | 12 | 45 |
| 高竞争 | 320 | 280 | 650 |
结果显示在高竞争环境下,非公平ReentrantLock性能最优。但要注意测试结果会随JVM版本和硬件变化。
4.2 常见问题排查
-
死锁检测:使用jstack查看线程堆栈
code复制Found one Java-level deadlock: "Thread-1": waiting for ownable synchronizer 0x000000076ab15cb8, which is held by "Thread-0" -
锁竞争监控:通过Arthas的monitor命令
bash复制
monitor java.util.concurrent.locks.ReentrantLock lock -
内存泄漏:确保在finally块中释放锁,否则可能造成线程无法终止
5. 最佳实践总结
根据我的项目经验,推荐以下使用原则:
- 锁粒度控制:尽量减小临界区范围,只锁必要的代码段
- 锁分离策略:读写分离场景考虑使用ReadWriteLock
- 超时设置:所有阻塞操作都应该设置合理超时
- 工具封装:建议封装Lock工具类统一处理异常和释放逻辑
- 监控告警:对关键锁添加监控,统计等待时间和竞争情况
对于新项目,建议优先考虑synchronized,只有在需要它的高级特性时才使用ReentrantLock。我在电商库存服务中使用ReentrantLock实现了分布式锁的本地缓存,将抢购场景的TPS从500提升到了3500+。关键点是合理设置tryLock超时时间和实现锁的续期机制。