1. 为什么需要理解ReentrantLock而不仅仅是会用?
作为Java开发者,你可能已经在项目中用过ReentrantLock很多次了。但你真的理解它背后的工作原理吗?很多人只是机械地调用lock()和unlock(),遇到死锁问题就束手无策。今天我们就来彻底拆解这个Java并发编程中的重量级选手。
ReentrantLock是Java 5引入的显式锁机制,相比synchronized关键字,它提供了更灵活的锁控制能力。但灵活也意味着复杂,理解其内部实现原理,能帮助你在高并发场景下更好地诊断问题、优化性能。
2. ReentrantLock核心架构解析
2.1 锁的三大核心组件
ReentrantLock的实现主要依赖于三个关键组件:
- 同步器(Sync):继承自AQS(AbstractQueuedSynchronizer),是锁实现的核心
- 非公平锁(NonfairSync):默认实现,允许插队获取锁
- 公平锁(FairSync):严格按照FIFO顺序获取锁
java复制// 典型使用示例
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
2.2 AQS工作原理深度剖析
AQS是ReentrantLock的基石,它维护了一个volatile int state变量和一个FIFO线程等待队列。state=0表示锁未被占用,state>0表示锁被占用且可能重入。
锁获取的核心逻辑:
- 尝试通过CAS修改state
- 成功则获取锁
- 失败则进入等待队列
关键点:AQS使用模板方法模式,将具体获取/释放锁的逻辑交给子类实现
3. 公平锁与非公平锁实现差异
3.1 非公平锁实现原理
java复制final void lock() {
if (compareAndSetState(0, 1)) // 直接尝试抢锁
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
非公平锁的特点:
- 新来的线程可以直接尝试获取锁,不用排队
- 吞吐量更高,但可能导致线程饥饿
- 适合锁持有时间短的场景
3.2 公平锁实现原理
java复制final void lock() {
acquire(1); // 直接进入队列排队
}
公平锁的特点:
- 严格按照FIFO顺序获取锁
- 避免线程饥饿,但吞吐量较低
- 适合锁持有时间长的场景
4. 重入机制实现细节
ReentrantLock的可重入性是通过记录当前持有锁的线程和重入次数实现的:
java复制protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 重入判断
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
重入锁的实现关键:
- 检查当前线程是否是锁持有者
- 如果是,简单增加state计数
- 释放锁时需要对应减少计数,直到state=0才真正释放
5. 条件变量(Condition)实现原理
ReentrantLock的条件变量比Object的wait/notify更灵活:
java复制Condition condition = lock.newCondition();
// 等待方
lock.lock();
try {
while(!conditionSatisfied) {
condition.await(); // 释放锁并进入等待
}
// 条件满足后的处理
} finally {
lock.unlock();
}
// 通知方
lock.lock();
try {
// 改变条件
condition.signalAll();
} finally {
lock.unlock();
}
Condition内部维护了一个条件队列,await()会将当前线程加入这个队列并释放锁,signal()会将线程从条件队列转移到同步队列。
6. 性能优化与最佳实践
6.1 锁分段技术
对于高并发场景,可以考虑将一个大锁拆分为多个小锁:
java复制class StripedMap {
private final ReentrantLock[] locks;
private final Map<String, String>[] segments;
public StripedMap(int stripes) {
locks = new ReentrantLock[stripes];
for (int i = 0; i < stripes; i++) {
locks[i] = new ReentrantLock();
}
segments = new Map[stripes];
// 初始化segments...
}
public void put(String key, String value) {
int hash = key.hashCode() % locks.length;
locks[hash].lock();
try {
segments[hash].put(key, value);
} finally {
locks[hash].unlock();
}
}
}
6.2 避免常见陷阱
- 忘记释放锁:务必在finally块中释放锁
- 锁粒度过大:只锁必要的代码块
- 死锁:按固定顺序获取多个锁
- 活锁:考虑使用tryLock()带超时机制
7. 实战问题排查技巧
7.1 死锁诊断
使用jstack工具可以检测死锁:
bash复制jstack <pid>
输出中查找"deadlock"关键词,会显示死锁线程的堆栈信息。
7.2 性能瓶颈定位
使用JProfiler或VisualVM监控:
- 锁争用情况
- 线程阻塞时间
- 锁持有时间
8. 与synchronized的对比选择
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现机制 | Java代码实现 | JVM内置实现 |
| 锁获取方式 | 显式lock/unlock | 隐式获取释放 |
| 可中断性 | 支持lockInterruptibly() | 不支持 |
| 公平锁 | 可配置 | 非公平 |
| 条件变量 | 支持多个Condition | 只有一个等待队列 |
| 性能 | Java 6+优化后两者相当 | Java 6+优化后两者相当 |
| 代码复杂度 | 高 | 低 |
选择建议:
- 简单场景用synchronized
- 需要高级功能时用ReentrantLock
- 高竞争环境下测试两者性能差异
9. 源码级调试技巧
在IDE中调试ReentrantLock时,重点关注:
- AbstractQueuedSynchronizer的state变化
- 等待队列中节点的状态变化
- 条件变量的signal/signalAll操作
调试技巧:
- 在AQS的enq()方法设断点观察入队过程
- 在shouldParkAfterFailedAcquire()观察节点状态变化
- 使用条件断点过滤特定线程
10. 高级应用场景
10.1 读写锁实现
基于ReentrantLock可以实现读写锁:
java复制class ReadWriteLock {
private final ReentrantLock lock = new ReentrantLock();
private final Condition readCondition = lock.newCondition();
private final Condition writeCondition = lock.newCondition();
private int readers = 0;
private boolean writing = false;
public void lockRead() throws InterruptedException {
lock.lock();
try {
while (writing) {
readCondition.await();
}
readers++;
} finally {
lock.unlock();
}
}
public void lockWrite() throws InterruptedException {
lock.lock();
try {
while (writing || readers > 0) {
writeCondition.await();
}
writing = true;
} finally {
lock.unlock();
}
}
// 解锁方法类似...
}
10.2 分布式锁扩展
结合Redis可以实现分布式可重入锁:
java复制public class RedisReentrantLock {
private final Jedis jedis;
private final String lockKey;
private final String lockValue;
private final int expireTime;
public boolean tryLock(long timeout, TimeUnit unit) {
long start = System.currentTimeMillis();
try {
while (true) {
String result = jedis.set(lockKey, lockValue, "NX", "PX", expireTime);
if ("OK".equals(result)) {
return true;
}
// 检查是否已经持有锁(可重入)
String currentValue = jedis.get(lockKey);
if (lockValue.equals(currentValue)) {
return true;
}
if (System.currentTimeMillis() - start > unit.toMillis(timeout)) {
return false;
}
Thread.sleep(100);
}
} catch (Exception e) {
return false;
}
}
// 解锁需要检查lockValue匹配,避免误删其他线程的锁
}
在实际项目中,理解ReentrantLock的底层原理能让你在遇到复杂的并发问题时更快定位原因。我曾经在一个高并发订单系统中,通过分析AQS队列状态,发现了一个隐藏很深的锁竞争问题,最终通过锁分段技术将系统吞吐量提升了3倍。