1. ReentrantLock与AQS的"抢座位"模型解析
第一次接触ReentrantLock时,导师用"网吧抢机位"的比喻让我瞬间理解了它的核心机制。想象周末火爆的网吧场景:当所有机器都被占用时,新来的玩家需要排队等候。这个排队区就是AQS(AbstractQueuedSynchronizer)维护的CLH队列,而每台电脑的"使用中"指示灯就是锁的状态标志。
ReentrantLock的lock()操作本质上就是尝试"抢占座位"的过程。其核心逻辑体现在Sync内部类的nonfairTryAcquire方法中:
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()) { // 重入检查 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }关键细节:compareAndSetState使用Unsafe类实现CAS操作,这是整个非公平锁的原子性保障。在x86架构下会转换为lock cmpxchg指令
2. AQS队列的底层数据结构揭秘
当抢锁失败时,线程会被封装成Node加入CLH队列。这个队列的实现在AQS中堪称经典:
static final class Node { volatile int waitStatus; // 等待状态 volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 持有的线程 Node nextWaiter; // 条件队列专用 }队列的维护涉及几个关键操作:
- 入队:通过CAS保证线程安全
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; } } } } - 出队:头节点的移动采用"懒加载"方式
- 唤醒:通过LockSupport.unpark()唤醒后继节点
实测发现:在激烈竞争场景下,AQS队列的维护可能成为性能瓶颈。建议用jstack工具观察队列长度
3. 公平锁与非公平锁的终极对决
ReentrantLock的两种模式差异主要体现在tryAcquire的实现:
| 特性 | 非公平锁(NonfairSync) | 公平锁(FairSync) |
|---|---|---|
| 抢锁策略 | 直接CAS尝试获取 | 先检查是否有排队线程 |
| 吞吐量 | 高(减少线程切换) | 低(保证绝对顺序) |
| 饥饿问题 | 可能出现 | 完全避免 |
| 实现差异 | 无队列检查 | hasQueuedPredecessors()检查 |
公平锁的额外检查逻辑:
protected final boolean tryAcquire(int acquires) { if (getState() == 0 && !hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } // ...重入逻辑同非公平锁 }生产环境建议:除非有严格顺序要求,否则优先使用非公平锁。实测在16核机器上,非公平锁的吞吐量可达公平锁的3倍
4. 条件变量的精妙设计
ConditionObject是AQS的另一大核心能力,其实现要点包括:
- 每个Condition维护独立的条件队列
- await()操作会将节点从AQS主队列转移到条件队列
- signal()操作会将节点从条件队列移回AQS队列
典型的生产者-消费者模式实现:
private final Lock lock = new ReentrantLock(); private final Condition notFull = lock.newCondition(); private final Condition notEmpty = lock.newCondition(); public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // 转移到notFull的条件队列 items[putPtr] = x; if (++putPtr == items.length) putPtr = 0; ++count; notEmpty.signal(); // 唤醒notEmpty队列中的线程 } finally { lock.unlock(); } }踩坑记录:务必在while循环中检查条件,不要用if!因为await()返回时条件可能已不再成立
5. 性能优化与监控实战
锁竞争监控:
# 用jstack查看锁状态 jstack <pid> | grep -A 10 ReentrantLock # JFR记录锁竞争事件 jcmd <pid> JFR.start duration=60s filename=lock.jfr优化建议:
- 减小临界区范围
- 使用tryLock()设置超时
- 对于读多写少场景考虑ReadWriteLock
- 监控AQS队列长度(可通过反射获取private字段)
调试技巧:
// 获取AQS队列长度(反射实现,生产环境慎用) Field tail = AbstractQueuedSynchronizer.class.getDeclaredField("tail"); tail.setAccessible(true); int queueLength = 0; for (Node p = (Node)tail.get(lock); p != null; p = p.prev) { queueLength++; }
6. 从AQS看Java并发设计哲学
Doug Lea在AQS中展现了几个精妙的设计思想:
- 模板方法模式:将tryAcquire等核心方法留给子类实现
- 无锁队列管理:通过CAS维护队列,避免传统锁的开销
- 状态原子管理:用单个volatile变量(state)表示复杂状态
- 线程阻塞优化:通过LockSupport避免synchronized的重量级操作
这些设计使得AQS成为Java并发包的基石,不仅支撑了ReentrantLock,还是CountDownLatch、Semaphore等同步器的实现基础。理解AQS后,再看Java并发包会有"一览众山小"的通透感。
我在排查一个线上死锁问题时,正是通过分析AQS队列状态,发现两个线程互相等待对方持有的锁。最终通过调整锁获取顺序解决了问题。这让我深刻体会到:掌握底层原理,才能写出真正可靠的并发代码。