当前位置: 首页 > news >正文

浅谈AQS

1.前言

AQS是AbstractQueuedSynchronizer(抽象同步队列)的简写,它是实现同步器的基础组件,并发包下的锁就是通过AQS实现的。作为开发者可能并不会直接用到AQS,但是知道其原理对于架构设计还是很有帮助的。

那为什么说浅谈呢,因为我也仅仅是根据书加上自己的想法来看AQS。

2.LockSupport工具类

在正式讲解AQS之前,我们需要先了解一下LockSupport类,他的主要作用就是用来阻塞唤醒线程。

LockSupport类与每个使用它的线程都会关联一个许可证,在默认情况下调用LockSupport类的方法的线程是不持有许可证的。LockSupport是通过Unsafe类实现的。接下来我们介绍一下LockSupport类的主要方法。

park()

前面我们已经说过,LockSupport类与每个使用它的线程都会关联一个许可证,如果调用park方法的线程持有许可证,那么就会立马返回,否则就会被阻塞挂起。

System.out.println("begin park!");
LockSupport.park();
System.out.println("end park!");

上述代码会在输出begin park之后进入阻塞状态。因为默认情况下是不具有许可证的。

只有在其他线程调用unpark(Thread t)方法并且将该线程作为参数,该线程才能返回。如果其他线程调用了该线程的interrupt()方法,设置了中断标志,或者线程被虚假唤醒(parkNanos方法可以指定阻塞一段时间后自己唤醒,所以可能会出现虚假唤醒!),那么该线程也会返回,所以调用park()方法的时候最好使用循环条件判断方式。

不过使用park阻塞的线程被其他线程中断返回时并不会抛出InterruptedException。

park(Object blocker)

public static void park(Object blocker) {// 获取调用线程Thread t = Thread.currentThread();// Thread类中有一个变量parkBlocker,用来存放park方法传递的blocker对象// 设置该线程的blocker变量setBlocker(t, blocker);// 挂起线程UNSAFE.park(false, 0L);// 线程被激活以后清楚blocker变量setBlocker(t, null);
}

当线程在没有持有许可证调用该方法时,会被阻塞挂起,同时blocker对象会被记录到线程内部。

我们可以使用LockSupport.park(this),这样当程序运行以后,我们使用jstack pid打印线程堆栈可以查看到具体是哪个类被阻塞了。

unpark(Thread thread)

当一个线程调用unpark的时候,如果作为参数的thread没有持有许可证,则会让线程持有。

如果thread之前因为调用park阻塞挂起,则调用unpark后会被唤醒。

如果thread之前还没有调用park,则在调用unpark以后,如果调用park则会立即返回。

3.AQS

初识AQS

请添加图片描述

由类图我们可以知道,AQS是一个FIFO双向队列,通过节点head和tail记录队首队尾元素。

Node

Node节点内部的SHARED用来标记该线程是在获取共享资源时被阻塞挂起放入AQS队列的,EXCLUSIVE用来标识该线程是获取独占资源时被阻塞挂起放入AQS队列的。

Node节点内部有一个成员变量waitStatus记录当前线程等待状态,可以为CANCELLED(线程被取消了)、SIGNAL(线程需要唤醒)、CONDITION(线程在条件队列里等待)、PROPAGATE(释放资源时需要通知其他节点)。

ConditionObject

ConditionObjectNode一样是AQS的内部类。它用来结合锁实现线程同步,其可以访问AQS的内部变量(state和AQS阻塞队列)。

ConditionObject是条件变量,每个条件变量对应一个条件队列,我们可以看到ConditionObject中有两个指针,分别指向条件队列的队尾和队头。条件队列用来存放调用条件变量的await方法后被阻塞的线程。

state

在AQS中,维护了一个单一变量state,对于不同的实现其有不同的意义:

  • 在ReentrantLock中,state表示重入式锁的可重入次数
  • 在ReentrantReadWriteLock中,state的高16位用于表示读锁的可获取次数,低16位用于表示写锁的可重入次数。(state的类型是int,占用4个字节)

Shared与Interruptibly

方法名中带有Shared的方法表示获取或释放共享资源,如acquireShared(int arg)

方法名中带有Interruptibly表示对中断做出响应,当线程调用了带Interruptibly的方法,如果这时被其他线程中断,那么就会抛出InterruptedException返回。

AQS工作流程

独占方式下

获取资源

首先使用tryAcquire尝试获取资源,获取成功直接返回,失败则将当前线程封装为EXCLUSIVE的节点插入到AQS阻塞队列尾部并使用LockSupport.park(this)挂起自己。

public final void acquire(int arg) {// tryAcquire的具体实现要由子类来完成,AQS中并不提供实现if (!tryAcquire(arg) &&// addWaiter的作用是将当前线程封装为独占类型的节点插入AQS阻塞队列,并且将该节点返回// acquireQueuedacquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

释放资源

当一个线程调用 release(int arg) 方法时会尝试使用 tryRelease 操作释放资源,这里是设置状态变量 state 的值,然后调用LockSupport.unpark(thread) 方法激活 AQS 队列里面被阻塞的一个线程 (thread)。被激活的线程则使用tryAcquire尝试,如果条件满足则激活继续向下运行,否则被放入AQS继续阻塞。

public final boolean release(int arg) {if (tryRelease(arg)) {// 释放资源成功则尝试激活线程Node h = head;if (h != null && h.waitStatus != 0)unparkSuccessor(h);return true;}return false;
}

共享方式下

获取资源

线程调用acquireShared(int arg)获取共享资源时,会首先使用tryAcquireShared尝试获取资源(修改state的值),成功直接返回,失败则将当前线程封装为Node.SHARED的Node节点插入到AQS阻塞队列的尾部。

public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);
}

释放资源

当一个线程调用 releaseShared(int arg) 时会尝试使用 tryReleaseShared 操作释放资 源,这里是设置状态变量 state 的值,然后使用 LockSupport.unpark(thread)激活 AQS 队 列里面被阻塞的一个线程 (thread)。跟独占模式下的流程差不多。

public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {doReleaseShared();return true;}return false;
}

注意

AQS类并没有提供tryAcquire、tryRelease、tryAcquireShare、tryReleaseShare方法,这些方法都需要子类实现。

同时如果你想使用AQS实现一个自己的锁,那你还需要根据情景重写isHeldExclusively方法,用来判断锁是被当前线程共享还是独占。

条件变量

条件变量拥有一个条件队列,条件队列要跟AQS阻塞队列区别开。

当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其 他线程会被转换为 Node 节点插入到 lock 锁对应的 AQS 阻塞队列里面,并做自旋 CAS 尝试获取锁。
如果获取到锁的线程又调用了对应的条件变量的 await() 方法,则该线程会释放获取 到的锁,并被转换为 Node 节点插入到条件变量对应的条件队列里面。

请添加图片描述

http://www.lryc.cn/news/8733.html

相关文章:

  • 关于服务连接器(Servlet)你了解多少?
  • 面对学员的投诉,中创教育是如何处理的?
  • 算法问题——排序算法问题
  • ArcGIS网络分析之构建网络分析数据集(一)
  • 微电影的行业痛点有哪些?
  • spark3.0源码分析-driver-executor心跳机制
  • 数据分析就要选择这款免费报表工具
  • node学习-3:服务器渲染和客户端渲染
  • LeetCode刷题笔记和周赛题解总目录
  • 用类比方式学习编程中函数递归(个人理解仅供参考)(内含汉诺塔问题的求解)
  • 【云原生之Docker实战】使用Docker部署Taskover开源个人任务管理工具
  • 5、SQL编程开发与注意事项
  • Allegro如何通过视图显示区分动态和静态铜皮操作指导
  • 测试开发之Django实战示例 第十一章 渲染和缓存课程内容
  • 90%企业在探索的敏捷开发怎么做?极狐GitLab总结了这些逻辑与流程
  • LeetCode-257. 二叉树的所有路径
  • 测试用例该怎么设计?—— 日常加更篇(下)
  • Java基础:接口
  • vuex基础入门:uniapp实现用户登录授权实战
  • Windows系统从权限维持角度进行应急响应
  • 什么是DNS解析?如何提升DNS解析安全?
  • 电路学习笔记
  • C# 数据结构
  • powerjob的worker启动,研究完了这块代码之后我发现了,代码就是现实中我们码农的真实写照
  • 配置Qt Creator
  • C++-类和对象(下)
  • 什么是仓库管理?
  • 对话系统学习概述(仅够参考)
  • 免费CRM客户管理系统真的存在吗?不仅有,还有5个!
  • C#开发的OpenRA使用自定义字典的比较函数