AQS-AbstractQueuedSynchronizer源码解析(二)(中)

本文涉及的产品
云解析 DNS,旗舰版 1个月
全局流量管理 GTM,标准版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: AQS-AbstractQueuedSynchronizer源码解析(二)

parkAndCheckInterrupt


  • 将当前线程挂起,阻塞调用栈并返回当前线程的中断状态
private final boolean parkAndCheckInterrupt() {
  // 进入休息区 unpark就是从休息区唤醒
    LockSupport.park(this);
    return Thread.interrupted();
}

一图小结该方法流程


image.png


从上图可以看出,跳出当前循环的条件是当“前驱节点是头结点,且当前线程获取锁成功”。



6.1.3 cancelAcquire

shouldParkAfterFailedAcquire中取消节点是怎么生成的呢?什么时候会把一个节点的waitStatus设置为-1?又是在什么时间释放节点通知到被挂起的线程呢?


image.png


    private void cancelAcquire(Node node) {
        // 如果节点不存在,无视该方法
        if (node == null)
            return;
    // 设置该节点不关联任何线程,即虚节点
        node.thread = null;
        // 跳过被取消的前驱结点们
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
        // predNext 是要取消拼接的明显节点。如果没有,以下情况 CAS 将失败,在这种情况下,我们输掉了和另一个cancel或signal的竞争,因此无需采取进一步措施。
        // 通过前驱节点,跳过取消状态的node
        Node predNext = pred.next;
        // 这里可以使用无条件写代替CAS,把当前node的状态设置为CANCELLED
        // 在这个原子步骤之后,其他节点可以跳过我们。
        // 在此之前,我们不受其他线程的干扰。
        node.waitStatus = Node.CANCELLED;
        // 如果是 tail 节点, 移除自身
        // 如果当前节点是尾节点,将从后往前的第一个非取消状态的节点设置为尾节点
  // 更新失败的话,则进入else,如果更新成功,将tail的后继节点设置为null
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }
            node.next = node; // 辅助 GC
        }
    }



当前的流程:


获取当前节点的前驱节点,如果前驱节点的状态是CANCELLED,那就一直往前遍历,找到第一个waitStatus <= 0的节点,将找到的Pred节点和当前Node关联,将当前Node设置为CANCELLED。

根据当前节点的位置,考虑以下三种情况:

(1) 当前节点是尾节点。

(2) 当前节点是Head的后继节点。

(3) 当前节点不是Head的后继节点,也不是尾节点。



根据(2),来分析每一种情况的流程。

  • 当前节点是尾节点


image.png


当前节点是Head的后继节点


image.png


当前节点不是Head的后继节点,也不是尾节点


image.png


通过上面的流程,我们对于CANCELLED节点状态的产生和变化已经有了大致的了解,但是为什么所有的变化都是对Next指针进行了操作,而没有对Prev指针进行操作呢?什么情况下会对Prev指针进行操作?



执行 cancelAcquire 时,当前节点的前驱节点可能已经出队(已经执行过try代码块中的shouldParkAfterFailedAcquire),如果此时修改 prev 指针,有可能会导致 prev 指向另一个已经出队的 Node,因此这块变化 prev 指针不安全。



6.2 tryAcquireNanos

尝试以独占模式获取,如果中断将中止,如果超过给定超时将直接失败。首先检查中断状态,然后至少调用一次#tryAcquire,成功后返回。否则,线程将排队,可能会反复地阻塞和取消阻塞,调用#tryAcquire,直到成功或线程中断或超时结束。此方法可用于实现方法 Lock#tryLock(long, TimeUnit)。


image.png



尝试性的获取锁, 获取锁不成功, 直接加入到同步队列,加入操作即在doAcquireNanos

doAcquireNanos

以独占限时模式获取。


private boolean doAcquireNanos(int arg, long nanosTimeout)
        throws InterruptedException {
    if (nanosTimeout <= 0L)
        return false;
    // 截止时间    
    final long deadline = System.nanoTime() + nanosTimeout;
    // 将当前的线程封装成 Node 加入到同步对列里面
    final Node node = addWaiter(Node.EXCLUSIVE);
    boolean failed = true;
    try {
        for (;;) {
          // 获取当前节点的前驱节点(当一个n在同步对列里, 并且没有获取
          // lock 的 node 的前驱节点不可能是 null)
            final Node p = node.predecessor();
            // 判断前驱节点是否为 head
            // 前驱节点是 head, 存在两种情况 
            //  (1) 前驱节点现在持有锁 
            //  (2) 前驱节点为 null, 已经释放锁, node 现在可以获取锁
            // 则再调用 tryAcquire 尝试获取
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null; // 辅助GC
                failed = false;
                return true;
            }
            // 计算剩余时间
            nanosTimeout = deadline - System.nanoTime();
            // 超时,直接返回 false
            if (nanosTimeout <= 0L)
                return false;
            // 调用 shouldParkAfterFailedAcquire 判断是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
              // 若未超时, 并且大于 spinForTimeoutThreshold, 则将线程挂起
                nanosTimeout > spinForTimeoutThreshold)
                LockSupport.parkNanos(this, nanosTimeout);
            if (Thread.interrupted())
                throw new InterruptedException();
        }
    } finally {
      // 在整个获取中出错(中断/超时等),则清除该节点
        if (failed)
            cancelAcquire(node);
    }
}

6.3 acquireSharedInterruptibly

  • 以共享模式获取,如果中断将中止。


image.png



首先检查中断状态,然后至少调用一次 tryAcquireShared(int),成功后返回。否则,线程将排队,可能会反复阻塞和取消阻塞,调用 tryAcquireShared(int),直到成功或线程被中断。

arg 参数,这个值被传递给 tryAcquireShared(int),但未被解释,可以代表你喜欢的任何东西。如果当前线程被中断,则抛 InterruptedException。


doAcquireSharedInterruptibly

共享可中断模式的获取锁

private void doAcquireSharedInterruptibly(int arg)
    throws InterruptedException {
    // 创建"当前线程"的 Node 节点,且其中记录的共享锁
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        for (;;) {
          // 获取前驱节点
            final Node p = node.predecessor();
            // 如果前驱节点是头节点
            if (p == head) {
              // 尝试获取锁(由于前驱节点为头节点,所以可能此时前驱节点已经成功获取了锁,所以尝试获取一下)
                int r = tryAcquireShared(arg);
                // 获取锁成功
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // 辅助 GC
                    failed = false;
                    return;
                }
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}



7 锁的释放

7.1 release


以独占模式释放。 如果 tryRelease 返回true,则通过解锁一个或多个线程来实现。此方法可用于实现方法 Lock#unlock

arg 参数将传送到 tryRelease,并且可以表示你自己喜欢的任何内容。



自定义实现的 tryRelease 如果返回 true,说明该锁没有被任何线程持有


image.png



头结点不为空并且头结点的waitStatus不是初始化节点情况,解除线程挂起状态


image.png



h == null

Head还没初始化。初始时 head == null,第一个节点入队,Head会被初始化一个虚节点。所以说,这里如果还没来得及入队,就会出现head == null


h != null && waitStatus == 0

后继节点对应的线程仍在运行中,不需要唤醒


h != null && waitStatus < 0

后继节点可能被阻塞了,需要唤醒


目录
相关文章
|
2月前
|
监控 网络协议 Java
Tomcat源码解析】整体架构组成及核心组件
Tomcat,原名Catalina,是一款优雅轻盈的Web服务器,自4.x版本起扩展了JSP、EL等功能,超越了单纯的Servlet容器范畴。Servlet是Sun公司为Java编程Web应用制定的规范,Tomcat作为Servlet容器,负责构建Request与Response对象,并执行业务逻辑。
Tomcat源码解析】整体架构组成及核心组件
|
26天前
|
存储 缓存 Java
什么是线程池?从底层源码入手,深度解析线程池的工作原理
本文从底层源码入手,深度解析ThreadPoolExecutor底层源码,包括其核心字段、内部类和重要方法,另外对Executors工具类下的四种自带线程池源码进行解释。 阅读本文后,可以对线程池的工作原理、七大参数、生命周期、拒绝策略等内容拥有更深入的认识。
什么是线程池?从底层源码入手,深度解析线程池的工作原理
|
1月前
|
开发工具
Flutter-AnimatedWidget组件源码解析
Flutter-AnimatedWidget组件源码解析
148 60
|
26天前
|
设计模式 Java 关系型数据库
【Java笔记+踩坑汇总】Java基础+JavaWeb+SSM+SpringBoot+SpringCloud+瑞吉外卖/谷粒商城/学成在线+设计模式+面试题汇总+性能调优/架构设计+源码解析
本文是“Java学习路线”专栏的导航文章,目标是为Java初学者和初中高级工程师提供一套完整的Java学习路线。
222 37
|
5天前
|
Java Spring 容器
Spring IOC、AOP与事务管理底层原理及源码解析
Spring框架以其强大的控制反转(IOC)和面向切面编程(AOP)功能,成为Java企业级开发中的首选框架。本文将深入探讨Spring IOC和AOP的底层原理,并通过源码解析来揭示其实现机制。同时,我们还将探讨Spring事务管理的核心原理,并给出相应的源码示例。
37 9
|
18天前
|
编解码 开发工具 UED
QT Widgets模块源码解析与实践
【9月更文挑战第20天】Qt Widgets 模块是 Qt 开发中至关重要的部分,提供了丰富的 GUI 组件,如按钮、文本框等,并支持布局管理、事件处理和窗口管理。这些组件基于信号与槽机制,实现灵活交互。通过对源码的解析及实践应用,可深入了解其类结构、布局管理和事件处理机制,掌握创建复杂 UI 界面的方法,提升开发效率和用户体验。
91 12
|
2月前
|
测试技术 Python
python自动化测试中装饰器@ddt与@data源码深入解析
综上所述,使用 `@ddt`和 `@data`可以大大简化写作测试用例的过程,让我们能专注于测试逻辑的本身,而无需编写重复的测试方法。通过讲解了 `@ddt`和 `@data`源码的关键部分,我们可以更深入地理解其背后的工作原理。
36 1
|
2月前
|
存储 NoSQL Redis
redis 6源码解析之 object
redis 6源码解析之 object
60 6
|
2月前
|
开发者 Python
深入解析Python `httpx`源码,探索现代HTTP客户端的秘密!
深入解析Python `httpx`源码,探索现代HTTP客户端的秘密!
74 1
|
2月前
|
开发者 Python
深入解析Python `requests`库源码,揭开HTTP请求的神秘面纱!
深入解析Python `requests`库源码,揭开HTTP请求的神秘面纱!
134 1

热门文章

最新文章

推荐镜像

更多