AQS核心流程解析cancelAcquire方法

目录
  • 引出问题
  • 更新正常节点的链表
    • 当前取消节点是tail节点的情况
    • 当前取消节点是非tail节点的情况

引出问题

首先,先考虑一个问题,什么条件会触发cancelAcquire()方法?

cancelAcquire()方法的反向查找

可以清楚的看到在互斥锁和共享锁的拿锁过程中都是有调用此方法的,而cancelAcquire()方法是写在finally代码块中,并且使用failed标志位来控制cancelAcquire()方法的执行。可以得出,在触发异常的情况下会执行cancelAcquire()方法。

响应中断的获锁方法

可以清楚的看到,这里是响应异常,如果发生了异常,比如中断异常,那么当前线程Node需要做出取消的操作,那么下面详细的说明cancelAcquire()方法。

private void cancelAcquire(Node node) {
    // Ignore if node doesn't exist
    if (node == null)
        return;
    // 当前节点的线程指向置为null
    node.thread = null;
    // 协同取消的处理。
    // 这里是判断当前节点的上一个节点的状态是否是取消状态(状态大于0只有是取消状态)
    // 如果上一个节点是取消状态,那么继续往上遍历,直到找到状态为小于0的状态节点。
    // 并且把当前节点的prev指向非取消节点。
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    // 得到没有取消节点的下一个节点。
    Node predNext = pred.next;
    // 因为当前cancelAcquire()方法就是取消的处理
    // 所以将当前节点设置为取消状态。
    node.waitStatus = Node.CANCELLED;
    // 如果当前取消的节点是tail节点,也就是最后一个节点
    // 那么就把tail指针指向上面while循环遍历出的prev节点(因为要指向一个没有被取消的节点)。
    if (node == tail && compareAndSetTail(node, pred)) {
        // help GC
        // 为什么说help GC呢?
        // 因为把prev的next节点设置为null,
        // 这样GC ROOT扫描发现没有根节点的引用。
        compareAndSetNext(pred, predNext, null);
    } else {
        // 走到else代表当前节点不是tail节点,或者是cas操作的时候tail发生了变化
        // 如果不是tail节点,不能直接把tail节点指向到上面while循环得出的prev节点
        int ws;
        // 这里是的if代码块,是为了尝试一次,如果不成功再去复杂的处理。
        // 这里的if判断条件如下:
            // 1.如果上面while循环得到的prev节点不是head节点
            // 2.如果上面while循环得到的prev节点为-1,如果不为-1,cas改变成-1也。
            // 3.如果上面while循环得到的rpev节点的线程指向不为null(如果为null代表在取消的过程中)
        // 因为&&是拼接,所以上面任意一个条件为false就会进入到else条件中。
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            // 进到这里代表这次尝试成功了。
            // 得到当前节点的下一个节点
            // 然后把前面while循环得到的prev节点的next指向当前节点的next节点。
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
            // 直接唤醒当前节点的下一个节点。
            // 唤醒的目的是为了去执行shouldParkAfterFailedAcquire方法去处理取消节点。
            unparkSuccessor(node);
        }
        // 把当前节点的下一个节点指向自己.
        // help gc。
        node.next = node; // help GC
    }
}

比较复杂,所以笔者为了读者的观看顺利, 下面会拆分步骤,并且画图来理解。

跳过已经取消的节点,找到一个非取消的节点

// Skip cancelled predecessors
// 跳过已经被取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
    node.prev = pred = pred.prev;

更新正常节点的链表

当前取消节点是tail节点的情况

if (node == tail && compareAndSetTail(node, pred)) {
    compareAndSetNext(pred, predNext, null);
} else {
    // 后面讲
}

当前取消节点是非tail节点的情况

// 当前取消的节点不为tail节点的情况
int ws;
// if的逻辑可以理解为尝试一次。
// 代表while循环得到的prev节点不是head节点
// 代表while循环得到的prev节点是可唤醒的正常节点
// 节点while循环得到的prev节点不是待取消节点
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; // help GC

如果if的判断能通过,那就代表当前这次尝试是成功的,成功了就把链表都链上。

问题来了,为什么这里不把Node.next(取消节点的下一个节点)和Node(当前取消节点)的链给断开,不断开的话,JVM是无法回收掉Node(当前取消节点),那不是内存泄漏了?

Doug Lea这里是不是写的有问题?

nonono.

当正常唤醒节点时,抢到锁的节点会执行

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

看到else条件下unparkSuccessor(node);的执行逻辑。

这里已经相当难描述清楚,笔者会执行流程和代码都已经写明白。

这里其实就是一个唤醒操作,唤醒的意义在于去执行shouldParkAfterFailedAcquire()方法从后往前遍历找到一个waitStatus不为1的节点,然后链起来。

cancelAcquire
    |->unparkSuccessor(node当前取消节点)
        |->LockSupport.unpark(node.next.thread)
            |->shouldParkAfterFailedAcquire(node(取消的节点),node.next(取消的节点的下一个节点))
// Node pred 是取消的节点
// Node node 是取消的节点的下一个节点
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);        // 循环条件,所以找到一个小于等于0的节点就会退出
        pred.next = node;              // 链起来
    } else {
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

最终唤醒节点,走到shouldParkAfterFailedAcquire()方法中。从后往前的遍历找到正常的节点

到此这篇关于AQS核心流程解析cancelAcquire方法的文章就介绍到这了,更多相关AQS cancelAcquire内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java AQS中ReentrantLock条件锁的使用

    目录 一.什么是AQS 1.定义 2.特性 3.属性 4.资源共享方式 5.两种队列 6.队列节点状态 7.实现方法 二.等待队列 1.同步等待队列 2.条件等待队列 三.condition接口 四.ReentrantLock 五.源码解析 一.什么是AQS 1.定义 java.util.concurrent包中的大多数同步器实现都是围绕着共同的基础行为,比如等待队列.条件队列.独占获取.共享获取等,而这些行为的抽象就是基于AbstractQueuedSynchronizer(简称AQS)实现的

  • Java AQS中ReentrantReadWriteLock读写锁的使用

    目录 一. 简介 二. 接口及实现类 三.使用 四. 应用场景 五. 锁降级 六.源码解析 七.总结 一. 简介 为什么会使用读写锁? 日常大多数见到的对共享资源有读和写的操作,写操作并没有读操作那么频繁(读多写少),在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发):但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥).在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量.

  • Java AQS信号量Semaphore的使用

    目录 一.什么是Semaphore 二.Semaphore的使用 三.Semaphore源码分析 一.什么是Semaphore Semaphore,俗称信号量,它是操作系统中PV操作的原语在java的实现,它也是基于AbstractQueuedSynchronizer实现的. Semaphore的功能非常强大,大小为1的信号量就类似于互斥锁,通过同时只能有一个线程获取信号量实现.大小为n(n>0)的信号量可以实现限流的功能,它可以实现只能有n个线程同时获取信号量. PV操作是操作系统一种实现进程

  • AQS加锁机制Synchronized相似点详解

    目录 正文 1. Synchronized加锁流程 2. AQS加锁原理 3. 总结 正文 在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁.Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS. AQS全称 AbstractQueuedSynchronizer ,即抽象队列同步器,是一种用来构建锁和同步器的框架. 我们常见的并发锁Reentr

  • Java AQS中闭锁CountDownLatch的使用

    目录 一. 简介 二. 使用 三. 应用场景 四. 底层原理 五. CountDownLatch与Thread.join的区别 一. 简介 CountDownLatch(闭锁)是一个同步协助类,允许一个或多个线程等待,直到其他线程完成操作集. CountDownLatch使用给定的计数值(count)初始化.await方法会阻塞直到当前的计数值(count)由于countDown方法的调用达到0,count为0之后所有等待的线程都会被释放,并且随后对await方法的调用都会立即返回.这是一个一次

  • Tomcat启动核心流程示例详解

    目录 一.Tomcat的启动核心流程 1.启动的入口 2.init方法 3.load方法 4.start方法 5.核心流程的总结 一.Tomcat的启动核心流程 前面给大家介绍了Tomcat中的生命周期的设计,掌握了这块对于我们分析Tomcat的核心流程是非常有帮助的,也就是我们需要创建相关的核心组件,比如Server,Service肯定都绕不开生命周期的方法. 1.启动的入口 你可以通过脚本来启动Tomcat服务(startup.bat),但如果你看过脚本的命令,你会发现最终调用的还是Boot

  • axios库的核心代码解析及总结

    目录 一.关键步骤 1.创建axios对象 2.请求 二.Axios类 1.基础属性 2.辅助方法 3.request方法 三.adpter适配器 1.xhradpter 2.httpadpter 一.关键步骤 1.创建axios对象 axios库导出的对象是一个已经被创建好的axios对象,它本质上是一个方法,可以直接接收一个config配置参数进行请求.在库的入口处,即可看到如下代码: function createInstance(defaultConfig) { // 传入默认配置生成a

  • Android编程解析XML方法详解(SAX,DOM与PULL)

    本文实例讲述了Android编程解析XML方法.分享给大家供大家参考,具体如下: XML在各种开发中都广泛应用,Android也不例外.作为承载数据的一个重要角色,如何读写XML成为Android开发中一项重要的技能.今天就由我向大家介绍一下在Android平台下几种常见的XML解析和创建的方法. 在Android中,常见的XML解析器分别为SAX解析器.DOM解析器和PULL解析器,下面,我将一一向大家详细介绍. SAX解析器: SAX(Simple API for XML)解析器是一种基于事

  • Python爬虫DNS解析缓存方法实例分析

    本文实例讲述了Python爬虫DNS解析缓存方法.分享给大家供大家参考,具体如下: 前言: 这是Python爬虫中DNS解析缓存模块中的核心代码,是去年的代码了,现在放出来 有兴趣的可以看一下. 一般一个域名的DNS解析时间在10~60毫秒之间,这看起来是微不足道,但是对于大型一点的爬虫而言这就不容忽视了.例如我们要爬新浪微博,同个域名下的请求有1千万(这已经不算多的了),那么耗时在10~60万秒之间,一天才86400秒.也就是说单DNS解析这一项就用了好几天时间,此时加上DNS解析缓存,效果就

  • python manage.py runserver流程解析

    这篇文章主要介绍了python manage.py runserver流程解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 版本 python27 django 1.0 搭建可运行的环境 创建python27 虚拟环境 github 下载 django-1.0.tar.gz(1.0 版本的django) 解压 可以看到,有个 demo 在 examples 目录 把 django 目录拷贝到 examples 下面,这样 example 可以

  • Android9.0 SystemUI 网络信号栏定制修改的流程解析

    前情提要 Android 8.1平台SystemUI 导航栏加载流程解析 9.0 改动点简要说明 1.新增 StatusBarMobileView 替代 SignalClusterView,用以控制信号栏显示 同时增加的还有 StatusBarIconView.StatusBarWifiView 2.整体流程和 8.1 类似 效果图 整体流程图 上代码 先来看初始赋值的地方 MobileSignalController.java,在 notifyListeners() 方法中进行我们对应的定制,

  • activiti实现员工请假流程解析

    源码下载: http://xiazai.jb51.net/202007/yuanma/FirstActiviti_jb51.rar 链接: https://pan.baidu.com/s/1tCN7SDAdEUerZxcTr_9cqA 提取码: twmp 在开始之前,先说一下刚开始学习工作流的时候遇到的问题,感觉比较困惑,经过这两天的学习,也算有所收获 1.部署了多个流程变量,如何准确开启特定的流程 2. 在一个流程的执行过程中,怎么确定执行到哪一步,即执行到哪个任务了 3. 在有多种情况的条件

  • 采用React编写小程序的Remax框架的编译流程解析(推荐)

    Remax是蚂蚁开源的一个用React来开发小程序的框架,采用运行时无语法限制的方案.整体研究下来主要分为三大部分:运行时原理.模板渲染原理.编译流程:看了下现有大部分文章主要集中在Reamx的运行时和模板渲染原理上,而对整个React代码编译为小程序的流程介绍目前还没有看到,本文即是来补充这个空白. 关于模板渲染原理看这篇文章:https://www.jb51.net/article/132635.htm 关于remax运行时原理看这篇文章:https://www.jb51.net/artic

  • 三种Java自定义DNS解析器方法与实践

    目录 1.InMemoryDnsResolver 2.SystemDefaultDnsResolver 3.自定义DnsResolver 4.连接池管理器 5.测试 前言: 最近终于用上了高性能的测试机(54C96G * 3),相较之前的单机性能提升了三倍,数量提升了三倍,更关键的宽带提单机升了30倍不止,总体讲提升了100多倍,这下再也不用担心单机压力机瓶颈,直接原地起飞. 不过没高兴5分钟,我发现接口居然请求不通,经过一阵拨乱反正终于找到原因:域名无法解析,IP无法直接访问. 自然而然,解决

  • Python可视化程序调用流程解析

    目录 引言 安装 graphviz 工具 实战 引言 今天我们来分享一个 Python 领域的神级第三方库 -- pycallgraph,通过该库并结合 graphviz 工具,就可以非常方便的完成 Python 应用程序调用流程的可视化工作 我们先来看下效果图 怎么样,很是惊艳吧~ 下面我们就来一起完成这个可视化过程 安装 graphviz 工具 生成图片的过程,是依赖工具 graphviz 的,我们先进行下载安装 下载地址 www.graphviz.org/download/ 详细对于 gr

随机推荐