关于对ArrayBlockingQueue 的AQS探究

news2025/2/26 23:13:27

1、介绍

条件队列是 AQS 中最容易被忽视的一个细节。大部分时候,我们都用不上条件队列,但是这并不说明条件队列就没有用处了,它反而是我们学习生产者-消费者模式的最佳教材。条件队列是指一个阻塞队列,其中的元素是等待某个条件成立的线程。在 Java 的同步机制中,条件队列通常用于实现线程等待和唤醒的操作。当条件不满足时,线程会被加入到条件队列中等待;当条件满足时,线程会被从条件队列中移除并继续执行

2、AQS 中的条件队列

AbstractQueuedSynchronizer 中的条件队列是一个单向链表,每个节点代表一个等待线程。条件队列的头节点是一个特殊的节点,表示等待队列的头部。当条件不满足时,线程会被加入到条件队列的尾部等待;当条件满足时,线程会从条件队列中移除并加入到同步队列中等待获取锁。
AbstractQueuedSynchronizer 中的条件队列是通过内部维护的等待队列和同步队列实现的。当线程调用 await() 方法时,它会被加入到等待队列中等待条件满足。
AQS模型当有其他线程调用了条件队列的 signal() 方法,线程则会从条件队列中移除,并加入到同步队列中等待获取锁,示例如下:
在这里插入图片描述

3、方法调用

(1)await

该方法的作用是将当前线程加入条件队列并阻塞,直到被唤醒或中断。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

如果线程被中断过,那么直接抛出中断异常
创建一个 Condition 类型的节点,并插入到等待队列中。

private Node addConditionWaiter() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node t = lastWaiter;
    // If lastWaiter is cancelled, clean out.
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }

    Node node = new Node(Node.CONDITION);

    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

我们看到 isHeldExclusively(),这个方法主要就是判断当前线程是否是持有独占锁的线程,如果不是,则直接抛出异常。所以,我们可以知道条件队列只适用于独占锁的情况。并且调用 await() 方法前,必须先获取锁。
然后,如果最后一个节点被取消的话,则会调用 unlinkCancelledWaiters() 方法移除掉所有被取消的节点。这个方法就是从等待的第一个元素开始,依次向后查找被取消的节点,然后将这些被取消的节点移除出等待队列中。
最后创建一个 Condition 节点,并且加入到条件队列中。

注意:操作等待队列的过程中,因为依然持有独占锁,所以是线程安全的,并不需要额外的同步操作。
释放锁
fullyRelease 是一个释放当前持有锁的方法,这个方法是调用 tryRelease 释放所持有的锁。
这里释放锁的原因是接下来将阻塞线程,如果不释放锁,那么这个锁资源将无法再被使用,直到这个持有锁的线程被唤醒,会造成资源的浪费
循环判断当前节点是否在同步队列中

while (!isOnSyncQueue(node)) {
    LockSupport.park(this);
    if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        break;
}

如果不在同步队列中,则直接阻塞线程,此时线程就停留在LockSupport.park(this); 处。直到其他线程将他唤醒或者中断,才会继续执行
如何判断节点是否在同步队列中呢?

我们可以先思考一下,等待队列中的节点状态都是 Condition 的,而同步队列中的状态则是 0、Signal等等,因此我们通过判断节点的状态。其次,同步队列中使用 pred、next 指针来组织同步队列的结构,我们可以通过判断节点的 pred 和 next 指针是否有值来判断。

final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
        return false;
    if (node.next != null) // If has successor, it must be on queue
        return true;
    /*
     * node.prev can be non-null, but not yet on queue because
     * the CAS to place it on queue can fail. So we have to
     * traverse from tail to make sure it actually made it.  It
     * will always be near the tail in calls to this method, and
     * unless the CAS failed (which is unlikely), it will be
     * there, so we hardly ever traverse much.
     */
    return findNodeFromTail(node);
}

第一种情况:通过判断节点的状态和 pred 指针来确定节点是否已经在同步队列中。

第二种情况:通过判断节点的 next 指针来确定节点是否已经在同步队列中。

而第三种情况其实是第一种情况的延伸,主要跟 enq() 插入同步队列有关。主要考虑的是当 pred 指针不为 null 时,说明节点可能已经为同步队列中,为什么说可能,是因为 CAS 设置 next 指针可能会失败。所以需要从同步队列的后面从前面开始在同步队列寻找此节点。

private Node enq(Node node) {
    for (;;) {
        Node oldTail = tail;
        if (oldTail != null) {
            node.setPrevRelaxed(oldTail);
            if (compareAndSetTail(oldTail, node)) {
                oldTail.next = node;
                return oldTail;
            }
        } else {
            initializeSyncQueue();
        }
    }
}

在插入同步队列方法中,我们可以看到第 5 行,它是先设置节点的 pred 指针。然后再通过 CAS 设置节点的 next 指针,如果此时 CAS 失败了,就会出现 pred 指针有值,但是 next 是找不到节点的。
当线程被唤醒之后(signal 或者中断),检查中断。

private int checkInterruptWhileWaiting(Node node) {
    return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

如果线程在 signal 之前就已经被唤醒(中断方式),则返回 THROW_IE;如果在 signal 唤醒之后才被中断,则返回REINTERRUPT。
怎么判断线程的唤醒方式呢
我们可以来思考一下:signal 的时候,会将节点的 Condition 状态修改为 0,也就是说如果是使用 signal 的方式唤醒线程,那么节点在被唤醒之前,它的节点状态已经被修改为 0 了。而使用中断的方式唤醒线程,则不会修改节点的状态。因此我们只需要判断节点的状态就可以知道线程是被哪种方式唤醒的。

我们通过源码来揭晓我们的猜想是否正确

final boolean transferAfterCancelledWait(Node node) {
    if (node.compareAndSetWaitStatus(Node.CONDITION, 0)) {
        enq(node);
        return true;
    }
    /*
     * If we lost out to a signal(), then we can't proceed
     * until it finishes its enq().  Cancelling during an
     * incomplete transfer is both rare and transient, so just
     * spin.
     */
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

通过 CAS 修改节点的状态,如果节点的状态是 Condition 的话,则可以成功修改为 0。说明节点就是被中断唤醒。如果节点状态无法修改成功,则说明节点的状态已经不是 Condition 了,那么则说明在唤醒之前已经被修改了,那么只可能是 signal 方法唤醒的。

如果节点是通过 signal 方法唤醒的,这里会循环判断节点是否已经在同步队列中了,只有节点已经在同步队列,才会结束执行。
为什么这里需要循环判断节点是否已经在同步队列呢?
如果这里不执行这一段的话,那么直接返回 false。我们此时回到 await 方法,会进入到下一次循环,继续判断节点是否在同步队列中,如果节点此时还没有加入到同步队列的话,就会继续被阻塞。那么这个节点就会出现在同步队列,却被阻塞的情况。如果此时没有其他线程唤醒的话,该线程就成为了一个死线程。

被唤醒之后,尝试获取锁
这里获取锁的方法跟独占锁获取锁的方法是同一个。获取锁成功之后,将返回,并重新记录中断。

(2) signal

该方法的作用是将条件队列中的第一个线程唤醒。

public final void signal() {
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

如果线程没有获取锁,那么则直接抛出异常。
唤醒条件队列中的第一个节点。

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

将节点从条件队列中移除,并加入到同步队列中,同时唤醒线程。

这里为什么需要使用一个 while 循环? 因为有可能当前需要唤醒节点已经被取消了,那么就需要继续唤醒下一个节点,直到有一个节点被成功唤醒或者条件队列为空才结束,这样做是保证可以正常唤醒一个线程。

我们线程来看它是如何唤醒一个线程的:

final boolean transferForSignal(Node node) {
    if (!node.compareAndSetWaitStatus(Node.CONDITION, 0))
        return false;

    /*
     * Splice onto queue and try to set waitStatus of predecessor to
     * indicate that thread is (probably) waiting. If cancelled or
     * attempt to set waitStatus fails, wake up to resync (in which
     * case the waitStatus can be transiently and harmlessly wrong).
     */
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !p.compareAndSetWaitStatus(ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

从第一个条件判断,我们就可以知道当节点已经不是 Condition 状态时,说明它可能已经被取消了。因此直接返回 false,让外层的 while 循环去唤醒下一个节点。

当已经加入同步队列后,发现前驱节点已经被取消,或者设置前驱节点的状态为 Signal 失败了,那么则直接唤醒当前节点的线程。
为什么要唤醒线程呢?而不是根据同步队列的规则去唤醒?
这里是为了让节点在同步队列中,可以自己完成修正,主要细节在 acquireQueued 这个方法中。我们这里只做简单的介绍。acquireQueued 会去尝试获取锁,如果获取锁失败,那么则会根据前驱节点的状态,来做出调整。如果前驱节点已经是 Signal,那么则直接进入阻塞,等待前驱节点唤醒。如果前驱节点是取消状态,即 status > 0,那么则往前寻找一个 status <= 0 的节点。

如果当前节点的前驱节点的状态可以正常设置为 Signal,那么则不会进行唤醒,而是按照同步队列的规则去进行后续的唤醒操作。

当线程被唤醒之后,就会继续执行 await 方法的后续代码。

小结

  1. 条件队列只能在独占锁情况下使用
  2. 在调用 await 和 signal 方法前,必须先获取锁,否则会抛出异常
  3. 线程可以被中断和 signal 两种方式唤醒。
  4. 线程被唤醒之后,并不会马上获得锁,而是加入同步队列,跟同步队列中的其他节点一起竞争锁。
  5. 使用 await 方法时,如果中断发生调用 signal 方法之前,那么会直接抛出中断异常。如果不想处理中断,可以使用
    awaitUninterruptibly(),该方法不会抛出中断异常,只会记录中断标记。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1285378.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

派对的最大快乐值

与其明天开始&#xff0c;不如现在行动&#xff01; 文章目录 派对的最大快乐值 &#x1f48e;总结 派对的最大快乐值 题目 员工信息的定义如下&#xff1a; 公司的每个员工都符合 Employee 类的描述。整个公司的人员结构可以看作是一棵标准的、没有环的多叉树。树的头节点是公…

Win环境中安装Jenkins指南

目录 安装Java环境 下载并安装Jenkins Jenkins版本 启动Jenkins 如何删除Jenkins 安装Java环境 访问 Oracle官方网站 下载并安装JDK 安装完成后&#xff0c;设置系统环境变量 JAVA_HOME 到你的 JDK 安装路径&#xff0c;并将 %JAVA_HOME%\bin 添加到系统 PATH 中。 下载…

css实现姓名两端对齐

1.1 效果 1.2 主要代码 text-align-last: justify; 1.3 html完整代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0&quo…

外包干了3年,技术退步明显...

先说情况&#xff0c;大专毕业&#xff0c;18年通过校招进入湖南某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&#xf…

【UE】UEC++ CurveFloat、CurveVector、CurveLinearColor

目录 【UE】UEC CurveFloat、CurveVector、CurveLinearColor 一、CurveFloat 二、CurveVector 三、CurveLinearColor 四、材质设置 五、运行结果 【UE】UEC CurveFloat、CurveVector、CurveLinearColor 一、CurveFloat #pragma once#include "CoreMinimal.h" …

人工智能时代AIGC绘画实战

系列文章目录 送书第一期 《用户画像&#xff1a;平台构建与业务实践》 送书活动之抽奖工具的打造 《获取博客评论用户抽取幸运中奖者》 送书第二期 《Spring Cloud Alibaba核心技术与实战案例》 送书第三期 《深入浅出Java虚拟机》 送书第四期 《AI时代项目经理成长之道》 …

RAG落地实践、AI游戏开发、上海·深圳·广州线下工坊启动!星河社区重磅周

飞桨星河社区在成立的5年以来&#xff0c;已汇集660万AI开发者&#xff0c;覆盖深度学习初学者、在职开发者、企业开发者、高校教师、创业者等&#xff0c;已成为AI领域最具影响力的社区之一&#xff0c;无论是AI爱好者还是AI开发者&#xff0c;都能在这里探索AI的无限可能。飞…

如何无线桥接路由器,让你的网络覆盖范围变大,做到网络信号无缝连接

你是否希望通过在两个路由器之间创建无线网桥(网络桥接)来扩大网络覆盖范围?好吧,你来对地方了!在当今日益互联的世界,拥有一个强大可靠的网络比以往任何时候都更重要。 无线网桥允许你无线连接两个或多个路由器,有效地扩展网络覆盖范围,并在更大的区域提供无缝的互联…

算法-滑动窗口

一、滑动窗口思想 概念 在数组双指针里&#xff0c;我们介绍过 "对撞型" 和 "快慢型" 两种方式&#xff0c;而滑动窗口思想就是快慢型的特例。 实际使用 计算机网络中有滑动窗口协议&#xff08;Sliding Window Protocol&#xff09;&#xff0c;该协议…

02、pytest环境准备

工具准备 python官网下载&#xff1a;https://www.python.org/pycharm官网下载&#xff1a;https://www.jetbrains.com.cn/en-us/pycharm/pytest官方文档&#xff1a;https://docs.pytest.org/en/7.4.x/python-office官网文档&#xff1a;http://www.python-office.com/ 参考…

OA系统是什么,能用低代码开发吗?

OA是什么&#xff1f;管办公室活动的 OA是Office Automation&#xff08;办公自动化&#xff09;的简称&#xff0c;原是指利用电脑进行全自动的办公&#xff0c;现在基本所有和办公相关的系统都可以称作是OA。绝大部分企业将OA用于企业内部的协作沟通&#xff0c;处理企业内部…

NSSCTF 文件上传漏洞题目

目录 [SWPUCTF 2021 新生赛]easyupload1.0 [SWPUCTF 2021 新生赛]easyupload2.0 [SWPUCTF 2021 新生赛]easyupload3.0 [SWPUCTF 2021 新生赛]easyupload1.0 这是一个文件上传漏洞的题目 我们的思路是上传一句话木马&#xff0c;用工具进行连接 先编写一句话木马 将文件后缀…

CSS实现小球边界碰撞回弹

如何通过CSS实现一个物体在屏幕中无限的边界碰撞回弹呢&#xff1f;我们可以使用动画效果实现 代码 我们只做一个小球&#xff0c;通过定位属性叠加动画的方式&#xff0c; 让小球在屏幕中进行运动&#xff0c;通过设置animation的alternate属性来设置回弹。最后&#xff0c;只…

为什么有很多公司的 ERP 系统用得还不如 Excel?

回顾ERP的发展历史&#xff0c;我们不难发现&#xff0c;ERP业务包含范围越来越广&#xff0c;但是让信息化适应业务、辅佐业务&#xff0c;是根植在ERP的诞生基因里面的。 ERP信息化的概念看上去如此美妙&#xff0c;但是在国内企业落地的时候&#xff0c;却出现了很多问题——…

企业课——配置两条静态路由

在广播型的接口&#xff08;如以太网的接口&#xff09;可以不配置出接口&#xff0c;但是要配置下一跳 路由跟踪&#xff1a;tracert ip 1.配置IP地址 2.配置两条路线的静态路由 iprouter-static 目的网段 掩码 出接口 下一跳 3.实现选路&#xff0c;在静态路由配置后…

渗透技巧之403绕过【总结】

文章目录 渗透技巧之403绕过【总结】0x01 前言0x02 背景1.什么是网页403&#xff1f;2.什么是403绕过&#xff1f;3.造成403的成因 0x03 绕过方式1.绕过IP限制2.url覆盖绕过3.扩展名绕过&#xff08;路径fuzz&#xff09;4.更换协议版本5.HTTP 请求方法fuzz6.修改Referer7.修改…

Interpretable Multimodal Misinformation Detection with Logic Reasoning

原文链接 Hui Liu, Wenya Wang, and Haoliang Li. 2023. Interpretable Multimodal Misinformation Detection with Logic Reasoning. In Findings of the Association for Computational Linguistics: ACL 2023, pages 9781–9796, Toronto, Canada. Association for Computa…

智能全彩屏负氧离子监测站-生态环境知识科普

随着人们对健康和环境保护的关注度不断提高&#xff0c;一款名为 WX-FLZ50智能全彩屏负氧离子监测站的新产品应运而生。这款产品能够实时监测环境中的负氧离子浓度&#xff0c;为人们提供空气质量信息&#xff0c;帮助人们更好地了解和保护自身所处的环境。 WX-FLZ50智能全彩屏…

pyecharts可视化作图4:行业分布-条形图

pyecharts做条形图功能也非常强大&#xff0c;本文也只展示基本的功能。 1. 源代码 import pandas as pd from pyecharts.charts import Bar from pyecharts import options as opts from pyecharts.globals import ThemeType# 构建模拟数据 data_dict {行业名称: {0: 钢铁,…

如何解决vue中的组件样式冲突

目录 1&#xff1a;组件样式冲突问题 2&#xff1a;导致组件之间样式冲突的根本原因是&#xff1a; 3&#xff1a;问题演示 4&#xff1a;通过设置scoped解决组件之间样式冲突问题 5&#xff1a;设置scoped解决组件样式冲突的原理 6&#xff1a;使用deep修改子组件的样式…