AQS(AbstractQueuedSynchronizer)抽象队列同步器及工作原理解析
作者:聪明不喝牛奶 发布时间:2023-02-24 22:20:09
前言
AQS 绝对是JUC的重要基石,也是面试中经常被问到的,所以我们要搞清楚这个AQS到底是什么?骑工作原理是什么?
AQS是什么?
AQS,AbstractQueuedSynchronizer,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock、ReentrantReadWriteLock、Semaphore等),JUC并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。它是JUC并发包中的核心基础组件,相比synchronized,synchronized缺少了获取锁与释放锁的可操作性,可中断、超时获取锁。
是用来构建锁或者其他同步器组件的重量级基础框架及整个JUC体系的基石
,通过内置的FIFO对列
来完成资源获取线程的排队工作,并通过一个int类型变量
表示持有锁的状态。
CLH队列
:CLH(Craig, Landin, and Hagersten)队列是一个虚拟的双向队列
(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系
)。AQS 是将每条请求共享资源的线程封装成一个 CLH 锁队列的一个结点(Node)来实现锁的分配。通过CAS完成对State值的修改。
同步对列的内部结构及继承关系
用银行办理业务的案例模拟AQS如何进行线程管理和通知机制
1、初始化的时候,state = 0 (0 表示没有人,1表示有人),线程池也有没有人在执行,现在就是没有顾客的时候,因此第一个线程去的时候都是公平锁状态直接到窗口办理业务。
2、现在有一个线程Thread A进来那么就直接到了业务窗口去办理,并且通过CAS将state的值变成1
。
3、此时有一个线程Thread B 进来,通过getStatue() 方法查看到state = 1
,此时ThreadA有在占用着,所以现在ThreadB线程就必须先入队等待ThreadA结束后再调用,但是由于现在队列是空的,所以ThreadB线程并不会马上进入到队列,他会先进入addWaiter()
方法到enq()
这个方法中去,这个方法实质上就是一个自旋锁,在这个方法中主要的时候先要实现一个队列的初始化
工作,先形成一个傀儡结点(哨兵结点)起到一个占位的作用
,然后才能将ThreadB线程挂在后面。
部分源码附上:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;// 将tail的前指针赋值过去
if (pred != null) { //一开始的时候并没有指向所以位null,因此条件不成立不走里面的语句
node.prev = pred;
if (compareAndSetTail(pred, node)) {//CAS
pred.next = node;
return node;
}
}
enq(node);//一开始会来走这个enq()这个方法。
return node;
}
因为当前的ThreadB线程进入时,tail的prev指针为null
所以就会进enq()方法
。如下:
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
4、ThreadC线程开始入队,那么这个就是跟ThreadB是一样的,只不过是说没有了初始化那步了
,直接挂在线程ThreadB上即可。
5、ThreadB 会去尝试acquireQueued()
这个方法,那么第一次
的时候会将哨兵结点的waitStatus = -1
; 然后继续自旋,进入到if条件的下一个条件中被调用park() 阻塞掉,等待着ThreadA 线程的unpark()。这样才算真正的入列等待了。
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) //第一次的时候会阻塞在前面的条件并将哨兵的waitState置为-1,第二次的话第一个条件为真,所以会走第二个判断条件,会将访问的线程给阻塞掉等待unpark();
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); //将访问次方法的线程给阻塞掉,等待unpark()方法唤醒
return Thread.interrupted();
}
6、当ThreadA线程办好业务的时候那么就会调用unlock()方法释放锁
,unlock() 方法中又会调用sync.release()方法
,并将当前窗口的线程变成null,state 置成0
,通过调用 unparkSuccessor()
方法将 傀儡结点的waitState = 0
。
private void unparkSuccessor(Node node) {
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread); //释放锁唤醒等待队列中的等待线程
}
7、接着会调用unpark()
方法将在前面等待的ThreadB线程给唤醒去窗口办业务。将head结点指向ThreadB结点,ThreadB的prev结点为null,next结点也为null,ThreadB变成空结点,
此节点就成了新的哨兵结点,原来的哨兵结点被GC回收。
8、ThreadC线程出列和上面的过程是一样的。
来源:https://blog.csdn.net/Ppphill_C/article/details/123719598


猜你喜欢
- 本文实例讲述了Android编程设计模式之中介者模式。分享给大家供大家参考,具体如下:一、介绍中介者模式(Mediator Pattern)
- 前言EasyCache升级兼容 Springboot2,有个业务系统启动总是会卡住,最后抛出超时异常,如下:java.util.concur
- mybatis在持久层框架中还是比较火的,一般项目都是基于ssm。虽然mybatis可以直接在xml中通过SQL语句操作数据库,很是灵活。但
- 最近看到一道有点意思的逻辑算法题,便着手实现一下。题目是要求打印 出N*N顺时针螺旋数组,规律如下:// 1 2 &
- 如何从C#获取字符串中汉字的个数?C#中使用正则表达式来从字符串中判断出汉字,然后计数,从而得到字符串中的汉字个数。先看这段代码://首先引
- 举例:存在一个类:Public Class Student{ public string name; public int age;}Stu
- 学习平台微软开发者博客:https://devblogs.microsoft.com/?WT.mc_id=DT-MVP-5003986微软文
- 1.后台参数校验Spring Validation验证框架对参数的验证机制提供了@Validated(Spring JSR-303规范,是标
- Spring开启注解AOP的支持放置的位置放在springmvc的aop,需要在springmvc的配置文件中写开启aop,而不是sprin
- 前言首先,事务这个概念是数据库层面的,Spring只是基于数据库中的事务进行扩展,以及提供了一些能让程序员更新方便操作事务的方式Spring
- 核心思想:“分”与“合”。主体流程先将一个序列分成很多个不能再分割的子序列,将各个子序列分别排序后再将子序列合并。其实就是重复两个步骤:【1
- 1.继承Thread类,重写run方法2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Threa
- 1 运算符1.1 概述运算符 用于连接 表达式 的 操作数,并对操作数执行运算。例如,表达式num1+num2,其操作数是num1和num2
- 本文为大家介绍了FTP上传下载队列窗口的实现方法,供大家参考,具体内容如下1、首先看一下队列窗口的界面2、看一下上传队列窗口的界面3、看一下
- 本文实例为大家分享了Android弹窗控件CustomFiltControl的使用方法,供大家参考,具体内容如下效果:起初踩的坑:&nbs
- 快速回顾1.Lambda表达式: (参数) -> {主体}Lambda表达式打开了函数式编程爱好者继续使用Java的大门。Lambda
- 问题在Android开发中,遇到一个问题,是ListView嵌套GridView,需要点击整个ListView的Item进行跳转。但是在点击
- 实例如下:public class DataTypeChangeHelper { /** * 将一个单字节的b
- 一、为基本数据类型起别名typedef int myint;myint x = 5;"myint"是"int&
- Spring MVC 请求处理流程用户发起请求,到 DispatcherServlet;然后到 HandlerMapping 返回处理器链(