AQS的核心方法-acquire()解析
以下是AQS的acquire()方法源码:
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
一步步拆解,先看下tryAcquire()方法,这个方法是由AQS子类来做具体实现的,暂不关注,重点关注AQS的几个模版方法。
addWaiter()方法
先看addWaiter()方法,参数是一个Node对象,那么就先看下Node对象所对应的类是怎样的定义:
可以发现Node类是AQS类的一个内部类,有若干的方法和属性,这时候还无法知道这些方法和属性的作用是什么,所以继续跟进addWaiter()方法中
addWaiter()方法
先看看addWaiter()方法的源码:
private Node addWaiter(Node mode) {//构造一个Node对象,参数是当前线程对象以及mode对象,mode表示该节点的共享/排他性,值为null为排他模式,不为null则共享模式Node node = new Node(Thread.currentThread(), mode);//拿到AQS的尾节点 Node pred = tail;//如果尾节点不为空if (pred != null) {//先把新加入的节点的前驱节点设置为尾节点,新加入的节点会加入队列的尾部node.prev = pred;//通过CAS操作把新节点设置为尾节点,传入原来的尾节点pred和新节点node做判断,保证并发安全if (compareAndSetTail(pred, node)) {//把新节点设置为原来尾节点的后继节点pred.next = node;//返回新节点,这个节点里封装了当前的线程return node;}}//尾节点为null,则将新节点加入队列enq(node);return node;}
enq()方法
继续跟进enq()方法,看看它是怎么将节点加入队列的:
private Node enq(final Node node) {//这是一个死循环,不满足一定的条件就不会跳出循环for (;;) {//获取AQS尾节点Node t = tail;//如果为null,其实这是个循环判断,可能下次再做判断时,就有其他线程已经往队列中添加了节点,那么tail尾节点可能就不为空了,就走else逻辑了if (t == null) { // 必须初始化//则新建一个Node对象,通过CAS设置成头节点,这个head其实是冗余节点if (compareAndSetHead(new Node()))//把尾节点设置为headtail = head;} else {//尾节点不为空,则把尾节点设置为新节点的前驱节点node.prev = t;//做CAS操作,把新节点设置为尾节点if (compareAndSetTail(t, node)) {//CAS成功后,则把新节点设置为原来尾节点的后继节点t.next = node;//返回新节点,这个节点里封装了当前的线程return t;}}}}//这个方法关注一下,可能会豁然开朗,这就是当尾节点尾null时,需要设置头节点,来做个初始化的方法,把头节点从null设置为update对象private final boolean compareAndSetHead(Node update) {return unsafe.compareAndSwapObject(this, headOffset, null, update);}
所以enq()方法是很有意思的,它通过死循环的方式,来保证节点的正确添加,可以发现只有当新节点被设置为尾节点时,当前线程才能从enq()方法返回,然后再配合上CAS,是不是有一种很抽象的感觉,节点一个一个的被加到队列中,一个一个的接着被设置为尾节点,并发的操作,串行的感觉。
所以通过addWaiter()方法,竞争同步状态失败的线程就被成功的加入到对列的尾部了
acquireQueued()方法
再接着看看acquireQueued()方法:
final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {//获取node前继节点final Node p = node.predecessor();//如果node的前继节点是头节点,同时当前线程获取同步状态成功if (p == head && tryAcquire(arg)) {//那么把当前节点设置为头节点,同时把当前节点的前继节点置为nullsetHead(node);//再把前头节点p的后继节点设置为null,这样前头节点就没有任何引用了,帮助GC,清理前头节点p.next = null; // help GC//这里设置把标志位failed设为false,说明成功走到了这步逻辑failed = false;//要注意,无限循环只有这个出口,返回interrupted后,跳出循环,这个返回值就表示了要不要中断return interrupted;}//当node的前继节点不是头节点或者获取锁失败时,判断是否需要阻塞等待,如果需要等待,那么就调用parkAndCheckInterrupt()方法阻塞等待if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())//如果线程被中断的,那么重新设置中断状态为true,然后返回表示需要中断interrupted = true;}} finally {//如果出现不正常情况,failed标志位还没被置为false,就会取消if (failed)cancelAcquire(node);}}
shouldParkAfterFailedAcquire()方法
深入进去看看shouldParkAfterFailedAcquire()方法:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取前继节点状态int ws = pred.waitStatus;//如果等于SIGNAL,则直接返回true,表示要阻塞if (ws == Node.SIGNAL)//要注意只有这个分支会返回true return true;//如果状态大于0,表示前继节点需要做的请求被取消了, if (ws > 0) {//这个分支循环做一件事,把所有的被取消的前继节点移除,直到waitStatus值不再大于0,然后把这个没有被取消的节点和node节点连接起来do {node.prev = pred = pred.prev;//上述代码转换成如下模式可能会更好理解,其实就是反复取值赋值(node.prev = pred;pred = pred.prev;)} while (pred.waitStatus > 0);pred.next = node;} else {//通过CAS设置前置节点的状态为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}//返回falsereturn false;}
这里需要再回顾acquireQueued()方法,shouldParkAfterFailedAcquire()方法的结果一旦返回false,那么acquireQueued()方法的死循环就不会跳出,还是会继续检查node的前继节点是否是头节点,同时当前线程获取同步状态是否成功,而如果shouldParkAfterFailedAcquire()方法的结果是true,就会调用parkAndCheckInterrupt()方法:
private final boolean parkAndCheckInterrupt() {//LockSupport.park()实现阻塞等待,等着unpark和interrupt叫醒他LockSupport.park(this);//检查是否被中断,清除中断状态,并返回中断标志return Thread.interrupted();}
到这步可以发现,acquireQueued()方法的死循环逻辑配合上shouldParkAfterFailedAcquire()方法的去除取消节点和设置SIGNAL状态的操作,整个队列慢慢的会趋向于:
- 只要不是头节点,那么其他的节点都是返回true,表示需要中断,这都是shouldParkAfterFailedAcquire()方法的功劳
- 只要不是尾节点,那么其他的节点状态都是SIGNAL,因为只有节点状态是SIGNAL,才会返回true,这也是shouldParkAfterFailedAcquire()方法的功劳
最后如果acquireQueued()方法中出现了异常,则会调用cancelAcquire()方法来取消节点。
selfInterrupt()方法
最后再看看selfInterrupt()方法:
static void selfInterrupt() {Thread.currentThread().interrupt();}
这个线程没能获取到同步状态,同时acquireQueued()方法返回true,那么就会调用selfInterrupt()方法来设置当前线程的中断状态。
acquire()方法流程图
总结
看完源码分析,做到大致熟悉,然后以代码为切入口理解流程图即可。
AQS的核心方法-acquire()解析相关推荐
- Java Executor源码解析(3)—ThreadPoolExecutor线程池execute核心方法源码【一万字】
基于JDK1.8详细介绍了ThreadPoolExecutor线程池的execute方法源码! 上一篇文章中,我们介绍了:Java Executor源码解析(2)-ThreadPoolExecutor ...
- 【面试资料】 Java中高级核心面试知识解析
[面试资料] Java中高级核心面试知识解析 一.Java (一). 基础 (二). 容器 (三). 并发 (四). JVM 二.网络 (一). 计算机网络知识 (二). HTTPS中的TLS 三.L ...
- Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改
Spring Cloud Gateway-ServerWebExchange核心方法与请求或者响应内容的修改 前提 本文编写的时候使用的Spring Cloud Gateway版本为当时最新的版本Gr ...
- 深入OKHttp源码分析(二)----OkHttp任务调度核心类Dispatcher解析
OkHttp任务调度核心类Dispatcher解析 上一篇我们分析了okhttp的同步和异步请求的执行流程并进行了源码分析,深入OKHttp源码分析(一)----同步和异步请求流程和源码分析 那么今天 ...
- Java中高级核心知识全面解析——什么是Spring Cloud、需要掌握哪些知识点?(下)
目录 一.必不可少的 Hystrix 1.什么是 Hystrix之熔断和降级 2.什么是Hystrix之其他 二.微服务网关--Zuul 1.Zuul 的路由功能 1)简单配置 2)统一前缀 3)路由 ...
- Android Framework 电源子系统(05)核心方法updatePowerStateLocked分析-3 更新屏保 发送通知 更新wakelock
该系列文章总纲链接:专题分纲目录 Android Framework 电源子系统 本章关键点总结 & 说明: 本章节主要关注➕ updatePowerStateLocked 方法中 更新屏保 ...
- DophinScheduler ui部分 核心代码详细解析——重中之重的src文件夹里究竟有何种玄机
2021SC@SDUSC 文章目录 一.整体结构 二.具体细节 1.components 2.images 3.js 1.dag-canvas 2.contextMenu 3.nodeStatus.j ...
- DophinScheduler server部分 核心代码详细解析——掌控任务和进程的呼吸与脉搏:log、monitor与registry
2021SC@SDUSC 文章目录 一.整体结构 二.具体分析 1.log 1.LoggerRequestProcessor 2.LoggerServer 3.MasterLogFilter 2.mo ...
- DophinScheduler server部分 核心代码详细解析——统领全局调度全场的服务器server部分究竟干了什么?
2021SC@SDUSC 文章目录 一.整体结构 二.细节分析 1.builder 2.entity 1.DataxTaskExecutionContext 2.DependenceTaskExecu ...
最新文章
- [LeetCode] Factorial Trailing Zeroes
- python 同步 事件 event 简介
- ReentrantLock可以是公平锁,sync只能是非公平锁。
- 扶凯:海量视频和用户时代的CDN
- 《spring-boot学习》-10-RabbitMQ
- python3 txt 读写_Python3 读写txt文件
- android gson解析json数据,Android中使用Gson解析JSON数据的两种方法
- linux 无盘 重新生成内核,Linux无盘系统_尐惢
- IE浏览器连不上网电脑无法联网
- mac下应该是一样的,ln命令
- MySQL InnoDB聚簇索引和普通索引浅析
- vscode中用快捷键 Alt + Shift + F 格式化代码不生效的问题
- [Revit教程]斑马:分享一个用Revit自适应构件做安全疏散距离分析的方法#S007
- lumia 525 android 7.1,给大神跪了!诺基亚Lumia 520成功刷上安卓7.1
- Json-server简单实现mock数据
- ICESat2数据hd5文件的预处理
- 上高职业技术学校计算机专科,上高职业技术学校
- 【C#学习】单个问号的作用
- Java实现输出1-100之间的完全数,输出100以内的偶数和这两个程序
- 把周杰伦的脸放进漫画——MangaGAN人脸照片生成漫画论文解析