上期的《全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础》中介绍了什么是AQS,以及AQS的基本结构。有了这些概念做铺垫之后,我们就可以正式地看看AQS是如何通过state(以下也称资源)和同步队列,实现线程之间的同步功能了

那么线程之间是如何同步呢?其实就是通过资源的获取和释放来进行同步。如果获取到就继续运行,获取不到就放入同步队列阻塞等待,释放就是交出获得的资源,并释放同步队列中需要被唤醒的线程。对,就是这么简单!

本篇我们继续深入AQS内部,一起来看看线程是怎么利用AQS来获取、释放资源的~

获取资源

AQS获取资源是通过各种acquire方法。不同acquire方法之间存在区别,如下:

  • acquire:以互斥模式获取资源,忽略中断
  • acquireInterruptibly:以互斥模式获取资源,响应中断
  • acquireShared:以共享模式获取资源,忽略中断
  • acquireSharedInterruptibly:以共享模式获取资源,响应中断

获取互斥资源

忽略中断的acquire方法

acquire方法是获取互斥资源,忽略中断。如果获取成功,直接返回,否则该线程会进入同步队列阻塞等待。源码如下:

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

acquire是一个模板方法,定义为final方法防止子类重写。其中的钩子方法tryAcquire需要子类去实现。

如果tryAcquire返回true,说明尝试获取成功,直接返回即可。如果tryAcquire返回false,说明尝试获取失败,会调用addWaiter方法进入等待队列。该方法的解析见上一篇博客《全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础》。当该线程处于同步队列中(queued),就会调用acquireQueued方法

acquireQueued方法为一个已经位于同步队列的线程,以互斥模式获取资源,不响应中断但是会记录中断状态。源码如下:

final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor(); // 获取node的前一个节点
if (p == head && tryAcquire(arg)) { // 如果p是head,说明node是队列头,可以竞争资源
setHead(node); // 将node出队
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

在acquireQueued方法代码主要都包含在一个for循环中。如果发现node是队首节点,就会再次尝试获取资源。如果此时获取成功,就直接出队并返回,不用阻塞等待,这里体现了同步队列先进先出的特点

如果不是队首节点,或者是再次尝试获取资源又双叒叕失败了,则调用shouldParkAfterFailedAcquire方法判断当前线程是否应该被阻塞(在这里打一个断点)

shouldParkAfterFailedAcquire方法会检查当前线程是否应该被阻塞,如果是就返回true,否则返回false。其源码如下:

// 调用此方法必须保证pred是node的直接前驱,即node.prev == pred
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) {
// 如果前面的Node都被cancel了,那么就跳过这些Node
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.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}

只有当node的直接前驱节点状态是SIGNAL时,才会认为该线程应该被阻塞。否则还需要回到acquireQueued的for循环中重新检查,不会立即阻塞

我画了一张shouldParkAfterFailedAcquire的执行流程图,如下:

那么会不会有一种可能:shouldParkAfterFailedAcquire方法一直返回false,始终认为该线程不应该阻塞,那么该线程就会一直占用CPU资源,“忙等”

其实一般来说是不会的,原因见上面示意图中的紫色文字部分

再回到acquireQueued方法中,如果shouldParkAfterFailedAcquire判断该线程,并返回了true,就需要执行parkAndCheckInterrupt将该线程阻塞,源码如下:

private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}

在parkAndCheckInterrupt中借助了工具类LockSuppport将线程阻塞。阻塞过程中如果该线程被设置了中断状态,虽然中断不会导致阻塞立即被唤醒,但是线程的中断状态会被记录下来,并作为该方法的返回值

总体来说,acquireQueued方法的执行流程如下图所示:

再回到acquire方法中。如果acquire失败而阻塞等待的过程中被中断,那么等它被唤醒并成功获得资源之后,会立即调用setInterrupt方法设置线程的中断状态。setInterrupt的源码如下:

static void selfInterrupt() {
Thread.currentThread().interrupt();
}

最后补充一点,acquire方法除了会在线程获取互斥资源时被调用,也会被条件等待方法await方法调用,具体分析见本系列最后一期博客《全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(三)条件变量》

响应中断的acquireInterruptibly方法

acquireInterruptibly用于获取互斥资源。顾名思义,这个方法响应中断,即如果在调用过程中发生了中断,会抛出中断异常,中止资源的获取。其源码如下:

public final void acquireInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}

acquireInterruptibly方法首先会检查中断状态,如果没有发生中断,才会继续向下执行,否则抛出中断异常

接下来执行钩子方法tryAcquire,如果获取成功则直接返回,否则获取失败,执行doAcquireInterruptibly方法:

private void doAcquireInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

doAcquireInterruptibly会先调用addWaiter方法,将当前线程加入队尾。之后的逻辑和acquireQueued类似,就是在for循环中,先判断当前节点是否是头节点,如果是则再次尝试获取资源。如果不是队首或者获取失败,则调用shouldParkAfterFailedAcquire方法判断该线程是否应该被阻塞。如果不是就进入下一轮循环。如果需要被阻塞,则调用parkAndCheckInterrupt方法将其阻塞。如果阻塞过程中发生中断,则当该线程被唤醒后回到doAcquireInterruptibly中,会抛出中断异常,并调用cancelAcquire执行取消节点的逻辑

doAcquireInterruptibly和acquireQueued的区别有两点:

  • acquireQueued调用之前,当前线程就已经被放入同步队列;而doAcquireInterruptibly没有,需要自己调用addWaiter方法
  • acquireQueued中不会因发生中断而抛出中断异常、取消节点,只会记录是否发生中断并返回;而doAcquireInterruptibly会响应中断,抛出中断异常,并取消该线程对应的节点
作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15674098.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

获取共享资源

忽略中断的acquireShared方法

acquireShared是以共享模式获取资源,并且忽略中断。源码如下:

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

该方法首先会调用钩子方法tryAcquireShared尝试获取共享资源,如果获取成功则直接返回,否则获取失败,调用doAcquireShared方法:

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) {
int r = tryAcquireShared(arg);
if (r >= 0) { // 表示tryAcquireShared获取成功 // 设置head,并判断是否需要唤醒后继线程。如果需要则唤醒,并保证传播
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}

这里也会调用addWaiter将当前线程加入同步队列,不过这里的Node是共享模式(Node.SHARED)

在接下来的for循环中,如果当前线程位于队首,则再次尝试获取资源。如果获取成功,则调用setHeadAndPropagate方法,处理中断之后返回

其中setHeadAndPropagate方法的作用是弹出队头,并检测其后继节点是否需要被唤醒,如果需要的话就唤醒,并确保传播。源码如下;

private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
// 这个复杂的if条件判断就是用于判断:后继节点的线程是否要被唤醒
// propagate > 0 表示允许后续节点继续获取共享资源
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared(); // 唤醒后继的共享模式的线程,并确保状态传播下去
}
}

在共享模式下,一个线程获取资源成功后,可能会引起后继等待获取共享资源的线程。注意,这里是后继而非同步队列中所有后面的。在这一点上,不同于互斥资源的获取,共享资源的获取更像是一人得道,鸡犬升天

如果在setHeadAndPropagate中发现后继有线程需要被释放,则调用doReleaseShared方法将它释放,并确保传播,它也是releaseShared方法的核心,该方法会在后面讲解释放共享资源时给出解析,这里暂时不分析

确保传播的含义:

保证被唤醒的线程可以继续唤醒它的后继线程。如果每个线程都能确保传播,那么所有应该被释放的后继线程都能得到释放

总的来说,acquireShared的流程与acquire基本一致,最大的区别在于:获取共享资源成功后,可能需要唤醒后继的多个线程。而获取互斥资源成功后,不需要唤醒其他任何线程

响应中断的acquireSharedInterruptibly方法

acquireSharedInterruptibly方法用于获取共享资源,但是该方法会响应中断,即在获取过程中接收到中断信号,会抛出中断异常。其源码如下:

public final void acquireSharedInterruptibly(int arg) throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}

和acquireInterruptibly一样,acquireSharedInterruptibly也会先检查线程的中断状态是否已经被设置。如果设置则直接抛出中断异常

接下来会调用钩子方法tryAcquireShared尝试获取共享资源,获取成功则直接返回,获取失败就会调用doAcquireSharedInterruptibly方法:

private void doAcquireSharedInterruptibly(int arg) throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

不多解释,直接上图吧!下面是doAcquireSharedInterruptibly方法的执行流程图:

doAcquireSharedInterruptibly方法和doAcquireShared方法大体上差不多,区别仅在于前者响应中断并会抛出中断异常,而后者忽略中断,只记录中断状态并返回

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15674098.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

释放资源

AQS释放资源是通过各种release方法。不同release之间存在区别,如下:

  • release:以独占模式释放对象
  • releaseShared:以共享模式释放对象

release不存在响应中断的区别,都是忽略中断的,因为线程在释放资源的时候被中断可能引起意外的错误

释放互斥资源

AQS使用release方法释放互斥资源,源码如下:

public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}

该方法会先调用钩子方法tryRelease,如果释放失败则直接返回false,如果释放成功,则调用unparkSuccessor方法唤醒队首线程,并返回true

unparkSuccessor方法是唤醒线程的主要逻辑。源码如下:

private void unparkSuccessor(Node node) {

    // 如果status < 0(表明可能需要signal),先清除状态(设为0)
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); // 一般来说后继需要unpark的节点就是next节点
// 但是如果next被cancel或为null,则需要从后向前遍历,直到找到有效的后继节点
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);
}

该方法的作用是唤醒node的有效后继节点。有效指的是跳过那些被cancel的节点。 由于同步队列是FIFO的,所以node一定是head

作者:酒冽        出处:https://www.cnblogs.com/frankiedyz/p/15674098.html

版权:本文版权归作者和博客园共有

转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文连接;否则必究法律责任

释放共享资源

releaseShared用于释放共享资源,源码如下:

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

该方法首先调用钩子方法tryReleaseShared尝试释放资源,如果失败则直接返回false,如果成功则执行doReleaseShared方法唤醒后继的其他共享模式线程同时确保传播,最后返回true

doReleaseShared方法在前面的acquireShared -> setHeadAndPropagate中出现过,该方法的作用是在共享模式下唤醒后继线程,并确保传播。其源码如下:

private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 如果需要,则唤醒后继线程,同时设置waitStatus为0
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h); // 唤醒后继线程
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) // 设置PROPAGATE状态,保证唤醒可以传播下去
continue; // loop on failed CAS
}
// 如果上述的执行过程没有被别的线程打扰,那就退出,否则重新loop
if (h == head) // loop if head changed
break;
}
}

最后来做个总结:

AQS针对互斥资源、共享资源的获取和释放,提供了不同的方法。而获取资源的方法也可以分为响应中断和忽略中断,释放资源都是忽略中断的

AQS正是通过资源 (state)的释放和获取,配合同步队列让线程排队等待,以FIFO的方式让竞争资源失败的线程阻塞、唤醒

这些释放、获取方法都是AQS提供给子类去调用的模板方法,其中的一些关键步骤均设计为了钩子方法,让子类可以个性化定制

正是有了AQS这个强大的后盾,才能诞生出那么多实用的并发同步工具类。不得不说,AQS是真的牛啊

好了,能看到这里的读者,相信已经掌握了AQS的基本结构,以及AQS是获取、释放资源的原理

我这里其实并没有剖析所有AQS提供的资源获取方法,还有两个可超时方法tryAcquireNanos、tryAcquireSharedNanos没有讲解,但是基本上和其他获取资源方法是类似的,只是多了一个超时而取消的逻辑,感兴趣的读者可以打开AQS源码自己分析

接下来的就是AQS的最后一篇了,我们来看看AQS里面的条件队列是怎么实现的

全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(二)资源的获取和释放的更多相关文章

  1. 全网最详细的AbstractQueuedSynchronizer(AQS)源码剖析(一)AQS基础

    AbstractQueuedSynchronizer(以下简称AQS)的内容确实有点多,博主考虑再三,还是决定把它拆成三期.原因有三,一是放入同一篇博客势必影响阅读体验,而是为了表达对这个伟大基础并发 ...

  2. Qt信号槽源码剖析(二)

    大家好,我是IT文艺男,来自一线大厂的一线程序员 上节视频给大家讲解了Qt信号槽的基本概念.元对象编译器.示例代码以及Qt宏:今天接着深入分析,进入Qt信号槽源码剖析系列的第二节视频. Qt信号槽的宏 ...

  3. Django Rest Framework源码剖析(二)-----权限

    一.简介 在上一篇博客中已经介绍了django rest framework 对于认证的源码流程,以及实现过程,当用户经过认证之后下一步就是涉及到权限的问题.比如订单的业务只能VIP才能查看,所以这时 ...

  4. AbstractQueuedSynchronizer(AQS)源码解析

          关于AQS的源码解析,本来是没有打算特意写一篇文章来介绍的.不过在写本学期课程作业中,有一门写了关于AQS的,而且也画了一些相关的图,所以直接拿过来分享一下,如有错误欢迎指正.       ...

  5. AbstractQueuedSynchronizer AQS源码分析

    申明:jdk版本为1.8 AbstractQueuedSynchronizer是jdk中实现锁的一个抽象类,有排他和共享两种模式. 我们这里先看排他模式,共享模式后面结合java.util.concu ...

  6. JDK源码之AQS源码剖析

    除特别注明外,本站所有文章均为原创,转载请注明地址 AbstractQueuedSynchronizer(AQS)是JDK中实现并发编程的核心,平时我们工作中经常用到的ReentrantLock,Co ...

  7. 并发编程之 AQS 源码剖析

    前言 JDK 1.5 的 java.util.concurrent.locks 包中都是锁,其中有一个抽象类 AbstractQueuedSynchronizer (抽象队列同步器),也就是 AQS, ...

  8. 图解源码之java锁的获取和释放(AQS)篇

    以独占式不公平锁为例,通过5个线程争夺ReentrantLock的过程,图解ReentrantLock源码实现,了解显示锁的工作流程. 任何时刻拿到锁的只有一个线程,未拿到锁的线程会打包成节点(nod ...

  9. jdk源码剖析二: 对象内存布局、synchronized终极原理

    很多人一提到锁,自然第一个想到了synchronized,但一直不懂源码实现,现特地追踪到C++层来剥开synchronized的面纱. 网上的很多描述大都不全,让人看了不够爽,看完本章,你将彻底了解 ...

随机推荐

  1. (五)MySQL函数

    5.1  常用函数 5.2  聚合函数(常用) 函数名称 描述 COUNT() 计数 SUM() 求和 AVG() 平均值 MAX() 最大值 MIN() 最小值 ....   ....   想查询一 ...

  2. 基于Mui与H5+开发webapp的Android原生工程打包步骤(使用新版本5+SDK与Android studio)(部分内容转自dcloud官网)

    文章背景: dcloud官网给出的打包步骤对于有一定安卓打包基础的同学来说比较容易掌握,但是对于webapp小白来讲有的地方可能没有说的太具体.下面我给大家介绍的详细一点,保证大家按照步骤就能学会打包 ...

  3. silky微服务框架服务注册中心介绍

    目录 服务注册中心简介 服务元数据 主机名称(hostName) 服务列表(services) 终结点 时间戳 使用Zookeeper作为服务注册中心 使用Nacos作为服务注册中心 使用Consul ...

  4. 菜鸡的Java笔记 第三十三 - java 泛型

    泛型 GenericParadigm        1.泛型的产生动机        2.泛型的使用以及通配符        3.泛型方法的使用                JDK1.5 后的三大主 ...

  5. 力扣 - 剑指 Offer 22. 链表中倒数第k个节点

    题目 剑指 Offer 22. 链表中倒数第k个节点 思路1(栈) 既然要倒数第k个节点,那我们直接把所有节点放到栈(先进后出)里面,然后pop弹出k个元素就可以了 代码 class Solution ...

  6. <C#任务导引教程>练习十

    /*83,使用接口完成多继承问题 简化版*/using System;interface ITeacher{    string Name    {        get;        set;   ...

  7. <C#任务导引教程>练习一

    //1,定位显示ASCI码值为30到119的字符using System;class Program{    static void Main()    {        int i, n = 0;  ...

  8. [loj3364]植物比较

    结论:设$b_{i}$满足该限制,则$a_{i}$合法当且仅当$\forall i\ne j,a_{i}\ne a_{j}$且$\forall |i-j|<k,[a_{i}<a_{j}]= ...

  9. c语言if语句是如何变成汇编代码的?

    1. 要编译的测试代码: int a; int b = 3; int main(void) { if (3) a = 4; else b = 5; } 2. 词法分析 词法分析将c源代码解析成一个个的 ...

  10. 【GS基础】植物基因组选择研究人员及数量遗传学发展一览

    目录 1.GS研究 2.数量遗传发展 GS应用主要在国外大型动物和种企,国内仍以学术为主.近期整理相关学术文献,了解到一些相关研究人员,记录下备忘查询,但不可能全面. 1.GS研究 Theo Meuw ...