知其然要知其所以然,探索每一个知识点背后的意义,你知道的越多,你不知道的越多,一起学习,一起进步,如果文章感觉对您有用的话,关注、收藏、点赞,有困惑的地方可以评论,我们一起探讨!
ReentrantLock 深度分析
ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中的核心类,实现了 可重入互斥锁,相比 synchronized 关键字,提供了更灵活的锁控制、可中断性、公平锁策略和多个条件变量支持。以下是其设计思想、实现原理及最佳实践的系统分析。
一、核心设计思想
1. 可重入性(Reentrancy)
- 定义:同一线程可多次获取同一把锁,避免自身死锁。
- 实现:通过计数器记录锁的持有次数,释放时需完全解锁(计数器归零)。
- 示例:
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { lock.lock(); // 可重入,计数器+1 // 临界区代码 } finally { lock.unlock(); // 计数器-1 lock.unlock(); // 计数器归零,释放锁 }
2. 公平性(Fairness)
- 公平锁:锁的获取按请求顺序(FIFO),避免线程饥饿。
- 非公平锁(默认):允许插队,减少线程切换开销,提高吞吐量。
- 选择策略:
- 高竞争场景:公平锁减少饥饿,但性能较低。
- 低竞争场景:非公平锁性能更优。
3. 显式锁控制
- 手动加锁/解锁:需在
try-finally块中确保锁释放,避免死锁。 - 灵活性:支持尝试获取锁(
tryLock)、超时获取(lockInterruptibly)、可中断等待等。
二、底层实现:AQS(AbstractQueuedSynchronizer)
ReentrantLock 的核心依赖于 AQS 框架,通过 CLH 队列(Craig, Landin, Hagersten 队列)管理线程的阻塞与唤醒。
1. AQS 的核心机制
- 状态变量(state):
- 对于
ReentrantLock,state表示锁的持有计数(0 表示未锁定,≥1 表示锁定次数)。
- 对于
- CLH 队列:
- 双向链表结构,保存等待锁的线程。
- 每个节点(Node)封装线程的等待状态(如取消、唤醒信号)。
2. 加锁流程(以非公平锁为例)
sequenceDiagram
participant Thread as 线程
participant AQS as AQS状态机
participant CLH as CLH队列
Thread->>AQS: 1. 尝试CAS设置state(直接获取锁)
alt CAS成功(state=0→1)
AQS-->>Thread: 获取锁成功,设置当前线程为持有者
else CAS失败
Thread->>AQS: 2. 尝试再次获取锁(可能插队)
alt 再次CAS成功
AQS-->>Thread: 获取锁成功
else
Thread->>AQS: 3. 将线程包装为Node加入CLH队列尾部
AQS->>CLH: 加入队列
loop 自旋或阻塞
CLH->>AQS: 4. 检查前驱节点是否为头节点
AQS->>Thread: 5. 若前驱是头节点,尝试CAS获取锁
alt 获取成功
AQS->>CLH: 移除当前节点,设为头节点
AQS-->>Thread: 获取锁成功
else
AQS->>Thread: 6. 挂起线程(LockSupport.park)
end
end
end
end
3. 解锁流程
public void unlock() {
sync.release(1); // 调用AQS的release方法
}
// AQS的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) { // 尝试释放锁(由ReentrantLock.Sync实现)
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // 唤醒CLH队列中的下一个线程
return true;
}
return false;
}
三、公平锁 vs 非公平锁的源码差异
1. 非公平锁(NonfairSync)
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { // 直接尝试CAS,允许插队
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 可重入
setState(c + acquires);
return true;
}
return false;
}
2. 公平锁(FairSync)
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && // 检查CLH队列是否有等待线程
compareAndSetState(0, acquires)) { // 无等待线程才尝试CAS
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) { // 可重入
setState(c + acquires);
return true;
}
return false;
}
四、高级功能与使用场景
1. 可中断锁获取(lockInterruptibly)
ReentrantLock lock = new ReentrantLock();
try {
lock.lockInterruptibly(); // 可响应中断
try {
// 临界区代码
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 处理中断
}
2. 超时尝试获取锁(tryLock)
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 等待1秒
try {
// 临界区代码
} finally {
lock.unlock();
}
} else {
// 超时处理
}
3. 条件变量(Condition)
- 与
synchronized的wait/notify对比:- 一个
ReentrantLock可创建多个Condition,实现精细化的线程等待/唤醒。 - 示例:生产者-消费者模型。
ReentrantLock lock = new ReentrantLock(); Condition notFull = lock.newCondition(); // 队列未满条件 Condition notEmpty = lock.newCondition(); // 队列非空条件 // 生产者 lock.lock(); try { while (queue.isFull()) { notFull.await(); // 等待队列未满 } queue.add(item); notEmpty.signal(); // 唤醒消费者 } finally { lock.unlock(); } // 消费者 lock.lock(); try { while (queue.isEmpty()) { notEmpty.await(); // 等待队列非空 } item = queue.remove(); notFull.signal(); // 唤醒生产者 } finally { lock.unlock(); } - 一个
五、最佳实践与常见问题
1. 必须手动释放锁
- 错误示例:
lock.lock(); // 若此处抛出异常,锁无法释放! lock.unlock(); - 正确做法:始终在
finally块中释放锁。
2. 避免嵌套锁
- 死锁风险:
lockA.lock(); try { lockB.lock(); // 若另一线程以相反顺序获取锁,可能死锁 // ... } finally { lockB.unlock(); lockA.unlock(); } - 解决:按全局固定顺序获取锁。
3. 性能调优
- 选择非公平锁:默认策略,适合大多数场景。
- 减少锁粒度:结合分段锁(如
ConcurrentHashMap)。 - 监控锁竞争:使用
jstack或JFR分析线程阻塞情况。
六、ReentrantLock vs synchronized
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现方式 | JDK 类,基于 AQS | JVM 内置关键字 |
| 锁获取方式 | 显式调用 lock()/unlock() | 隐式获取(代码块/方法) |
| 公平性 | 支持公平/非公平锁 | 仅非公平锁 |
| 可中断性 | 支持(lockInterruptibly) | 不支持 |
| 超时机制 | 支持(tryLock) | 不支持 |
| 条件变量 | 支持多个 Condition | 单一 wait/notify |
| 性能 | 高竞争下更优(可配置策略) | Java 6 后优化,低竞争下性能接近 |
| 代码复杂度 | 高(需手动管理锁) | 低(自动释放) |
七、源码分析:AQS 的等待队列
// AQS 中的 Node 类(简化版)
static final class Node {
volatile int waitStatus; // 等待状态(CANCELLED、SIGNAL等)
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 关联的线程
Node nextWaiter; // 条件队列中的下一个节点
}
// CLH 队列示意图
Head -> Node(Thread1, SIGNAL) ↔ Node(Thread2, CANCELLED) ↔ Tail
八、总结
ReentrantLock 的设计体现了 灵活性 和 可扩展性:
- 基于 AQS 的模板方法模式:将锁的获取/释放逻辑委托给子类实现。
- 分离公平性与非公平性:通过不同的
Sync实现类支持不同策略。 - 条件变量的精细化控制:解决
synchronized中wait/notify的局限性。
适用场景:
- 需要可中断、超时或公平锁的高并发场景。
- 需要多个条件变量实现复杂线程协作(如线程池任务队列)。