Appearance
Locks-ReentrantLock源码解析
1. ReentrantLock简介
ReentrantLock是Java并发包(java.util.concurrent.locks)中的一个可重入互斥锁。它具有与使用synchronized方法和语句访问的隐式监视器锁相同的基本行为和语义,但扩展了一些高级功能,如可轮询的锁请求、定时的锁请求、可中断的锁请求等。它还具有更好的可伸缩性(在竞争激烈的情况下表现更好)。
主要特点
- 可重入性:同一个线程可以多次获取同一把锁,而不会产生死锁。
- 可中断:等待获取锁的线程可以被中断。
- 公平性选择:支持公平锁和非公平锁。公平锁保证等待时间最长的线程优先获取锁,但性能略低;非公平锁则提供更高的吞吐量。
- 超时获取:可以尝试在指定时间内获取锁,超时则放弃。
- 条件变量(Condition):一个锁可以关联多个条件变量,提供更灵活的线程等待/唤醒机制。
2. ReentrantLock基本使用
java
// 基本使用模式
Lock lock = new ReentrantLock();
lock.lock(); // 加锁
try {
// 临界区代码
} finally {
lock.unlock(); // 释放锁
}
3. 源码分析
ReentrantLock的核心是基于AbstractQueuedSynchronizer(AQS)实现的。在ReentrantLock中有三个内部类:Sync、NonfairSync和FairSync,其中Sync继承自AQS,NonfairSync和FairSync分别继承Sync,分别实现非公平锁和公平锁的获取逻辑。
3.1 Sync类分析
首先,我们来看Sync类。由于ReentrantLock是可重入锁,所以它使用一个state变量(继承自AQS)来表示锁的持有计数。当state为0时表示锁未被任何线程持有,大于0时表示被某个线程持有,并且值表示重入次数。
Sync类是一个抽象类,主要提供非公平尝试获取和释放锁的机制,但是FairSync会覆盖部分方法以实现公平获取。
3.2 NonfairSync(非公平锁)
非公平锁在获取锁时,不管等待队列中是否有其他线程在等待,都会直接尝试获取锁,如果获取失败,则加入队列等待。
java
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
// 非公平锁获取的入口方法
final void lock() {
// 非公平策略核心:直接进行一次CAS尝试获取锁
if (compareAndSetState(0, 1)) { // 成功则设置当前线程为独占线程
setExclusiveOwnerThread(Thread.currentThread());
} else { // 失败后进入AQS标准的获取流程
acquire(1); // 调用AQS.acquire(),最终会调用tryAcquire()
}
}
// 重写AQS的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
// 调用Sync类中定义的nonfairTryAcquire方法
return nonfairTryAcquire(acquires);
}
}
在nonfairTryAcquire
方法中(定义在Sync中):
java
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
// 当前锁未被任何线程持有,尝试获取锁
if (c == 0) {
// 非公平策略体现:直接进行CAS抢占
// 如果成功,则设置当前线程为独占线程
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 可重入逻辑:如果当前线程已经是锁的持有者
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
// 重入次数溢出检测(MAX_COUNT = 65535)
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 更新state值实现重入计数增加
setState(nextc);
return true;
}
// 返回false表示获取锁失败,主要有两种情况:
// 1. 锁当前被其他线程持有
// 2. CAS操作失败(存在并发竞争)
return false;
}
3.3 FairSync(公平锁)
公平锁的获取方式:
java
/**
* 公平锁实现,继承自Sync类
* 公平锁严格按照FIFO顺序获取锁,避免线程饥饿
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
/**
* 获取锁的入口方法
* 直接调用AQS的acquire方法,公平锁不进行直接CAS尝试
*/
final void lock() {
acquire(1); // 参数1表示请求获取锁
}
/**
* 尝试获取锁的核心方法
* @param acquires 请求获取锁的数量,通常为1
* @return true表示获取成功,false表示失败
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取当前锁状态
// 情况1:锁未被任何线程持有
if (c == 0) {
// 公平锁核心逻辑:必须先检查队列中是否有更早的等待线程
if (!hasQueuedPredecessors() && // 队列中没有更早的线程
compareAndSetState(0, acquires)) { // CAS设置状态
setExclusiveOwnerThread(current); // 设置当前线程为锁持有者
return true;
}
}
// 情况2:锁已被当前线程持有(可重入)
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 增加重入次数
// 重入次数溢出检查(达到int最大值)
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc); // 更新状态
return true;
}
// 获取锁失败的情况:
// 1. 锁被其他线程持有
// 2. 队列中有更早的等待线程
// 3. CAS竞争失败
return false;
}
}
3.4 AQS同步队列管理
ReentrantLock 的等待队列管理由 AQS (AbstractQueuedSynchronizer) 实现,采用 CLH (Craig, Landin, and Hagersten) 队列的变体。队列通过两个关键指针和状态变量进行管理:
java
// 队列头节点,通常表示当前持有锁的节点
// 注意:头节点是延迟初始化的,只有第一次发生争用时才会创建
private transient volatile Node head;
// 队列尾节点,新节点会添加到尾部
private transient volatile Node tail;
// 锁状态:
// 0 - 表示锁未被任何线程持有
// >0 - 表示锁被某个线程持有,数值表示重入次数
private volatile int state;
Node 节点是 AQS 队列的基本单元,包含以下关键字段:
java
static final class Node {
// 等待状态值:
static final int CANCELLED = 1; // 节点因超时或中断被取消
static final int SIGNAL = -1; // 后继节点需要被唤醒
static final int CONDITION = -2; // 节点在条件队列中等待
static final int PROPAGATE = -3; // 共享模式下需要传播唤醒
volatile int waitStatus; // 当前节点的等待状态
volatile Node prev; // 前驱节点
volatile Node next; // 后继节点
volatile Thread thread; // 关联的线程
Node nextWaiter; // 用于区分共享/独占模式,或条件队列链接
// 判断是否为共享模式节点
final boolean isShared() {
return nextWaiter == SHARED;
}
// 构造方法
Node() {} // 用于建立初始头节点或共享标记
Node(Thread thread, Node mode) { // 用于addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // 用于条件队列
this.waitStatus = waitStatus;
this.thread = thread;
}
}
3.5 条件变量实现 (ConditionObject)
ReentrantLock 的条件变量是通过内部类 ConditionObject 实现的,它是 AQS 的内部类并实现了 Condition 接口。以下是其核心实现:
java
public class ConditionObject implements Condition {
private transient Node firstWaiter; // 条件队列头节点
private transient Node lastWaiter; // 条件队列尾节点
/**
* 使当前线程等待,直到被signal或中断
* 1. 创建节点加入条件队列
* 2. 完全释放锁
* 3. 阻塞直到被转移到同步队列
* 4. 重新获取锁
*/
public final void await() throws InterruptedException {
if (Thread.interrupted()) // 检查中断状态
throw new InterruptedException();
Node node = addConditionWaiter(); // 创建新节点加入条件队列尾部
int savedState = fullyRelease(node); // 完全释放锁,返回释放前的state值
int interruptMode = 0;
// 循环检查是否被转移到同步队列
while (!isOnSyncQueue(node)) {
LockSupport.park(this); // 阻塞当前线程
// 检查中断状态,可能被signal或中断唤醒
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 被唤醒后,尝试获取锁并处理中断
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
// 清理取消的等待节点
if (node.nextWaiter != null)
unlinkCancelledWaiters();
// 根据中断模式处理中断
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
/**
* 唤醒条件队列中等待时间最长的线程
* 1. 检查当前线程是否持有锁
* 2. 将条件队列头节点转移到同步队列
*/
public final void signal() {
if (!isHeldExclusively()) // 检查是否独占模式
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); // 从条件队列头开始唤醒
}
}
4. 核心设计原理
4.1 可重入实现机制
ReentrantLock 通过维护两个关键状态实现可重入:
state
:记录锁被获取的次数exclusiveOwnerThread
:记录当前持有锁的线程
当线程第一次获取锁时,state=1;同一线程再次获取锁时,state递增;释放锁时state递减,直到state=0时锁才完全释放。
4.2 公平性与非公平性差异
非公平锁
- 新线程可以直接尝试获取锁,不必排队
- 可能导致"饥饿"现象
- 吞吐量通常更高
公平锁
- 严格按照FIFO顺序获取锁
- 避免饥饿问题
- 吞吐量通常较低
4.3 性能优化点
- CAS操作:使用compareAndSetState实现无锁化的状态变更
- 自旋优化:在入队前短暂自旋尝试获取锁
- 队列管理优化:通过CLH队列减少争用
- 延迟初始化:head和tail节点按需创建
5. 使用示例与最佳实践
基本使用模式
java
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
高级特性使用
尝试获取锁
java
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 获取锁成功
} finally {
lock.unlock();
}
} else {
// 获取锁超时
}
可中断获取锁
java
try {
lock.lockInterruptibly();
// 临界区代码
} catch (InterruptedException e) {
// 处理中断
} finally {
if (lock.isHeldByCurrentThread()) {
lock.unlock();
}
}
条件变量使用
java
Condition condition = lock.newCondition();
lock.lock();
try {
while (!conditionSatisfied) {
condition.await();
}
// 处理条件满足的情况
} finally {
lock.unlock();
}
6. 与synchronized的对比
特性 | ReentrantLock | synchronized |
---|---|---|
实现机制 | Java代码实现,基于AQS | JVM内置实现 |
锁获取方式 | 显式调用lock()/unlock() | 隐式获取和释放 |
可中断 | 支持 | 不支持 |
超时机制 | 支持 | 不支持 |
公平性 | 可选择公平或非公平 | 非公平 |
条件变量 | 支持多个Condition | 单个等待队列 |
性能 | 高竞争下表现更好 | 低竞争下性能更好 |
代码复杂度 | 需要手动释放锁 | 自动释放 |
7. 典型应用场景
- 需要可中断锁的场景:如取消长时间等待的任务
- 需要尝试获取锁的场景:避免死锁或长时间等待
- 需要公平性的场景:确保线程按顺序获取资源
- 需要多个条件队列的场景:如生产者-消费者模型
- 需要锁细粒度控制的场景:如锁分段技术
8. 常见问题与解决方案
忘记释放锁
- 总是使用try-finally块确保锁释放
- 使用静态代码分析工具检查
死锁问题
- 使用tryLock设置超时
- 保持一致的锁获取顺序
性能问题
- 评估是否真的需要锁
- 考虑减小临界区范围
- 在低竞争场景考虑使用synchronized
条件变量误用
- 总是使用while循环检查条件
- 确保在调用await()前持有锁
9. 总结
ReentrantLock 是 Java 并发编程中的重要工具,它提供了比 synchronized 更丰富的功能特性。通过深入理解其实现原理,开发者可以:
- 更合理地选择锁策略(公平/非公平)
- 更高效地使用高级特性(可中断、超时等)
- 更好地诊断和解决并发问题
- 在适当场景下获得更好的性能表现
掌握 ReentrantLock 的源码实现,不仅有助于正确使用该工具,也是理解 Java 并发框架设计思想的重要途径。