
1. ReentrantLock与AQS的关系解析第一次接触Java并发编程时很多人都会对ReentrantLock这个加强版synchronized产生好奇。但真正让我理解其精髓的是发现它背后站着一位隐形裁判——AbstractQueuedSynchronizerAQS。这个隐藏在JDK并发包中的框架才是支撑ReentrantLock强大功能的幕后英雄。AQS本质上是一个用于构建锁和同步器的脚手架。它通过一个int成员变量表示同步状态配合内置的FIFO队列管理竞争线程。当我在调试ReentrantLock的lock()方法时发现它所有的加锁操作最终都委托给了内部Sync类AQS的子类。这种设计就像去餐厅吃饭我们直接跟服务员(ReentrantLock)打交道但真正的菜品调度是由后厨(AQS)完成的。关键认知ReentrantLock只是AQS的一个皮肤所有核心逻辑都在Sync这个内部类中实现。公平锁与非公平锁的区别也仅在于tryAcquire的实现方式不同。2. 从抢座位看锁竞争的本质想象一下图书馆自习室的场景当座位充足时同学们可以直接入座相当于无竞争获取锁但当座位紧张时晚到的同学需要在登记表上排队同步队列。这就是ReentrantLock工作的真实写照。在非公平锁模式下默认实现新来的线程会直接尝试插队获取锁final void lock() { if (compareAndSetState(0, 1)) // 直接尝试CAS抢锁 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); // 抢锁失败进入AQS队列 }这种设计虽然可能造成后来者居上但大幅减少了线程切换的开销。我在压测中发现非公平锁在高并发场景下的吞吐量比公平锁高出30%以上。不过对于需要严格顺序的业务场景就需要使用公平锁protected final boolean tryAcquire(int acquires) { // 检查队列中是否有等待线程 if (hasQueuedPredecessors()) return false; // ...后续获取逻辑 }3. AQS队列的精密运作机制当线程获取锁失败时AQS会将线程包装成Node节点加入CLH队列。这个队列的实现充满精妙设计节点状态waitStatus不仅表示线程状态还承担着通信作用。比如SIGNAL(-1)表示后继节点需要被唤醒CANCELLED(1)表示线程已放弃等待。自旋入队在enq()方法中通过无限循环CAS保证节点正确插入队尾。我在测试中故意制造竞争观察到有时需要旋转5-6次才能成功入队。头节点设计队列的头节点是虚节点它代表的线程已经获取到锁。这个设计简化了很多边界条件的处理。// 典型的获取锁失败处理流程 private Node addWaiter(Node mode) { Node node new Node(Thread.currentThread(), mode); Node pred tail; if (pred ! null) { node.prev pred; if (compareAndSetTail(pred, node)) { pred.next node; return node; } } enq(node); // 快速入队失败时走完整入队流程 return node; }4. 条件变量的实现奥秘除了基本的锁功能ReentrantLock的条件等待机制也值得研究。每个ConditionObject都维护着自己的等待队列与AQS主队列形成互动await()时线程会释放锁并进入条件队列signal()时将条件队列的头节点转移到AQS主队列只有回到AQS队列后线程才有机会重新竞争锁。这种设计完美解决了等待通知机制中的丢失唤醒问题。我在实现一个有界队列时就利用这个特性实现了精确的队列非满和队列非空通知public 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(); // 释放锁并进入等待 // ...入队操作 notEmpty.signal(); } finally { lock.unlock(); } } }5. 性能优化的关键细节在阅读源码过程中我发现了几个影响性能的关键点自旋优化在shouldParkAfterFailedAcquire()方法中线程在挂起前会短暂自旋这能减少上下文切换。但在虚拟化环境中这个自旋时间可能需要调整。取消节点清理取消竞争的线程会将自己的waitStatus改为CANCELLED后续线程会帮忙清理这些僵尸节点。这解释了为什么我们有时会看到prev指针的跳跃。头节点竞争释放锁时新老头节点的竞争可能导致不必要的唤醒。这也是为什么unparkSuccessor()方法要从尾部逆向查找有效节点。// 释放锁后唤醒后继节点的典型流程 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); }6. 实战中的踩坑记录在真实项目中使用ReentrantLock时有几个容易忽略的问题锁泄漏忘记在finally块中释放锁是常见错误。我曾遇到过一个生产事故因为异常路径没有unlock()导致系统逐渐失去响应。条件误用不同Condition对象有独立的等待队列。有次我错误地混用notify()和signal()导致线程永远挂起。死锁检测JDK没有内置的死锁检测机制。后来我通过ThreadMXBean.findDeadlockedThreads()实现了监控ThreadMXBean bean ManagementFactory.getThreadMXBean(); long[] threadIds bean.findDeadlockedThreads(); if (threadIds ! null) { // 处理死锁情况 }性能陷阱在锁粒度控制不当的情况下使用ReentrantLock可能比synchronized更慢。通过JProfiler分析后我发现是因为锁范围过大导致竞争加剧。7. 从AQS看并发设计哲学深入AQS源码后我总结了几个并发设计原则状态最小化AQS仅用32位的state变量就管理了所有同步状态这种极简设计减少了内存争用。乐观重试几乎所有的状态变更都采用CAS自旋的方式这比直接挂起线程更高效。责任分离AQS处理队列管理子类只需实现tryAcquire等模板方法。这种设计让ReadWriteLock、CountDownLatch等都能复用AQS。失败快速在非公平锁实现中新请求会立即尝试获取锁而不是直接排队。这种设计契合大多数实际场景的竞争特点。在自研分布式锁时我就借鉴了这些思想。比如使用Redis的WATCH/MULTI实现乐观锁用Zookeeper的临时节点模拟AQS队列取得了不错的效果。