1. 从"抢座位"看ReentrantLock的直观理解
想象一下图书馆自习室的场景:当座位资源紧张时,学生们会自发形成排队机制。第一个到达的人会占据座位,后续到达的人会在登记表上依次签名排队。这就是ReentrantLock最朴素的工作原理——通过队列管理对共享资源的访问请求。
在Java并发编程中,ReentrantLock提供了比synchronized更灵活的锁机制。其核心特性包括:
- 可重入性:已经持有锁的线程可以重复获取同一把锁(类似学生可以多次进出已占座位)
- 公平性选择:支持先到先得的公平模式和非公平的竞争模式
- 条件变量:可通过Condition实现精细化的线程等待/唤醒机制
关键区别:synchronized是JVM内置的互斥原语,而ReentrantLock是用Java代码实现的锁工具类,这种实现方式使其具备更好的可扩展性。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 锁的获取:非公平与公平模式对比
2.1 非公平锁的"插队"机制
在非公平模式下(默认模式),新来的线程可以直接尝试获取锁,而不必立即进入等待队列。这种设计源于一个重要的性能优化考量:减少线程挂起和唤醒的开销。
java复制// ReentrantLock非公平锁获取源码片段
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;
}
这段代码揭示了两个关键点:
- 通过CAS(Compare-And-Swap)操作尝试直接获取锁
- 重入计数通过state变量维护(每次重入state+1)
2.2 公平锁的严格排队
公平模式下,hasQueuedPredecessors()方法会检查队列中是否有更早的等待者:
java复制public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
这种检查确保了绝对的先来后到顺序,但也带来了额外的性能开销。在实际应用中,除非有严格的顺序要求,否则非公平锁通常能提供更高的吞吐量。
3. AQS队列的底层架构解析
AbstractQueuedSynchronizer(AQS)是ReentrantLock的核心实现基础,其队列管理采用CLH锁的变体。这个等待队列有以下几个关键特性:
3.1 节点状态与队列结构
java复制static final class Node {
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
等待队列中的每个节点包含:
- waitStatus:表示节点状态(CANCELLED、SIGNAL等)
- 前驱/后继指针:构成双向链表
- thread:关联的线程引用
- nextWaiter:用于条件队列的特殊链接
3.2 队列操作的核心方法
- enq() - 节点入队:
java复制private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // 必须初始化
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
- shouldParkAfterFailedAcquire() - 检查是否需要挂起:
java复制private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
4. 锁的释放与线程唤醒机制
4.1 unlock()的完整流程
java复制public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease()方法会递减持有计数,当计数归零时完全释放锁:
java复制protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
4.2 唤醒后继节点的关键逻辑
unparkSuccessor()负责找到最合适的唤醒候选者:
java复制private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
这个实现有两个值得注意的细节:
- 从后向前遍历的可靠性更高(因为next指针可能因并发操作而不准确)
- 跳过已取消的节点(waitStatus > 0)
5. 条件变量的实现原理
ConditionObject是AQS的内部类,实现了Condition接口,为锁提供了更灵活的等待/通知机制。
5.1 条件队列与同步队列的关系
java复制public class ConditionObject implements Condition {
private transient Node firstWaiter;
private transient Node lastWaiter;
// ...
}
条件队列是单向链表,与同步队列(CLH队列)独立但可以相互转换。当调用await()时,线程会从同步队列转移到条件队列;调用signal()时,又会从条件队列移回同步队列。
5.2 await()的完整流程
java复制public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
这个实现展示了几个关键操作:
- 创建新的条件节点并加入条件队列
- 完全释放持有的锁(考虑重入情况)
- 挂起线程直到被signal或中断
- 重新获取锁后处理中断状态
6. 性能优化与实战建议
6.1 锁粒度的选择
在实际项目中,需要根据场景特点选择合适的锁策略:
| 场景特征 | 推荐策略 | 原因 |
|---|---|---|
| 竞争激烈且短暂 | 非公平锁 | 减少线程切换开销 |
| 需要严格顺序 | 公平锁 | 保证先来先服务 |
| 读多写少 | ReadWriteLock | 提高并发度 |
| 超高并发 | StampedLock | 乐观读优化 |
6.2 常见问题排查技巧
- 死锁检测:
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.getThreadName() +
" waiting on " + info.getLockName());
}
}
- 锁争用诊断:
- 使用JConsole或VisualVM监控锁的等待情况
- 通过Thread dump分析阻塞线程栈
6.3 最佳实践
- 总是使用try-finally确保锁释放:
java复制ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
-
避免在持有锁时调用外部方法(可能引发死锁或性能问题)
-
对于复杂同步需求,考虑使用更高级的并发工具:
- CountDownLatch:一次性屏障
- CyclicBarrier:可重复使用的屏障
- Phaser:更灵活的阶段同步器
在笔者参与的一个高频交易系统中,我们通过将非公平ReentrantLock与细粒度锁分段结合,将订单处理吞吐量提升了3倍。关键点在于:分析热点数据分布,确保锁只保护真正需要同步的最小代码块,同时避免过细的锁粒度导致的锁开销增加。
