🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞

💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论

🔥🔥🔥(源码获取 + 调试运行 + 问题答疑)🔥🔥🔥  有兴趣可以联系我

🔥🔥🔥  文末有往期免费源码,直接领取获取(无删减,无套路)

我们常常在当下感到时间慢,觉得未来遥远,但一旦回头看,时间已经悄然流逝。对于未来,尽管如此,也应该保持一种从容的态度,相信未来仍有许多可能性等待着我们。

《AQS acquire方法深度解析:从锁获取到线程阻塞的完整流程》

《逐行解读AQS acquire源码:tryAcquire、入队、自旋与中断处理的精妙设计》

《AQS acquire方法四步曲:掌握Java并发锁获取的核心逻辑》


正文

一、acquire方法:独占锁获取的入口

在AQS(AbstractQueuedSynchronizer)的架构中,acquire(int arg)方法是独占式获取资源的顶层入口。这个方法虽然代码量不多,但凝聚了AQS设计的核心思想,是整个同步器框架的精华所在。

acquire方法采用模板方法模式,定义了获取资源的整体流程,而将具体的资源获取逻辑交由子类实现。这种设计既保证了流程的一致性,又提供了足够的灵活性。

二、acquire方法源码全景

让我们首先从宏观角度理解acquire方法的整体结构:

 public final void acquire(int arg) {
     if (!tryAcquire(arg) &&
         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
         selfInterrupt();
 }

这段简洁的代码包含了四个关键步骤,通过逻辑运算符的短路特性巧妙地连接在一起。整个方法体现了"快速路径优先,慢速路径兜底"的设计哲学。

三、第一步:tryAcquire - 快速获取尝试

tryAcquire(arg)是acquire流程的第一个环节,也是子类必须实现的核心方法:

3.1 方法签名与职责
 protected boolean tryAcquire(int arg) {
     throw new UnsupportedOperationException();
 }

tryAcquire的职责是根据具体的同步语义尝试获取资源。在ReentrantLock中,它需要处理:

  • 锁的初始获取(state从0到1)

  • 锁的重入(当前线程已持有锁)

  • 公平性检查(是否有前驱节点在等待)

3.2 快速路径的优势

tryAcquire放在第一步体现了重要的性能优化思想:

  • 无竞争场景:大多数情况下锁是可用的,直接获取成功

  • 避免入队开销:不需要创建节点、操作队列

  • 减少上下文切换:线程不会被阻塞

统计表明,在大多数应用中,超过90%的锁获取操作在tryAcquire阶段就成功了,这证明了快速路径设计的价值。

四、第二步:addWaiter - 线程包装与入队

tryAcquire失败后,线程需要进入等待队列。addWaiter(Node.EXCLUSIVE)负责这个关键转换:

4.1 节点创建与模式
 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;
 }

节点模式的含义:

  • Node.EXCLUSIVE:独占模式,用于ReentrantLock等

  • Node.SHARED:共享模式,用于Semaphore、CountDownLatch等

4.2 入队算法的精妙设计

addWaiter采用了"快速路径+完整路径"的双重策略:

快速路径:当队列已初始化时,直接CAS尝试设置尾节点 完整路径:通过enq方法处理队列初始化或CAS竞争失败

这种设计避免了在无竞争情况下执行完整的入队逻辑,提升了性能。

4.3 enq方法的可靠性保证
 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;
             }
         }
     }
 }

enq方法通过无限循环确保节点最终成功入队,这种"乐观重试"策略是AQS高可靠性的基础。

五、第三步:acquireQueued - 队列中的等待艺术

这是acquire方法中最复杂、最精妙的部分,线程在队列中等待并适时尝试获取资源:

5.1 方法结构与核心循环
 final boolean acquireQueued(final Node node, int arg) {
     boolean failed = true;
     try {
         boolean interrupted = false;
         for (;;) {
             final Node p = node.predecessor();
             if (p == head && tryAcquire(arg)) {
                 setHead(node);
                 p.next = null; // help GC
                 failed = false;
                 return interrupted;
             }
             if (shouldParkAfterFailedAcquire(p, node) &&
                 parkAndCheckInterrupt())
                 interrupted = true;
         }
     } finally {
         if (failed)
             cancelAcquire(node);
     }
 }
5.2 关键问题:为什么阻塞前要再次尝试获取?

这是acquireQueued设计中最值得深思的问题。在调用parkAndCheckInterrupt()进入阻塞之前,线程会执行:

  1. 检查前驱节点是否为头节点(p == head

  2. 如果是,再次尝试获取资源(tryAcquire(arg)

这样设计的好处:

1. 减少不必要的阻塞 如果前驱节点刚好释放锁,当前线程可以直接获取,避免线程切换的开销。考虑这样的时序:

  • 线程A(头节点)释放锁

  • 线程B(第二个节点)正准备阻塞

  • 线程B检查发现自己是头节点的后继,尝试获取成功 避免了线程B的阻塞和后续的唤醒操作。

2. 利用时间局部性 锁的持有时间通常很短,前驱节点释放后,锁有很大概率仍然可用。

3. 提升吞吐量 减少了线程状态切换的次数,整体系统吞吐量得到提升。

4. 公平性保证 只有头节点的后继才有资格尝试获取,严格维护了FIFO顺序。

5.3 shouldParkAfterFailedAcquire - 状态管理智慧

这个方法负责设置前驱节点的状态,确保当前线程在适当的时候被唤醒:

 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;
 }

状态处理的三种情况:

  • SIGNAL(-1):前驱节点会通知当前节点,可以安全阻塞

  • CANCELLED(>0):跳过已取消的节点,重新链接队列

  • 0或PROPAGATE:CAS设置前驱为SIGNAL状态

5.4 parkAndCheckInterrupt - 阻塞与中断处理
 private final boolean parkAndCheckInterrupt() {
     LockSupport.park(this);
     return Thread.interrupted();
 }

这里的设计也很精妙:

  • 使用Thread.interrupted()检查并清除中断状态

  • 返回中断状态,但不立即响应

  • 在acquire方法最后统一处理中断

六、第四步:selfInterrupt - 中断的延迟处理

 static void selfInterrupt() {
     Thread.currentThread().interrupt();
 }

延迟中断处理的设计考量:

  1. 保持逻辑清晰:锁获取逻辑与中断处理分离

  2. 性能优化:在非中断路径中避免不必要的操作

  3. 语义正确:最终恢复中断状态,不丢失中断信息

七、acquire方法的异常处理与资源清理

acquireQueued方法的finally块体现了AQS的健壮性设计:

finally {
    if (failed)
        cancelAcquire(node);
}

cancelAcquire负责:

  • 清理失败节点的状态

  • 维护队列的完整性

  • 适时唤醒后继节点

这种设计确保了即使在异常情况下,队列状态也不会被破坏。

八、性能优化深度分析

8.1 自旋次数的权衡

acquireQueued中,线程在阻塞前会进行有限次数的尝试。这个设计权衡了两种开销:

  • 自旋开销:CPU周期浪费在忙等待上

  • 阻塞开销:线程切换和调度延迟

通过适度的自旋,在多数情况下找到了最佳平衡点。

8.2 内存屏障的合理使用

在整个acquire流程中,AQS恰当地使用了内存屏障:

  • volatile读写保证状态的可见性

  • CAS操作隐含的内存屏障效果

  • LockSupport.park提供的屏障保证

九、实战启示与最佳实践

理解acquire方法的源码,为我们提供了重要的实践指导:

9.1 自定义同步器实现

实现tryAcquire时应注意:

  • 正确使用CAS操作

  • 合理处理重入逻辑

  • 考虑公平性需求

9.2 性能调优方向
  • 减少锁竞争:缩小临界区范围

  • 选择合适的锁策略:读写锁、分段锁等

  • 监控队列长度:及时发现性能瓶颈

9.3 问题诊断技巧
  • 线程长时间阻塞:检查锁竞争情况

  • CAS失败频繁:考虑锁分解

  • 队列过长:优化锁粒度

十、总结:精妙设计的启示

AQS的acquire方法体现了多个重要的软件设计原则:

  1. 分离变与不变:模板方法固定流程,子类实现变化逻辑

  2. 快速路径优化:优先处理无竞争场景

  3. 渐进式复杂度:从简单尝试到复杂队列管理

  4. 资源清理保障:异常情况下的状态维护

通过深入分析acquire方法的每个细节,我们不仅理解了AQS的工作原理,更学到了系统设计的智慧。这种"简单接口背后隐藏复杂实现"的设计哲学,正是构建高质量软件系统的关键。



「在线考试系统源码(含搭建教程)」 (无删减,无套路):🔥🔥🔥  

链接:https://pan.quark.cn/s/96c4f00fdb43 提取码:WR6M


往期免费源码对应视频:

免费获取--SpringBoot+Vue宠物商城网站系统

🥂(❁´◡`❁)您的点赞👍➕评论📝➕收藏⭐➕关注👀是作者创作的最大动力🤞

💖📕🎉🔥 支持我:点赞👍+收藏⭐️+留言📝+关注👀欢迎留言讨论

🔥🔥🔥(源码 + 调试运行 + 问题答疑)

🔥🔥🔥  有兴趣可以联系我

💖学习知识需费心,
📕整理归纳更费神。
🎉源码免费人人喜,
🔥码农福利等你领!

💖常来我家多看看,
📕网址:扣棣编程
🎉感谢支持常陪伴,
🔥点赞关注别忘记!

💖山高路远坑又深,
📕大军纵横任驰奔,
🎉谁敢横刀立马行?
🔥唯有点赞+关注成!

⬇⬇⬇⬇⬇⬇⬇⬇⬇⬇点击此处获取源码⬇⬇⬇⬇⬇⬇⬇⬇⬇

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐