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

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

6 锁的获取

获取锁显式的方法就是 Lock.lock () ,最终目的其实是想让线程获得对资源的访问权。而 Lock 又是 AQS 的子类,lock 方法根据情况一般会选择调用 AQS 的 acquire 或 tryAcquire 方法。


acquire 方法 AQS 已经实现了,tryAcquire 方法是等待子类去实现,acquire 方法制定了获取锁的框架,先尝试使用 tryAcquire 方法获取锁,获取不到时,再入同步队列中等待锁。tryAcquire 方法 AQS 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁,接下来我们详细看下 acquire 的源码解析。


acquire 也分两种,一种是独占锁,一种是共享锁


6.1 acquire 独占锁

  • 独占模式下,尝试获得锁

image.png


在独占模式下获取,忽略中断。 通过至少调用一次 tryAcquire(int) 来实现,并在成功后返回。 否则,将线程排队,并可能反复阻塞和解除阻塞,并调用 tryAcquire(int) 直到成功。 该方法可用于实现方法 Lock.lock()。

对于 arg 参数,该值会传送给 tryAcquire,但不会被解释,可以实现你喜欢的任何内容。


看一下 tryAcquire 方法

image.png


AQS 对其只是简单的实现,具体获取锁的实现方法还是由各自的公平锁和非公平锁单独实现,实现思路一般都是 CAS 赋值 state 来决定是否能获得锁(阅读后文的 ReentrantLock 核心源码解析即可)。



image.png


image.png


执行流程

通过当前的线程和锁模式新建一个节点

pred 指针指向尾节点tail


将Node 的 prev 指针指向 pred

通过compareAndSetTail方法,完成尾节点的设置。该方法主要是对tailOffset和Expect进行比较,如果tailOffset的Node和Expect的Node地址是相同的,那么设置Tail的值为Update的值。


image.png


  • 如果 pred 指针为 null(说明等待队列中没有元素),或者当前 pred 指针和 tail 指向的位置不同(说明被别的线程已经修改),就需要 enq    
private Node enq(final Node node) {
  // juc 中看到死循环,肯定有多个分支
    for (;;) {
      // 初始值为 null
        Node t = tail;
        if (t == null) { // 那就初始化
          // 由于是多线程操作,为保证只有一个
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
          // 将当前线程 node 的 prev 设为t
          // 注意这里先更新的是 prev 指针
            node.prev = t;
            if (compareAndSetTail(t, node)) {
              // 有 next延后更新的,所以通过 next 不一定找得到后续结点,所以释放锁时是从 tail 节点开始找 prev 指针
                t.next = node;
                return t;
            }
            // 因为prev 指针是 volatile 的,所以这里的 node.prev = t 线程是可见的。所以只要 compareAndSetTail,那么必然其他线程可以通过 c 节点的 prev 指针访问前一个节点且可见。
        }
    }
}

if 分支


image.png


把新的节点添加到同步队列的队尾。


如果没有被初始化,需要进行初始化一个头结点出来。但请注意,初始化的头结点并不是当前线程节点,而是调用了无参构造函数的节点。如果经历了初始化或者并发导致队列中有元素,则与之前的方法相同。其实,addWaiter就是一个在双端链表添加尾节点的操作,需要注意的是,双端链表的头结点是一个无参构造函数的头结点。


线程获取锁的时候,过程大体如下:



  1. 当没有线程获取到锁时,线程1获取锁成功
  2. 线程2申请锁,但是锁被线程1占有


image.png


如果再有线程要获取锁,依次在队列中往后排队即可。


在 addWaiter 方法中,并没有进入方法后立马就自旋,而是先尝试一次追加到队尾,如果失败才自旋,因为大部分操作可能一次就会成功,这种思路在自己写自旋的时候可以多多参考哦。


6.1.2 acquireQueued

此时线程节点 node已经通过 addwaiter 放入了等待队列,考虑是否让线程去等待。

阻塞当前线程。


  • 自旋使前驱结点的 waitStatus 变成 signal,然后阻塞自身
  • 获得锁的线程执行完成后,释放锁时,会唤醒阻塞的节点,之后再自旋尝试获得锁
final boolean acquireQueued(final Node node, int arg) {
  // 标识是否成功取得资源
    boolean failed = true;
    try {
        // 标识是否在等待过程被中断过
        boolean interrupted = false;
        // 自旋,结果要么获取锁或者中断
        for (;;) {
          // 获取等待队列中的当前节点的前驱节点
            final Node p = node.predecessor();
            // 代码优化点:若 p 是头结点,说明当前节点在真实数据队列的首部,就尝试获取锁(此前的头结点还只是虚节点)
            if (p == head && tryAcquire(arg)) {
              // 获取锁成功,将头指针移动到当前的 node
                setHead(node);
                p.next = null; // 辅助GC
                failed = false;
                return interrupted;
            }
            // 获取锁失败了,走到这里
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

看其中的具体方法:

setHead

image.png


方法的核心:

shouldParkAfterFailedAcquire

依据前驱节点的等待状态判断当前线程是否应该被阻塞

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
  // 获取头结点的节点状态
    int ws = pred.waitStatus;
    // 说明头结点处于唤醒状态
    if (ws == Node.SIGNAL)
        /*
         * 该节点已经设置了状态,要求 release 以 signal,以便可以安全park
         */
        return true;
    // 前文说过 waitStatus>0 是取消状态    
    if (ws > 0) {
        /*
         * 跳过已被取消的前驱结点并重试
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus 必须为 0 或 PROPAGATE。 表示我们需要一个 signal,但不要 park。 调用者将需要重试以确保在 park 之前还无法获取。
         */
        // 设置前驱节点等待状态为 SIGNAL 
        // 给头结点放一个信物,告诉此时
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}


为避免自旋导致过度消费 CPU 资源,以判断前驱节点的状态来决定是否挂起当前线程


挂起流程图

image.png


如下处理 prev 指针的代码。shouldParkAfterFailedAcquire 是获取锁失败的情况下才会执行,进入该方法后,说明共享资源已被获取,当前节点之前的节点都不会出现变化,因此这个时候变更 prev 指针较安全。

image.png

目录
相关文章
|
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

热门文章

最新文章

推荐镜像

更多