广告位联系
返回顶部
分享到

java同步器AQS架构AbstractQueuedSynchronizer原理解析(下)

java 来源:互联网 作者:秩名 发布时间:2022-03-11 21:00:15 人浏览
摘要

引导语 AQS 的内容太多,所以我们分成了两个章节,没有看过 AQS 上半章节的同学可以回首看一下哈,上半章节里面说了很多锁的基本概念,基本属性,如何获得锁等等,本章我们主要聊

引导语

AQS 的内容太多,所以我们分成了两个章节,没有看过 AQS 上半章节的同学可以回首看一下哈,上半章节里面说了很多锁的基本概念,基本属性,如何获得锁等等,本章我们主要聊下如何释放锁和同步队列两大部分。

1、释放锁

释放锁的触发时机就是我们常用的 Lock.unLock () 方法,目的就是让线程释放对资源的访问权(流程见整体架构图紫色路线)。

释放锁也是分为两类,一类是排它锁的释放,一类是共享锁的释放,我们分别来看下。

1.1、释放排它锁 release

排它锁的释放就比较简单了,从队头开始,找它的下一个节点,如果下一个节点是空的,就会从尾开始,一直找到状态不是取消的节点,然后释放该节点,源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

// unlock 的基础方法

public final boolean release(int arg) {

    // tryRelease 交给实现类去实现,一般就是用当前同步器状态减去 arg,如果返回 true 说明成功释放锁。

    if (tryRelease(arg)) {

        Node h = head;

        // 头节点不为空,并且非初始化状态

        if (h != null && h.waitStatus != 0)

            // 从头开始唤醒等待锁的节点

            unparkSuccessor(h);

        return true;

    }

    return false;

}

// 很有意思的方法,当线程释放锁成功后,从 node 开始唤醒同步队列中的节点

// 通过唤醒机制,保证线程不会一直在同步队列中阻塞等待

private void unparkSuccessor(Node node) {

    // node 节点是当前释放锁的节点,也是同步队列的头节点

    int ws = node.waitStatus;

    // 如果节点已经被取消了,把节点的状态置为初始化

    if (ws < 0)

        compareAndSetWaitStatus(node, ws, 0);

    // 拿出 node 节点的后面一个节点

    Node s = node.next;

    // s 为空,表示 node 的后一个节点为空

    // s.waitStatus 大于0,代表 s 节点已经被取消了

    // 遇到以上这两种情况,就从队尾开始,向前遍历,找到第一个 waitStatus 字段不是被取消的

    if (s == null || s.waitStatus > 0) {

        s = null;

        // 这里从尾迭代,而不是从头开始迭代是有原因的。

        // 主要是因为节点被阻塞的时候,是在 acquireQueued 方法里面被阻塞的,唤醒时也一定会在 acquireQueued 方法里面被唤醒,唤醒之后的条件是,判断当前节点的前置节点是否是头节点,这里是判断当前节点的前置节点,所以这里必须使用从尾到头的迭代顺序才行,目的就是为了过滤掉无效的前置节点,不然节点被唤醒时,发现其前置节点还是无效节点,就又会陷入阻塞。

        for (Node t = tail; t != null && t != node; t = t.prev)

            // t.waitStatus <= 0 说明 t 没有被取消,肯定还在等待被唤醒

            if (t.waitStatus <= 0)

                s = t;

    }

    // 唤醒以上代码找到的线程

    if (s != null)

        LockSupport.unpark(s.thread);

}

1.2、释放共享锁 releaseShared

释放共享锁的方法是 releaseShared,主要分成两步:

tryReleaseShared 尝试释放当前共享锁,失败返回 false,成功走 2;

唤醒当前节点的后续阻塞节点,这个方法我们之前看过了,线程在获得共享锁的时候,就会去唤醒其后面的节点,方法名称为:doReleaseShared。

我们一起来看下 releaseShared 的源码:

1

2

3

4

5

6

7

8

9

// 共享模式下,释放当前线程的共享锁

public final boolean releaseShared(int arg) {

    if (tryReleaseShared(arg)) {

        // 这个方法就是线程在获得锁时,唤醒后续节点时调用的方法

        doReleaseShared();

        return true;

    }

    return false;

}

2、条件队列的重要方法

在看条件队列的方法之前,我们先得弄明白为什么有了同步队列,还需要条件队列?

主要是因为并不是所有场景一个同步队列就可以搞定的,在遇到锁 + 队列结合的场景时,就需要 Lock + Condition 配合才行,先使用 Lock 来决定哪些线程可以获得锁,哪些线程需要到同步队列里面排队阻塞;获得锁的多个线程在碰到队列满或者空的时候,可以使用 Condition 来管理这些线程,让这些线程阻塞等待,然后在合适的时机后,被正常唤醒。

同步队列 + 条件队列联手使用的场景,最多被使用到锁 + 队列的场景中。

所以说条件队列也是不可或缺的一环。

接下来我们来看一下条件队列一些比较重要的方法,以下方法都在 ConditionObject 内部类中。

2.1、入队列等待 await

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

// 线程入条件队列

public final void await() throws InterruptedException {

    if (Thread.interrupted())

        throw new InterruptedException();

    // 加入到条件队列的队尾

    Node node = addConditionWaiter();

    // 标记位置 A

    // 加入条件队列后,会释放 lock 时申请的资源,唤醒同步队列队列头的节点

    // 自己马上就要阻塞了,必须马上释放之前 lock 的资源,不然自己不被唤醒的话,别的线程永远得不到该共享资源了

    int savedState = fullyRelease(node);

    int interruptMode = 0;

    // 确认node不在同步队列上,再阻塞,如果 node 在同步队列上,是不能够上锁的

    // 目前想到的只有两种可能:

    // 1:node 刚被加入到条件队列中,立马就被其他线程 signal 转移到同步队列中去了

    // 2:线程之前在条件队列中沉睡,被唤醒后加入到同步队列中去

    while (!isOnSyncQueue(node)) {

        // this = AbstractQueuedSynchronizer$ConditionObject

        // 阻塞在条件队列上

        LockSupport.park(this);

        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)

            break;

    }

    // 标记位置 B

    // 其他线程通过 signal 已经把 node 从条件队列中转移到同步队列中的数据结构中去了

    // 所以这里节点苏醒了,直接尝试 acquireQueued

    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)

        interruptMode = REINTERRUPT;

    if (node.nextWaiter != null) // clean up if cancelled

        // 如果状态不是CONDITION,就会自动删除

        unlinkCancelledWaiters();

    if (interruptMode != 0)

        reportInterruptAfterWait(interruptMode);

}

await 方法有几点需要特别注意:

上述代码标记位置 A 处,节点在准备进入条件队列之前,一定会先释放当前持有的锁,不然自己进去条件队列了,其余的线程都无法获得锁了;上述代码标记位置 B 处,此时节点是被 Condition.signal 或者 signalAll 方法唤醒的,此时节点已经成功的被转移到同步队列中去了(整体架构图中蓝色流程),所以可以直接执行 acquireQueued 方法;Node 在条件队列中的命名,源码喜欢用 Waiter 来命名,所以我们在条件队列中看到 Waiter,其实就是 Node。

await 方法中有两个重要方法:addConditionWaiter 和 unlinkCancelledWaiters,我们一一看下。

2.1.1、addConditionWaiter

addConditionWaiter 方法主要是把节点放到条件队列中,方法源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

// 增加新的 waiter 到队列中,返回新添加的 waiter

// 如果尾节点状态不是 CONDITION 状态,删除条件队列中所有状态不是 CONDITION 的节点

// 如果队列为空,新增节点作为队列头节点,否则追加到尾节点上

private Node addConditionWaiter() {

    Node t = lastWaiter;

    // If lastWaiter is cancelled, clean out.

    // 如果尾部的 waiter 不是 CONDITION 状态了,删除

    if (t != null && t.waitStatus != Node.CONDITION) {

        unlinkCancelledWaiters();

        t = lastWaiter;

    }

    // 新建条件队列 node

    Node node = new Node(Thread.currentThread(), Node.CONDITION);

    // 队列是空的,直接放到队列头

    if (t == null)

        firstWaiter = node;

    // 队列不为空,直接到队列尾部

    else

        t.nextWaiter = node;

    lastWaiter = node;

    return node;

}

整体过程比较简单,就是追加到队列的尾部,其中有个重要方法叫做 unlinkCancelledWaiters,这个方法会删除掉条件队列中状态不是 CONDITION 的所有节点,我们来看下 unlinkCancelledWaiters 方法的源码,如下:

2.1.2、unlinkCancelledWaiters

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

// 会检查尾部的 waiter 是不是已经不是CONDITION状态了

// 如果不是,删除这些 waiter

private void unlinkCancelledWaiters() {

    Node t = firstWaiter;

    // trail 表示上一个状态,这个字段作用非常大,可以把状态都是 CONDITION 的 node 串联起来,即使 node 之间有其他节点都可以

    Node trail = null;

    while (t != null) {

        Node next = t.nextWaiter;

        // 当前node的状态不是CONDITION,删除自己

        if (t.waitStatus != Node.CONDITION) {

            //删除当前node

            t.nextWaiter = null;

            // 如果 trail 是空的,咱们循环又是从头开始的,说明从头到当前节点的状态都不是 CONDITION

            // 都已经被删除了,所以移动队列头节点到当前节点的下一个节点

            if (trail == null)

                firstWaiter = next;

            // 如果找到上次状态是CONDITION的节点的话,先把当前节点删掉,然后把自己挂到上一个状态是 CONDITION 的节点上

            else

                trail.nextWaiter = next;

            // 遍历结束,最后一次找到的CONDITION节点就是尾节点

            if (next == null)

                lastWaiter = trail;

        }

        // 状态是 CONDITION 的 Node

        else

            trail = t;

        // 继续循环,循环顺序从头到尾

        t = next;

    }

}

为了方便大家理解这个方法,画了一个释义图,如下:

图片描述

2.2、单个唤醒 signal 

signal 方法是唤醒的意思,比如之前队列满了,有了一些线程因为 take 操作而被阻塞进条件队列中,突然队列中的元素被线程 A 消费了,线程 A 就会调用 signal 方法,唤醒之前阻塞的线程,会从条件队列的头节点开始唤醒(流程见整体架构图中蓝色部分),源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

// 唤醒阻塞在条件队列中的节点

public final void signal() {

    if (!isHeldExclusively())

        throw new IllegalMonitorStateException();

    // 从头节点开始唤醒

    Node first = firstWaiter;

    if (first != null)

        // doSignal 方法会把条件队列中的节点转移到同步队列中去

        doSignal(first);

}

// 把条件队列头节点转移到同步队列去

private void doSignal(Node first) {

    do {

        // nextWaiter为空,说明到队尾了

        if ( (firstWaiter = first.nextWaiter) == null)

            lastWaiter = null;

        // 从队列头部开始唤醒,所以直接把头节点.next 置为 null,这种操作其实就是把 node 从条件队列中移除了

        // 这里有个重要的点是,每次唤醒都是从队列头部开始唤醒,所以把 next 置为 null 没有关系,如果唤醒是从任意节点开始唤醒的话,就会有问题,容易造成链表的割裂

        first.nextWaiter = null;

        // transferForSignal 方法会把节点转移到同步队列中去

        // 通过 while 保证 transferForSignal 能成功

        // 等待队列的 node 不用管他,在 await 的时候,会自动清除状态不是 Condition 的节点(通过 unlinkCancelledWaiters 方法)

        // (first = firstWaiter) != null  = true 的话,表示还可以继续循环, = false 说明队列中的元素已经循环完了

    } while (!transferForSignal(first) &&

             (first = firstWaiter) != null);

}

我们来看下最关键的方法:transferForSignal。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

// 返回 true 表示转移成功, false 失败

// 大概思路:

// 1. node 追加到同步队列的队尾

// 2. 将 node 的前一个节点状态置为 SIGNAL,成功直接返回,失败直接唤醒

// 可以看出来 node 的状态此时是 0 了

final boolean transferForSignal(Node node) {

    /*

     * If cannot change waitStatus, the node has been cancelled.

     */

    // 将 node 的状态从 CONDITION 修改成初始化,失败返回 false

    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))

        return false;

    // 当前队列加入到同步队列,返回的 p 是 node 在同步队列中的前一个节点

    // 看命名是 p,实际是 pre 单词的缩写

    Node p = enq(node);

    int ws = p.waitStatus;

    // 状态修改成 SIGNAL,如果成功直接返回

    // 把当前节点的前一个节点修改成 SIGNAL 的原因,是因为 SIGNAL 本身就表示当前节点后面的节点都是需要被唤醒的

    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))

        // 如果 p 节点被取消,或者状态不能修改成SIGNAL,直接唤醒

        LockSupport.unpark(node.thread);

    return true;

}

整个源码下来,我们可以看到,唤醒条件队列中的节点,实际上就是把条件队列中的节点转移到同步队列中,并把其前置节点状态置为 SIGNAL。

2.3、全部唤醒 signalAll

signalAll 的作用是唤醒条件队列中的全部节点,源码如下:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

public final void signalAll() {

    if (!isHeldExclusively())

        throw new IllegalMonitorStateException();

    // 拿到头节点

    Node first = firstWaiter;

    if (first != null)

        // 从头节点开始唤醒条件队列中所有的节点

        doSignalAll(first);

}

// 把条件队列所有节点依次转移到同步队列去

private void doSignalAll(Node first) {

    lastWaiter = firstWaiter = null;

    do {

        // 拿出条件队列队列头节点的下一个节点

        Node next = first.nextWaiter;

        // 把头节点从条件队列中删除

        first.nextWaiter = null;

        // 头节点转移到同步队列中去

        transferForSignal(first);

        // 开始循环头节点的下一个节点

        first = next;

    } while (first != null);

}

从源码中可以看出,其本质就是 for 循环调用 transferForSignal 方法,将条件队列中的节点循环转移到同步队列中去。

3、总结

AQS 源码终于说完了,你都懂了么,可以在默默回忆一下 AQS 架构图,看看这张图现在能不能看懂了。

图片描述


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 : https://blog.csdn.net/qq_34272760/article/details/120559494
相关文章
  • SpringBoot自定义错误处理逻辑介绍

    SpringBoot自定义错误处理逻辑介绍
    1. 自定义错误页面 将自定义错误页面放在 templates 的 error 文件夹下,SpringBoot 精确匹配错误信息,使用 4xx.html 或者 5xx.html 页面可以打印错误
  • Java实现手写一个线程池的代码

    Java实现手写一个线程池的代码
    线程池技术想必大家都不陌生把,相信在平时的工作中没有少用,而且这也是面试频率非常高的一个知识点,那么大家知道它的实现原理和
  • Java实现断点续传功能的代码

    Java实现断点续传功能的代码
    题目实现:网络资源的断点续传功能。 二、解题思路 获取要下载的资源网址 显示网络资源的大小 上次读取到的字节位置以及未读取的字节
  • 你可知HashMap为什么是线程不安全的
    HashMap 的线程不安全 HashMap 的线程不安全主要体现在下面两个方面 在 jdk 1.7 中,当并发执行扩容操作时会造成环形链和数据丢失的情况 在
  • ArrayList的动态扩容机制的介绍

    ArrayList的动态扩容机制的介绍
    对于 ArrayList 的动态扩容机制想必大家都听说过,之前的文章中也谈到过,不过由于时间久远,早已忘却。 所以利用这篇文章做做笔记,加
  • JVM基础之字节码的增强技术介绍

    JVM基础之字节码的增强技术介绍
    字节码增强技术 在上文中,着重介绍了字节码的结构,这为我们了解字节码增强技术的实现打下了基础。字节码增强技术就是一类对现有字
  • Java中的字节码增强技术

    Java中的字节码增强技术
    1.字节码增强技术 字节码增强技术就是一类对现有字节码进行修改或者动态生成全新字节码文件的技术。 参考地址 2.常见技术 技术分类 类
  • Redis BloomFilter布隆过滤器原理与实现

    Redis BloomFilter布隆过滤器原理与实现
    Bloom Filter 概念 布隆过滤器(英语:Bloom Filter)是1970年由一个叫布隆的小伙子提出的。它实际上是一个很长的二进制向量和一系列随机映射
  • Java C++算法题解leetcode801使序列递增的最小交换次

    Java C++算法题解leetcode801使序列递增的最小交换次
    题目要求 思路:状态机DP 实现一:状态机 Java 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 class Solution { public int minSwap(int[] nums1, int[] nums2) { int n
  • Mybatis结果集映射与生命周期介绍

    Mybatis结果集映射与生命周期介绍
    一、ResultMap结果集映射 1、设计思想 对简单的语句做到零配置,对于复杂一点的语句,只需要描述语句之间的关系就行了 2、resultMap的应用场
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计