【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顾客,耗时20minnew 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前面是Bnew 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 existif (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的状态值为canclenode.waitStatus = Node.CANCELLED;//如果是尾指针,CAS把前一个节点设置为队尾,并把pred.next置为nullif (node == tail && compareAndSetTail(node, pred)) {pred.compareAndSetNext(predNext, null);} else {//.....略

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

private void cancelAcquire(Node node) {// Ignore if node doesn't existif (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.hqwc.cn/news/287062.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

自动化测试流程(超详细整理)

最近很多小伙伴问我自动化测试到底该怎么做&#xff1f;流程是什么样的&#xff1f;在每个阶段都需要注意什么&#xff1f;本文也就主要从自动化测试的基本流程入手&#xff0c;对面试自动化测试工程师的同学会有不少帮助。对于在职的朋友&#xff0c;也可以参考此流程&#xf…

15 使用v-model绑定单选框

概述 使用v-model绑定单选框也比较常见&#xff0c;比如性别&#xff0c;要么是男&#xff0c;要么是女。比如单选题&#xff0c;给出多个选择&#xff0c;但是只能选择其中的一个。 在本节课中&#xff0c;我们演示一下这两种常见的用法。 基本用法 我们创建src/component…

继电器的工作原理及驱动电路

继电器是具有隔离功能的自动开关元件&#xff0c;广泛应用于遥控、遥测、通讯、自动控制、机电一体化及电力电了设备中&#xff0c;是最重要的控制元件之一。继电器实际上是用较小的电流去控制较大电流的一种“自动开关”。故在电路中起着自动调节、安全保护、转换电路等作用。…

高级桌面编程(二)

一、前言 文章的续作前文是&#xff1a; 高级桌面编程&#xff08;一&#xff09;-CSDN博客https://blog.csdn.net/qq_71897293/article/details/135072204?spm1001.2014.3001.5502 二、自定义控件 1创建自定义控件&#xff0c;如下图所示&#xff1a; 2 在创建的页面可以…

葡萄酒的主要区别只在于葡萄本身吗?

谈到葡萄酒&#xff0c;许多人认为选择最喜欢的葡萄酒只是简单地挑选一种颜色:红色或白色。红色和白色的区别是选择葡萄酒的一个很好的起点&#xff0c;但这仅仅是一个起点。要真正享受葡萄酒的体验&#xff0c;你应该深入了解自己。 如果你已经知道你喜欢白葡萄酒&#xff0c;…

25 redis 中 cluster 集群的工作模式

前言 我们这里首先来看 redis 这边实现比较复杂的 cluster集群模式 整个 cluster集群 中会包含多对 MasterSlave 的组合, 然后这多对 MasterSlave 来分解 16384 个 slot 然后 客户端这边 set, get 的时候, 先根据 key 计算对应存储的 slot, 然后 服务器这边响应 MOVED 目标…

飞速(FS)100G ZR4 光模块80km长距离传输

如今&#xff0c;100G QSFP28光模块已经被广泛部署在100m到40km的范围内。然而&#xff0c;传统的100G QSFP28模块面临一个挑战&#xff0c;因为它们的设计仅限于不超过40km的距离。超出此范围&#xff0c;色散、光衰减等问题就会增加&#xff0c;导致信噪比&#xff08;SNR&am…

【C语言】自定义类型:结构体深入解析(一)

&#x1f308;write in front :&#x1f50d;个人主页 &#xff1a; 啊森要自信的主页 ✏️真正相信奇迹的家伙&#xff0c;本身和奇迹一样了不起啊&#xff01; 欢迎大家关注&#x1f50d;点赞&#x1f44d;收藏⭐️留言&#x1f4dd;>希望看完我的文章对你有小小的帮助&am…

Sharding-Jdbc(5):Sharding-Jdbc通过配置文件形式配置分表

1 项目目录 2 配置maven <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache…

LeetCode Hot100 51.N皇后

题目&#xff1a; 按照国际象棋的规则&#xff0c;皇后可以攻击与之处在同一行或同一列或同一斜线上的棋子。 n 皇后问题 研究的是如何将 n 个皇后放置在 nn 的棋盘上&#xff0c;并且使皇后彼此之间不能相互攻击。 给你一个整数 n &#xff0c;返回所有不同的 n 皇后问题 的…

微机原理与接口技术——中断系统

文章目录 一、中断指令概念1、中断类型码2、中断向量3、中断向量表简述接收到中断指令后操作 二、8086中断指令开中断指令&#xff1a;STI关中断指令&#xff1a;CLI软件中断指令&#xff1a;INT n中断返回指令 IRET 三、微机系统中断分类四、CPU响应可屏蔽与非屏蔽中断的条件响…

34 无聊的小明

数组存放每一次运算后的结果&#xff0c;若有重复则满足小明心意。 #include <iostream> using namespace::std; using std::cout; using std::cin; int pfh(int n) {int sum 0;while(n ! 0){int tn%10;sum sumt*t;n n/10;}return sum; }int wlxm(int n) {int js0;i…