【后端面经-Java】AQS详解

news/2024/11/29 13:41:47/

【后端面经-Java】AQS详解

    • 1. AQS是什么?
    • 2. AQS核心思想
      • 2.1 基本框架
        • 2.1.1 资源state
        • 2.1.2 CLH双向队列
      • 2.2 AQS模板
    • 3. 源码分析
      • 3.1 acquire(int)
        • 3.1.1 tryAcquire(int)
        • 3.1.2 addWaiter(Node.EXCLUSIVE)
        • 3.1.3 acquireQueued(Node node, int arg)
      • 3.2 release(int)
        • 3.2.1 tryRelease(int)
        • 3.2.2 unparkSuccessor(h)
      • 3.3 acquireShared(int)和releaseShared(int)
        • 3.3.1 acquireShared(int)
        • 3.3.2 releaseShared(int)
    • 4. 面试问题模拟
    • 参考资料

1. AQS是什么?

AQS定义了一套多线程访问共享资源的同步器框架,许多同步类实现都依赖于它,如常用的ReentrantLock。
简单来说,AQS定义了一套框架,来实现同步类

2. AQS核心思想

2.1 基本框架

AQS的核心思想是对于共享资源,维护一个双端队列来管理线程,队列中的线程依次获取资源,获取不到的线程进入队列等待,直到资源释放,队列中的线程依次获取资源。
AQS的基本框架如图所示:
在这里插入图片描述

2.1.1 资源state

state变量表示共享资源,通常是int类型。

  1. 访问方法
    state类型用户无法直接进行修改,而需要借助于AQS提供的方法进行修改,即getState()setState()compareAndSetState()等。
  2. 访问类型
    AQS定义了两种资源访问类型:
    • 独占(Exclusive):一个时间点资源只能由一个线程占用;
    • 共享(Share):一个时间点资源可以被多个线程共用。

2.1.2 CLH双向队列

CLH队列是一种基于逻辑队列非线程饥饿的自旋公平锁,具体介绍可参考此篇博客。CLH中每个节点都表示一个线程,处于头部的节点获取资源,而其他资源则等待。

  1. 节点结构
    Node类源码如下所示:
static final class Node {// 模式,分为共享与独占// 共享模式static final Node SHARED = new Node();// 独占模式static final Node EXCLUSIVE = null;        // 结点状态// CANCELLED,值为1,表示当前的线程被取消// SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark// CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中// PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行// 值为0,表示当前节点在sync队列中,等待着获取锁static final int CANCELLED =  1;static final int SIGNAL    = -1;static final int CONDITION = -2;static final int PROPAGATE = -3;        // 结点状态volatile int waitStatus;        // 前驱结点volatile Node prev;    // 后继结点volatile Node next;        // 结点所对应的线程volatile Thread thread;        // 下一个等待者Node nextWaiter;// 结点是否在共享模式下等待final boolean isShared() {return nextWaiter == SHARED;}// 获取前驱结点,若前驱结点为空,抛出异常final Node predecessor() throws NullPointerException {// 保存前驱结点Node p = prev; if (p == null) // 前驱结点为空,抛出异常throw new NullPointerException();else // 前驱结点不为空,返回return p;}// 无参构造方法Node() {    // Used to establish initial head or SHARED marker}// 构造方法Node(Thread thread, Node mode) {    // Used by addWaiterthis.nextWaiter = mode;this.thread = thread;}// 构造方法Node(Thread thread, int waitStatus) { // Used by Conditionthis.waitStatus = waitStatus;this.thread = thread;}
}

Node的方法和属性值如图所示:
在这里插入图片描述

其中,

  • waitStatus表示当前节点在队列中的状态;
  • thread表示当前节点表示的线程;
  • prevnext分别表示当前节点的前驱节点和后继节点;
  • nextWaiterd当存在CONDTION队列时,表示一个condition状态的后继节点。
  1. waitStatus
    结点的等待状态是一个整数值,具体的参数值和含义如下所示:
  • 1-CANCELLED,表示节点获取锁的请求被取消,此时节点不再请求资源;
  • 0,是节点初始化的默认值;
  • -1-SIGNAL,表示线程做好准备,等待资源释放;
  • -2-CONDITION,表示节点在condition等待队列中,等待被唤醒而进入同步队列;
  • -3-PROPAGATE,当前线程处于共享模式下的时候会使用该字段。

2.2 AQS模板

AQS提供一系列结构,作为一个完整的模板,自定义的同步器只需要实现资源的获取和释放就可以,而不需要考虑底层的队列修改、状态改变等逻辑。
使用AQS实现一个自定义同步器,需要实现的方法:

  • isHeldExclusively():该线程是否独占资源,在使用到condition的时候会实现这一方法;
  • tryAcquire(int):独占模式获取资源的方式,成功获取返回true,否则返回false;
  • tryRelease(int):独占模式释放资源的方式,成功获取返回true,否则返回false;
  • tryAcquireShared(int):共享模式获取资源的方式,成功获取返回true,否则返回false;
  • tryReleaseShared(int):共享模式释放资源的方式,成功获取返回true,否则返回false;

一般来说,一个同步器是资源独占模式或者资源共享模式的其中之一,因此tryAcquire(int)tryAcquireShared(int)只需要实现一个即可,tryRelease(int)tryReleaseShared(int)同理。
但是同步器也可以实现两种模式的资源获取和释放,从而实现独占和共享两种模式。

3. 源码分析

3.1 acquire(int)

acquire(int)是获取源码部分的顶层入口,源码如下所示:

public final void acquire(int arg) {if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();
}

这段代码展现的资源获取流程如下:

  • tryAcquire()尝试直接去获取资源;获取成功则直接返回
  • 如果获取失败,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  • acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。

简单总结就是:

  • 获取资源;
  • 失败就排队;
  • 排队要等待。

从上文的描述可见重要的方法有三个:tryAquire()addWaiter()acquireQueued()。下面将逐个分析其源码:

3.1.1 tryAcquire(int)

tryAcquire(int)是获取资源的方法,源码如下所示:

protected boolean tryAcquire(int arg) {throw new UnsupportedOperationException();
}

该方法是一个空方法,需要自定义同步器实现,因此在使用AQS实现同步器时,需要重写该方法。这也是“自定义的同步器只需要实现资源的获取和释放就可以”的体现。

3.1.2 addWaiter(Node.EXCLUSIVE)

addWaiter(Node.EXCLUSIVE)是将线程加入等待队列的尾部,源码如下所示:

private Node addWaiter(Node mode) {//以给定模式构造结点。mode有两种:EXCLUSIVE(独占)和SHARED(共享)//aquire()方法是独占模式,因此直接使用Exclusive参数。Node node = new Node(Thread.currentThread(), mode);//尝试快速方式直接放到队尾。Node pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}//上一步失败则通过enq入队。enq(node);return node;
}

首先,使用模式将当前线程构造为一个节点,然后尝试将该节点放入队尾,如果成功则返回,否则调用enq(node)将节点放入队尾,最终返回当前节点的位置指针。
其中,enq(node)方法是将节点加入队列的方法,源码如下所示:

private Node enq(final Node node) {for (;;) { // 无限循环,确保结点能够成功入队列// 保存尾结点Node t = tail;if (t == null) { // 尾结点为空,即还没被初始化if (compareAndSetHead(new Node())) // 头节点为空,并设置头节点为新生成的结点tail = head; // 头节点与尾结点都指向同一个新生结点} else { // 尾结点不为空,即已经被初始化过// 将node结点的prev域连接到尾结点node.prev = t; if (compareAndSetTail(t, node)) { // 比较结点t是否为尾结点,若是则将尾结点设置为node// 设置尾结点的next域为nodet.next = node; return t; // 返回尾结点}}}
}

3.1.3 acquireQueued(Node node, int arg)

这部分源码是将线程阻塞在等待队列中,线程处于等待状态,直到获取到资源后才返回,源码如下所示:

// sync队列中的结点在独占且忽略中断的模式下获取(资源)
final boolean acquireQueued(final Node node, int arg) {// 标志boolean failed = true;try {// 中断标志boolean interrupted = false;for (;;) { // 无限循环// 获取node节点的前驱结点final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { // 前驱为头节点并且成功获得锁setHead(node); // 设置头节点p.next = null; // help GCfailed = false; // 设置标志return interrupted; }if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())////shouldParkAfterFailedAcquire只有当该节点的前驱结点的状态为SIGNAL时,才可以对该结点所封装的线程进行park操作。否则,将不能进行park操作。//parkAndCheckInterrupt首先执行park操作,即禁用当前线程,然后返回该线程是否已经被中断interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

acquireQueued(Node node, int arg)方法的主要逻辑如下:

  • 获取node节点的前驱结点,判断前驱节点是不是头部节点head,有没有成功获取资源。
  • 如果前驱结点是头部节点head并且获取了资源,说明自己应该被唤醒,设置该节点为head节点等待下一个获得资源;
  • 如果前驱节点不是头部节点或者没有获取资源,则判断是否需要park当前线程,
    • 判断前驱节点状态是不是SIGNAL,是的话则park当前节点,否则不执行park操作;
  • park当前节点之后,当前节点进入等待状态,等待被其他节点unpark操作唤醒。然后重复此逻辑步骤。

3.2 release(int)

release(int)是释放资源的顶层入口方法,源码如下所示:

public final boolean release(int arg) {if (tryRelease(arg)) { // 释放成功// 保存头节点Node h = head; if (h != null && h.waitStatus != 0) // 头节点不为空并且头节点状态不为0unparkSuccessor(h); //释放头节点的后继结点return true;}return false;
}

release(int)方法的主要逻辑如下:

  • 尝试释放资源,如果释放成功则返回true,否则返回false
  • 释放成功之后,需要调用unparkSuccessor(h)唤醒后继节点。

下面介绍两个重要的源码函数:tryRelease(int)unparkSuccessor(h)

3.2.1 tryRelease(int)

tryRelease(int)是释放资源的方法,源码如下所示:

protected boolean tryRelease(int arg) {throw new UnsupportedOperationException();
}

这部分是需要自定义同步器自己实现的,要注意的是返回值需要为boolean类型,表示释放资源是否成功。

3.2.2 unparkSuccessor(h)

unparkSuccessor(h)是唤醒后继节点的方法,源码如下所示:

private void unparkSuccessor(Node node) {//这里,node一般为当前线程所在的结点。int ws = node.waitStatus;if (ws < 0)//置零当前线程所在的结点状态,允许失败。compareAndSetWaitStatus(node, ws, 0);Node s = node.next;//找到下一个需要唤醒的结点sif (s == null || s.waitStatus > 0) {//如果为空或已取消s = null;for (Node t = tail; t != null && t != node; t = t.prev) // 从后向前找。if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。s = t;}if (s != null)LockSupport.unpark(s.thread);//唤醒
}

这部分主要是查找第一个还处于等待状态的节点,将其唤醒;
查找顺序是从后往前找,这是因为CLH队列中的prev链是强一致的,从后往前找更加安全,而next链因为addWaiter()方法和cancelAcquire()方法的存在,不是强一致的,因此从前往后找可能会出现问题。这部分的具体解释可以参考参考文献-1

3.3 acquireShared(int)和releaseShared(int)

3.3.1 acquireShared(int)

是使用共享模式获取共享资源的顶层入口方法,源码如下所示:

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

流程如下:

  • 通过tryAcquireShared(arg)尝试获取资源,如果获取成功则直接返回;
  • 如果获取资源失败,则调用doAcquireShared(arg)将线程阻塞在等待队列中,直到被unpark()/interrupt()并成功获取到资源才返回。

其中,tryAcquireShared(arg)是获取共享资源的方法,也是需要用户自己实现。

doAcquireShared(arg)是将线程阻塞在等待队列中,直到获取到资源后才返回,具体流程和acquireQueued()方法类似,
源码如下所示:

private void doAcquireShared(int arg) {final Node node = addWaiter(Node.SHARED);//加入队列尾部boolean failed = true;//是否成功标志try {boolean interrupted = false;//等待过程中是否被中断过的标志for (;;) {final Node p = node.predecessor();//前驱if (p == head) {//如果到head的下一个,因为head是拿到资源的线程,此时node被唤醒,很可能是head用完资源来唤醒自己的int r = tryAcquireShared(arg);//尝试获取资源if (r >= 0) {//成功setHeadAndPropagate(node, r);//将head指向自己,还有剩余资源可以再唤醒之后的线程p.next = null; // help GCif (interrupted)//如果等待过程中被打断过,此时将中断补上。selfInterrupt();failed = false;return;}}//判断状态,寻找安全点,进入waiting状态,等着被unpark()或interrupt()if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}
}

3.3.2 releaseShared(int)

releaseShared(int)是释放共享资源的顶层入口方法,源码如下所示:

public final boolean releaseShared(int arg) {if (tryReleaseShared(arg)) {//尝试释放资源doReleaseShared();//唤醒后继结点return true;}return false;
}

流程如下:

  • 使用tryReleaseShared(arg)尝试释放资源,如果释放成功则返回true,否则返回false;
  • 如果释放成功,则调用doReleaseShared()唤醒后继节点。

下面介绍一下doReleaseShared()方法,源码如下所示:

private void doReleaseShared() {for (;;) {Node h = head;if (h != null && h != tail) {int ws = h.waitStatus;if (ws == Node.SIGNAL) {if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))continue;unparkSuccessor(h);//唤醒后继}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue;}if (h == head)// head发生变化break;}
}

4. 面试问题模拟

Q:AQS是接口吗?有哪些没有实现的方法?看过相关源码吗?

AQS定义了一个实现同步类的框架,实现方法主要有tryAquiretryRelease,表示独占模式的资源获取和释放,tryAquireSharedtryReleaseShared表示共享模式的资源获取和释放。
源码分析如上文所述。

参考资料

  1. Java并发之AQS详解
  2. JUC锁: 锁核心类AQS详解
  3. 从ReentrantLock的实现看AQS的原理及应用

http://www.ppmy.cn/news/691897.html

相关文章

2023 中兴捧月算法挑战赛-自智网络-参赛总结

“中兴捧月”是由中兴通讯面向在校大学生举办的全球性系列赛事活动&#xff0c;致力于培养学生建模编程、创新、方案策划和团队合作能力。今年是在学校的宣传下了解到比赛&#xff0c;最初抱着学习的态度报名了比赛&#xff0c;最终进入了决赛&#xff0c;完成了封闭的开发与赛…

unity中单位是米还是厘米_cm在单位里是厘米还是毫米

展开全部 MM是代表毫米&#xff0c;cm是厘米。 1毫米0.1厘米&#xff1b;1mm0.1cm0.01dm0.001m0.000001km1 000μm1 000 000nm 例如&#xff1a;500毫米(mm)50厘米(cm) 解答e68a84e8a2ad62616964757a686964616f31333431363038过程如下&#xff1a; 厘米(cm)和毫米(mm)都是长度单…

python长度转换代码尺和米_尺,寸,跟米,厘米的换算??

展开全部 目前: 1丈=10尺, 1尺=10寸, 1寸=10分(1尺=33.33厘米) 度--即长度,由于和生活密切相关,自人类有始就出现了,原32313133353236313431303231363533e58685e5aeb931333365643662始人布指为寸,布掌为尺,舒肘为丈,到秦始皇统一度量衡,直至今天的现代计量技术的出现,…

java 米与厘米 转换_米转码换算(米与码的换算关系)

如果你说的这个--米是长度,还要知道宽度--米 两者相乘,才能得出面积--平方米 已知1米等于三尺,那么0.96米等于多少尺?请说出计算公式 1m=3c,0.96m=0.96*1m=0.96*3c=2.88c,即2.88尺~ 1米≈1.0936码1码≈0.9144米 百度有专门的工具的,好多度量衡都能直接转换,比如你直接在…

c语言厘米换算分米程序设计,厘米和分米换算(米和厘米换算)

1米10分米100厘米1000毫米1分米0.1米10厘米100毫米1厘米0.01米0.1分米10毫米1毫米0.001米0.01分米0.1厘米 米和分米和厘米的换算如下&#xff1a; 1米10分米100厘米&#xff0c;1分米0.1米10厘米&#xff0c;1厘米0.1分米0.01厘米 一分米等于10厘米 1米10分米1分米10厘米1厘米1…

关于JSON字符串

常用的JSON 字符串的 {} 外面一般没有加双引号是因为在某些上下文中&#xff0c;例如在传输数据或在代码中嵌入 JSON 字符串时&#xff0c;通常不需要额外的双引号来包围整个 JSON 字符串。 当我们将 JSON 字符串作为数据进行传输时&#xff0c;例如通过网络发送给服务器或在前…

wlan连接的笔记本电脑+开启移动热点+手机无法连接【已解决】

前言&#xff1a; 首先我的问题是&#xff1a; Win10系统&#xff0c;开启移动热点后在手机界面可以搜索到热点&#xff0c;但就是连接不上&#xff01;不提示”拒绝接入“&#xff0c;不提示密码错误&#xff01; 注释&#xff1a; 在尝试各种方法时&#xff0c;涉及到更改…

一文正确理解 分层架构系统 的接入层设计,以及接入层设计常见的问题和解决方案(雪崩、降级、限流、熔断)

分层架构系统之接入层 分布式架构设计之接入层1、定义2、优势3、技术方案3.1、考虑的问题&#xff08;负载均衡和高可用&#xff09;3.2、设计方式3.2.1、单个IP地址接入3.2.2、多个IP地址随机接入3.2.2、单IP地址 反向代理3.2.3、反向代理 高可用方案&#xff08;keepalived&a…