【JUC】三十一、AQS源码

在这里插入图片描述
📕前置笔记:【AQS核心概念与核心类】

文章目录

  • 1、ReentrantLock与AQS类的联系
  • 2、lock方法
  • 3、acquire方法
  • 4、源码分析Demo背景案例
  • 5、tryAcquire方法
  • 6、addWaiter方法
  • 7、acquireQueued方法
  • 8、unlock方法
  • 9、cancelAcquire方法

AQS是JUC的基石,很多JUC的相关类底层都是通过AQS框架实现的。PS:以下是Java11下的源码。

1、ReentrantLock与AQS类的联系

Lock接口的实现类,基本都是通过聚合了一个【队列同步器AQS】的子类来完成对线程访问的控制,这里以Lock接口的实现类ReentrantLock为例分析AQS源码。

在这里插入图片描述

Sync、NonfairSync、FairSync三个内部类的关系:

在这里插入图片描述

Lock对外暴露lock和unlock两个操作API,中间是NonfairSync非公平锁和FairSync公平锁这两个内部类,实际上操作的则是sync这个抽象内部类,而sync继承了AQS类。

在这里插入图片描述

2、lock方法

以下从lock方法开始,分析AQS源码:

//常见的操作:
Lock lock = new ReentrantLock();
lock.lock();
try{
	//do Something
} finally{
	lock.unlock();
}

点击跟进lock方法源码:

在这里插入图片描述

跟进acquire ==> tryAcquire,这里是模板模式:

在这里插入图片描述

继续跟进看其在NonfairSync、FairSync里的实现:

在这里插入图片描述

可以看到公平锁和非公平锁对lock方法的实现,区别是if条件中多了一个调用,判断有无前置节点(队列中有没线程在等待)。返回true,即当前线程前面有排队线程,false即当前线程就是队列中的头,或者队列为空。公平锁在加锁前调用hasQueuedPredecessors方法判断等待队列中是否存在有效的Node节点:

在这里插入图片描述

有了这个判断:

  • 对于公平锁,线程来了获取锁时,如果队列中已有线程在等待,则当前线程加入等待队列
  • 对于非公平锁,不管队列中有无等待线程,如果来抢锁时可以获取锁,则立刻占有,也就是说队列中排队的第一个线程苏醒后,不一定能拿到锁,因为期间可能有刚来的线程竞争成功,不讲武德直接插队夺锁

简言之,非公平锁的lock方法,直接是CAS抢,抢到了直接干活儿,抢不到再acquire排队。

3、acquire方法

从上面对lock方法的大致分析可以看到:不管公平锁非公平锁,底层都是AQS类的acquire方法,传入1。acquire方法细分为三个步骤:

  • tryAcquire(arg)
  • addWaiter(Node.EXCLUSIVE)
  • acquireQueued(addWaiter(Node.EXCLUSIVE),arg)

在这里插入图片描述

if体的selfInterrupt方法没啥好说的:

在这里插入图片描述

以下分别对三个步骤里的方法做详细分析。

4、源码分析Demo背景案例

写个简单的示意Demo,做为下面分析多线程出队入队的背景案例:A、B、C三个顾客去银行办理业务,A先到,此时窗口空无一人,他优先获得办理业务的机会。(类比A线程先抢到对象锁),但A顾客办理业务耗时长达20分钟,期间B、C线程也进来抢锁了:

public class CodeAnalyse {

    public static void main(String[] args) {

        ReentrantLock lock = new ReentrantLock(); //非公平锁
        //A顾客,耗时20min
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println("----come in A");
                TimeUnit.MINUTES.sleep(20);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }, "A").start();
        //第二个顾客,B顾客,一看A在办理,只能去候客区(进入AQS队列),等待A办理完后再尝试抢锁
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("----come in B");
            }finally {
                lock.unlock();
            }
        },"B").start();

        //第三个顾客,C顾客,一看A在办理,只能去候客区(进入AQS队列),等待A办理完后再尝试抢锁,在FIFO队列,C前面是B
        new Thread(() -> {
            lock.lock();
            try{
                System.out.println("----come in C");
            }finally {
                lock.unlock();
            }
        },"C").start();
        
        //后续顾客D、E、F....以此类推
    }
}


画个示意图,初始状态:

在这里插入图片描述

5、tryAcquire方法

从lock到acquire方法,接下来看acquire方法的第一个步骤tryAcquire方法的源码。如Demo案例,A线程先来,此时没线程,AQS类的volatile变量 state = 0,A抢占成功:

在这里插入图片描述

接着B线程lock,走到tryAcquire,判断c != 0,往下拱,当前线程current为B,OwnerThread为A,也不相等,返回false

在这里插入图片描述

false取反,继续往下判断条件,就到了addWaiter

在这里插入图片描述

因此,tryAcquire方法是尝试抢锁,看下state位:

  • state等于0时,CAS改掉状态位,并修改所有者线程为自己
  • state显示在忙时,去看看占锁的线程是不是自己

6、addWaiter方法

addWaiter方法,看名字就知道:为当前线程以给定模式创建Node并入队。接着上面看,当前addWaiter方法的传参是Node.EXCLUSIVE,即线程要以独占的方式等待锁,相反的,Node.SHARED表示线程以共享的模式等待锁。开始第一轮循环,AQS队列为空,尾指针等于null,不满足条件,走initializeSyncQueue:

在这里插入图片描述

注意现在的B线程所对应的Node B并不是头节点,而是一个新new的Node,这个Node也称虚拟节点或者哨兵节点,作用是占位,Node的两个属性:Thread = null ,waitStatus = 0。最后把头节点设置为尾节点

在这里插入图片描述

队列示意图:

在这里插入图片描述

此时,再回addWaiter方法进行下一次循环,尾节点tail不再为null,设置我最终要返回Node B的前置节点为oldTail,oldTail = tail,也即刚才的那个虚拟节点。然后CAS尾节点为Node B,返回Node B对象:

在这里插入图片描述

如下图示意:

在这里插入图片描述

此时,NodeB(线程B)入队成功。同理,C线程来,NodeC入队也一样,不同的是,尾节点不再为null,不用重复刚来时的初始化步骤initializeSyncQueue了:

在这里插入图片描述

总之就是来回再倒一个双向链表的prev和next指针,以及头尾指针的关系,最终实现入队。

双向链表中,第一个节点为虚节点(也叫哨兵节点),其实并不存储任何信息,只是占位。真正的第一个有数据的节点,是从第二个节点开始的。

7、acquireQueued方法

上面的addWaiter方法返回Node B 后,再回到acquire方法,该如何实现入队后的通知唤醒操作?

在这里插入图片描述

传入addWaiter返回的Node B节点与args = 1。进入acquireQueued方法,一轮循环,获得传入节点的前置节点,Node B的前置,即虚拟节点,判断p是头节点没问题,继续往下拱,到前面的tryAcquire,即再尝试抢一次,假设A线程仍还在办理业务,那这次tryAcquire自然也返回false,到了shouldParkAfterFailedAcquire(方法名含义:在抢锁失败后是否Park等待唤醒):

在这里插入图片描述

传入前置节点和Node B节点:ws = 0,Node.SINGLE= -1,不相等,直接到最后一个else分支,把前置节点的waitStatus属性改为-1,并返回false

在这里插入图片描述

当前状态示意图:

在这里插入图片描述
退回刚开始acquireQueued方法的for(;;)循环,同理,A还在继续占着锁,仍然进到shouldParkAfterFailedAcquire方法:

在这里插入图片描述

此时,前置节点的的waitStatus是-1了,返回true,到了parkAndCheckInterrupt方法:

在这里插入图片描述

parkAndCheckInterrupt方法使用了LockSupport,且被线程如果是被中断的,就返回true。LockSupport.park阻塞线程B,直到被unpark发放permit许可,到此,B就稳稳当当的停留在队列中了,等着后面的唤醒通知机制。

在这里插入图片描述

同理,Node C进来,node C的前置节点为node B,不是head,都不用tryAcquire尝试抢锁了,直接执行两次shouldParkAfterFailedAcquire,把它前置节点node B的waitStatus从0改为-1,还是到这儿,把B的waitStatus由默认的0改为-1,返回false。再到parkAndCheckInterrupt,稳当地在队里等着被唤醒。

在这里插入图片描述

到此,再看上篇的waitStatus这个属性,这个-1的含义就理解透彻了,即Node所在的线程已经安静坐着等释放锁了。

在这里插入图片描述

再整理梳理下上面acquireQueued方法内部调用的两个重要方法的源码:

  • shouldParkAfterFailedAcquire
  • parkAndCheckInterrupt

如果前置节点的waitStatus是SIGNAL状态,即shouldParkAfterFailedAcquire返回true,就执行parkAndCheckInterrupt挂起当前线程。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
    }
    return false;
}

在这里插入图片描述

在这里插入图片描述

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

在这里插入图片描述

到此,lock方法 ⇒ acquire方法下的三个方法整体流程结束:

  • tryAcquire(arg)
  • addWaiter(Node.EXCLUSIVE)
  • acquireQueued(addWaiter(Node.EXCLUSIVE),arg)

总结就是,它们分别在做尝试抢 ⇒ 抢不到,入队 ⇒ 帮助Node坐稳椅子(LockSupport.park() 挂起),等待锁释放后FIFO唤醒

在这里插入图片描述

8、unlock方法

以上,B、C在队列等,A线程终于忙完了unlock,state = 0了,AQS队列钟排队的线程也该出队了。接下来看unlock方法涉及的AQS的源码:

在这里插入图片描述

跟进release方法:

在这里插入图片描述

抛出不支持的操作异常,模板设计模式:

在这里插入图片描述

往下看在子类ReentrantLock的实现,看条件判断里的tryRelease方法:state = 1,传入的release = 1,int c = 1 -1 = 0,下面这个自然也一般不出现,调用方法准备释放锁的线程即线程A,Thread.currentThrad为A,ownerThread也是A

在这里插入图片描述

进入下面的if分支,设置free为true,改抢到的线程owenerThread为null,坑位腾出来,把state设置为0,返回true:

在这里插入图片描述

再回到调用点,tryRelease条件上面返回了true,进入,头节点给h,h成了虚拟节点,其不为null,且waitStatus在上面B入队时就改成了-1,条件成立,进入方法里unparkSuccessor

在这里插入图片描述

传入h,即虚拟头节点,waitStatus为-1,<0,CAS重新设回0:

在这里插入图片描述

Node s是h的下一个节点,也就是Node B ,不等于null,但其waitStatus是 -1(Node C入队时改的),不大于0,因此走unpark这儿,唤醒B:

在这里插入图片描述

回调用点,release方法返回true,即unlock成功:

在这里插入图片描述

再说刚才unparkSuccessor里的unpark B线程,退回上面lock下的源码里的Locksupport,这儿还LockSupport等待被放发permit后唤醒着呢:

在这里插入图片描述

B被unpark了,不是中断 了,这里返回false:

在这里插入图片描述

继续for(;;)进行下一轮循环,B的前置节点为虚拟节点,符合,继续tryAcquire抢占锁:

在这里插入图片描述

为什么B排队了,现在轮到它了却还要尝试抢锁?因为非公平锁,B被唤醒了,此时也有可能刚好又来了线程X,而X插队抢到了。

在这里插入图片描述

假设此时B去执行tryAcquire,return true,抢占成功,再回调用处:

在这里插入图片描述

看下setHead方法,此时Node node为Node B,Node p是虚拟节点,B为头节点,B的thread改为null,因为B抢到锁了,这把Node的椅子腾出来,Node B的前置指针改为null

在这里插入图片描述

示意图:

在这里插入图片描述

此时,p.next再设置为null,再没有任何变量指向虚拟节点的对象Node,需要回收,因此,才help GC ,返回false

在这里插入图片描述

acquire方法判断条件为false,不用自我中止,执行结束,lock方法执行结束,线程B终于抢锁成功,加冕!

在这里插入图片描述

此时,原本的虚拟节点仙逝了,而抢到锁的B线程所在的Node成了新一代的虚拟节点

在这里插入图片描述
到此,A彻底退出江湖,B抢到锁办理业务中…

9、cancelAcquire方法

主流程中,还有一个cancelAcquire(异常情况,等一半不想等了):
在这里插入图片描述

跟进:

在这里插入图片描述

跟进,看到cancelAcquire方法,某Node中途不排队了,取消抢锁:

在这里插入图片描述

如图:

在这里插入图片描述

如果现在走人的是队尾节点Node 5,那就node4.next = null,队尾指针tail指向node4就行:

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
		//node要走了,线程属性设置为空
        node.thread = null;

        // 取前置节点,后面做判断
        Node pred = node.prev;
        //waitStatus只有取值1时,才能大于0,1即获取锁的请求取消
        //即前置节点也要走,属于下面的情况,这里先说只有node5走的情况
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        Node predNext = pred.next;
		//设置node 5的状态值为cancle
        node.waitStatus = Node.CANCELLED;
		//如果是尾指针,CAS把前一个节点设置为队尾,并把pred.next置为null
        if (node == tail && compareAndSetTail(node, pred)) {
            pred.compareAndSetNext(predNext, null);
        } else {
            //.....略

如果走人的是中间节点,比如Node4,又如果一走就连着走了一片,比如node3也要走:

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;
		//node要走了,线程属性设置为空
        node.thread = null;

        // 取前置节点,后面做判断
        Node pred = node.prev;
        //waitStatus只有取值1时,才能大于0,1即获取锁的请求取消
        //即前置节点也要走,现在就是这个情况
        while (pred.waitStatus > 0)
        	//while当前置节点的状态大于0,即是取消状态,就一直循环
        	//如此就找到了非取消状态的前置节点,并把指针指向赋值改过来    
            node.prev = pred = pred.prev;

        Node predNext = pred.next;
        
        node.waitStatus = Node.CANCELLED;
		//如果是尾指针,这里走else分支
        if (node == tail && compareAndSetTail(node, pred)) {
            pred.compareAndSetNext(predNext, null);
        } else {
            //
            } 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 && pred.compareAndSetWaitStatus(ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    pred.compareAndSetNext(predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
}

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mfbz.cn/a/257647.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

智能优化算法应用:基于寄生捕食算法3D无线传感器网络(WSN)覆盖优化 - 附代码

智能优化算法应用&#xff1a;基于寄生捕食算法3D无线传感器网络(WSN)覆盖优化 - 附代码 文章目录 智能优化算法应用&#xff1a;基于寄生捕食算法3D无线传感器网络(WSN)覆盖优化 - 附代码1.无线传感网络节点模型2.覆盖数学模型及分析3.寄生捕食算法4.实验参数设定5.算法结果6.…

数字化技术助力英语习得 iEnglish成智慧化学习新选择

日前,美剧《老友记》中钱德勒的扮演者马修派瑞去世的消息引发不少人的回忆杀。《老友记》官方发文悼念马修派瑞:“对于马修派瑞去世的消息,我们深感悲痛,他是给我们所有人的真正礼物,我们的心和他的家人、爱人、所有的粉丝在一起。” 作为不少国人刷剧学习英语的首选,《老友记…

C语言-数组指针笔试题讲解(1)-干货满满!!!

文章目录 ▶️1.sizeof和strlen的对比&#x1f4af;➡️1.1 sizeof是什么&#xff1f;&#x1f4af;➡️1.2sizeof用法举例&#x1f4af;▶️1.3strlen是什么&#xff1f;&#x1f4af;▶️1.4 strlen函数用法举例&#xff1a;&#x1f4af;▶️1.5 strlen和sizeof的对比&#…

优化问题笔记(2)

目录 3. 约束优化问题的全局解3.1 凸优化问题3.2 二次优化问题3.3 无约束二次优化问题3.4 一个典型的二次等式约束二次优化问题 Reference 3. 约束优化问题的全局解 3.1 凸优化问题 局部解成为全局解的一类重要的优化问题是所谓凸优化问题. 我们称优化问题 ( f , D ) (f,\ma…

性能测试之全链路压测流量模型你了解多少

前言 现在全链路越来越火&#xff0c;各大厂商也纷纷推出了自己的全链路压测测试方案。特别是针对全链路压测流量模型&#xff0c;各家方案都有所不同。最近我看了一些这方面的资料&#xff0c;有一些感悟。分享给大家。 全链路压测流量模型的梳理呢&#xff0c;这里就先不讲…

Gemini Pro API 详细申请步骤

Gemini Pro API 详细申请步骤 什么是 Gemini ? 上周&#xff0c;谷歌发布了 Gemini&#xff08;双子座&#xff09;&#xff0c;它是谷歌最新、最强大的人工智能模型&#xff0c;旨在迎头痛击 OpenAI 的 GPT。Gemini 在构建时考虑到了多模态性&#xff0c;这意味着它能够理解…

【日常总结】连接Mysql,打开数据表非常慢

问题 Navicat 连接mysql时&#xff0c;第二次打开非常慢 原因 Mysql服务器端会定时清理长时间不活跃空闲的数据库连接&#xff0c;以此优化数据库的性能。 解决方案 数据库右键---编辑连接--高级---保持连接间隔30秒 带来的问题 每次打开Navicat时&#xff0c;设置设置自动…

2023-南京荣耀Honor最新探访-OPEN DAY,实况记录!

前言 金九银十的秋招季缓缓落幕&#xff01; 接完offer&#xff0c;博主也回归啦&#xff01;有幸带着公开公平的心态&#xff0c;给大家分享一波南京荣耀总部的实况解密&#xff01; 最基础的环境状况&#xff0c;有图有真相&#xff01;~上图~ 民以食为天-南京荣耀食堂&…

issue阶段的选择电路的实现

1-of-M的仲裁电路 为什么要实现oldest-first 功能的仲裁呢&#xff1f; 这是考虑到越是旧的指令&#xff0c;和它存在相关性的指令也就越多&#xff0c;因此优先执行最旧的指令&#xff0c;则可以唤醒更多的指令&#xff0c;能够有效地提高处理器执行指令的并行度,而且最旧的指…

MapReduce和Yarn部署+入门

看的黑马视频记的笔记 目录 1.入门知识点 分布式计算&#xff1a; 概念&#xff1a; 两种模式&#xff1a; MapReduce&#xff08;分布式计算&#xff0c;分散汇总模式&#xff09; 概念 执行原理 注&#xff1a; Yarn&#xff08;分布式资源调度&#xff09; 概述 Y…

Linux Mint 21.3 代号为“Virginia”开启下载

Linux Mint 团队今天放出了 Linux Mint 21.3 Beta ISO 镜像&#xff0c;正式版计划在今年圣诞节发布。 支持 在实验性支持 Wayland 之外&#xff0c;Cinnamon 6.0 版 Linux Mint 21.3 Beta 镜像还带来了其它改进&#xff0c;Nemo 文件夹管理器右键菜单支持下载相关操作。 Cin…

SpringBoot之分层解耦以及 IOCDI的详细解析

### 3.2 分层解耦 刚才我们学习过程序分层思想了&#xff0c;接下来呢&#xff0c;我们来学习下程序的解耦思想。 解耦&#xff1a;解除耦合。 #### 3.2.1 耦合问题 首先需要了解软件开发涉及到的两个概念&#xff1a;内聚和耦合。 - 内聚&#xff1a;软件中各个功能模块内…

机器学习---聚类(原型聚类、密度聚类、层次聚类)

1. 原型聚类 原型聚类也称为“基于原型的聚类” (prototype-based clustering)&#xff0c;此类算法假设聚类结构能通过一 组原型刻画。算法过程&#xff1a;通常情况下&#xff0c;算法先对原型进行初始化&#xff0c;再对原型进行迭代更新求解。著 名的原型聚类算法&#…

OneBlog Shiro 反序列化漏洞复现

0x01 产品简介 OneBlog 是一个简洁美观、功能强大并且自适应的Java博客。使用springboot开发,前端使用Bootstrap。支持移动端自适应,配有完备的前台和后台管理功能。 0x02 漏洞概述 OneBlog v2.2.2 及之前的版本存在 shiro 反序列化漏洞,该漏洞源于软件存在硬编码的 shir…

【数据结构和算法】盛最多水的容器

其他系列文章导航 Java基础合集数据结构与算法合集 设计模式合集 多线程合集 分布式合集 ES合集 文章目录 其他系列文章导航 文章目录 前言 一、题目描述 二、题解 2.1 方法一&#xff1a;暴力枚举 2.2 方法二&#xff1a;双指针 三、代码 3.1 方法一&#xff1a;暴力…

递归实现归并排序与测试各类排序的性能

目录 基本思想 代码实现 时空复杂度 测试各排序性能 基本思想 将待排序的数组不断二分&#xff0c;直到每个子数组只包含一个元素或为空。然后通过合并操作将这些子数组逐步合并成较大的有序数组&#xff0c;最终得到完全有序的结果&#xff1a; 下面是递归版本的归并排序…

Linux(4)-LAMP

L-LinuxA-apache/nginxM-mysqlp-php 搭建LAMP以及使用discuz搭建论坛网站 安装apache yum install httpd -y // 安装service httpd start // 启动Apache通过netstat -tunlp查看apache运行的端口&#xff0c;然后打开虚拟机ip 80端口能看到以下页面 或者 安装Mysql centOS6…

Python Pandas 重复值处理(第12讲)

Python Pandas 重复值处理(第12讲)         🍹博主 侯小啾 感谢您的支持与信赖。☀️ 🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔ꦿ🌹꧔…

基于JAVA的农村物流配送系统 开源项目

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块2.1 系统登录、注册界面2.2 系统功能2.2.1 快递信息管理&#xff1a;2.2.2 位置信息管理&#xff1a;2.2.3 配送人员分配&#xff1a;2.2.4 路线规划&#xff1a;2.2.5 个人中心&#xff1a;2.2.6 退换快递处理&#xff1a;…

yolov5障碍物识别-雪糕筒识别(代码+教程)

简介 这是一个检测交通锥并识别颜色的项目。我使用 yolov5 来训练和检测视锥细胞。此外&#xff0c;我使用 k 均值来确定主色&#xff0c;以对锥体颜色进行分类。目前&#xff0c;支持的颜色为红色、黄色、绿色和蓝色。其他颜色被归类为未知。 数据集和注释 我使用了一个自收…