1. 为什么我们需要理解ReentrantLock的底层原理
很多Java开发者都有这样的经历:面试时被问到ReentrantLock的实现原理,只能支支吾吾说出"它是可重入的"、"比synchronized更灵活"这样的表面结论。在实际工作中,也只会机械地使用lock()和unlock()方法,遇到死锁问题就束手无策。这正是因为缺乏对底层机制的深入理解。
我曾在生产环境遇到过一个典型场景:某个核心服务在高并发时频繁出现线程阻塞,表面看是ReentrantLock导致的,但团队花了三天时间才定位到根本原因——公平锁与非公平锁的选择不当。如果当时有人真正理解AQS(AbstractQueuedSynchronizer)的排队机制,这个问题可能半小时就能解决。
2. ReentrantLock的核心架构解析
2.1 可重入性的实现机制
ReentrantLock的可重入特性是通过一个计数器实现的。当线程第一次获取锁时,计数器从0变为1;同一线程再次获取锁时,计数器会递增。每次释放锁时计数器递减,只有当计数器归零时,锁才真正被释放。
java复制final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState(); // 获取当前锁状态
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires; // 重入时计数器增加
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
关键点:可重入锁必须配套相同次数的unlock()调用,否则会导致锁无法释放。这是新手常犯的错误。
2.2 AQS队列的运作原理
AbstractQueuedSynchronizer是ReentrantLock的核心,它维护了一个CLH队列(Craig, Landin, and Hagersten lock queue)。这个队列不是传统的链表结构,而是通过线程内保存的前驱节点信息实现的虚拟队列。
当锁被占用时,新来的线程会被包装成Node节点加入队列尾部。队列中的节点会自旋检查前驱节点状态,当前驱节点释放锁时,后续节点会被唤醒。这种设计减少了线程间的竞争,提高了吞吐量。
3. 公平锁与非公平锁的深度对比
3.1 非公平锁的性能优势
默认情况下,ReentrantLock使用非公平策略。新来的线程会先尝试直接获取锁,而不考虑队列中是否有等待线程。这种"插队"行为看似不公平,但能显著减少线程切换的开销。
java复制// 非公平锁的获取逻辑
final void lock() {
if (compareAndSetState(0, 1)) // 先尝试直接获取
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 失败后再走正常流程
}
实测数据显示,在100个线程竞争的场景下,非公平锁的吞吐量比公平锁高出约40%。但这也可能导致某些线程长时间获取不到锁(线程饥饿)。
3.2 公平锁的适用场景
公平锁严格按照FIFO顺序分配锁,适合以下场景:
- 任务执行时间差异大,需要避免长任务垄断锁
- 对延迟敏感,需要保证响应时间的可预测性
- 防止低优先级线程被永久阻塞
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;
}
}
// ...重入逻辑与非公平锁相同
}
生产建议:除非有明确需求,否则优先使用非公平锁。在监控系统中需要特别关注线程等待时间指标。
4. 条件变量(Condition)的高级用法
4.1 生产者-消费者模型的正确实现
Condition接口提供了比Object.wait()/notify()更灵活的线程通信机制。每个Condition对象都维护一个独立的等待队列。
java复制class BoundedBuffer {
final Lock lock = new ReentrantLock();
final Condition notFull = lock.newCondition();
final Condition notEmpty = lock.newCondition();
void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length)
notFull.await(); // 等待不满条件
items[putPtr] = x;
if (++putPtr == items.length) putPtr = 0;
++count;
notEmpty.signal(); // 通知不空条件
} finally {
lock.unlock();
}
}
// take方法类似...
}
4.2 条件谓词的重要性
使用Condition时必须检查条件谓词(condition predicate),即在await()返回后重新检查条件是否满足。这是因为:
- 虚假唤醒(spurious wakeup)可能发生
- 其他线程可能在我们被唤醒前修改了状态
java复制while (!conditionPredicate()) {
condition.await();
}
5. 锁的调试与性能优化
5.1 死锁检测与预防
ReentrantLock提供了tryLock()方法,可以避免死锁:
java复制if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// 临界区
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
JDK自带的工具也能帮助诊断:
- jstack查看线程堆栈
- JConsole监控锁竞争情况
5.2 锁分段技术
对于高度竞争的场景,可以考虑锁分段(Lock Striping):
java复制class StripedMap {
private static final int N_LOCKS = 16;
private final Node[] buckets;
private final Object[] locks;
public Object get(Object key) {
int hash = hash(key);
synchronized (locks[hash % N_LOCKS]) {
for (Node m = buckets[hash]; m != null; m = m.next)
if (m.key.equals(key)) return m.value;
}
return null;
}
// ...
}
这种技术被ConcurrentHashMap采用,将数据分成多个段,每个段由独立的锁保护。
6. ReentrantLock与synchronized的抉择
6.1 功能对比
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 可重入性 | 支持 | 支持 |
| 公平性选择 | 支持 | 不支持 |
| 超时获取 | tryLock()支持 | 不支持 |
| 中断响应 | lockInterruptibly() | 不支持 |
| 条件变量 | 多Condition支持 | 单一wait/notify |
| 性能 | JDK6+优化后相当 | 更轻量 |
6.2 选型建议
选择synchronized当:
- 简单的同步需求
- 不需要高级功能
- 代码简洁性更重要
选择ReentrantLock当:
- 需要尝试获取锁、定时获取
- 需要公平性保证
- 需要更细粒度的条件等待
- 需要可中断的锁获取
7. 常见陷阱与最佳实践
7.1 必须使用try-finally
锁必须在finally块中释放,确保异常时也能释放:
java复制lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 绝对保证执行
}
7.2 避免锁泄漏
确保锁的释放次数与获取次数匹配。我曾见过这样的bug:
java复制public void process() {
lock.lock();
if (condition) {
return; // 直接返回导致锁泄漏!
}
lock.unlock();
}
7.3 监控锁竞争
通过ThreadMXBean可以监控锁信息:
java复制ThreadMXBean bean = ManagementFactory.getThreadMXBean();
long[] threadIds = bean.findDeadlockedThreads();
if (threadIds != null) {
ThreadInfo[] infos = bean.getThreadInfo(threadIds);
for (ThreadInfo info : infos) {
System.out.println(info.getLockName());
}
}
在实际项目中,建议将锁等待时间纳入监控系统,当平均等待时间超过阈值时触发告警。