【并发编程】深入探索AQS

news2024/12/25 9:21:10

文章目录

  • 一、AQS 介绍
  • 二、通过ReentrantLock分析AQS的实现
    • 2.1、获取锁流程
    • 2.2、获取锁源码分析
      • 2.2.1、acquire
      • 2.2.2、tryAcquire
      • 2.2.3、addWaiter
      • 2.2.4、acquireQueued
      • 2.2.5、shouldParkAfterFailedAcquire
    • 2.3、解锁源码分析
      • 2.3.1、unlock
      • 2.3.2、release
      • 2.3.3、tryRelease
      • 2.3.4、unparkSuccessor
  • 三、公平锁&非公平锁

一、AQS 介绍

AQS(AbstractQueuedSynchronizer)就是一个抽象类,其主要维护一个资源同步状态变量的值(state)和一个存放排队线程的CLH双向队列,同时线程阻塞等待以及被唤醒时锁分配的机制。具体结构如下:
在这里插入图片描述

AQS使用一个volatile的int类型的成员变量来表示同步状态,通过内置的先进先出的CLH队列来完成获取资源线程的排队工作,将每条要去抢占资源的线程封装成一个Node节点来实现锁的分配,通过CAS完成对State值的修改。

CLH锁其实就是一种是基于逻辑队列非线程饥饿的一种自旋公平锁,由于是 Craig、Landin 和
Hagersten三位大佬的发明,因此命名为CLH锁。

AQS有一个内部类Node,Node节点对每一个等待获取资源的线程的封装,其包含了需要同步的线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。

这里可以直观地看出该node节点会存储当前被阻塞的请求资源线程,变量waitStatus则表示当前Node节点的等待状态,共有5种取值如下:

  • CANCELLED(1):表示当前结点已取消调度。当timeout或被中断(响应中断的情况下),会触发变更为此状态,进入该状态后的结点将不会再变化。
  • SIGNAL(-1):表示后继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为SIGNAL。
  • CONDITION(-2):表示结点等待在Condition上,当其他线程调用了Condition的signal()方法后,- CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • 0:新结点入队时的默认状态。

AQS的关键方法有以下几个:

  • acquire():用于线程获取同步器的状态,如果无法获取,则线程会进入阻塞状态等待其他线程释放同步器。
  • release():用于线程释放同步器的状态,并唤醒等待队列中的其他线程。
  • tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
  • tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。•
  • tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余可用资源。
  • tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点则返回true,否则返回false。

二、通过ReentrantLock分析AQS的实现

ReentrantLock是JUC并发包中一个重要常用的同步器,其底层就是依赖AQS实现的。下面我们将详细看下ReentrantLock的实现。

2.1、获取锁流程

ReentrantLock抢占锁交互图:

整体流程如下
在这里插入图片描述

2.2、获取锁源码分析

下面我们分别看下这些方法的源码:
打开lock.lock()这时候跳转到ReentrantLock的内部类NonfairSync(NonfairSync继承自AQS)的lock()方法:
在这里插入图片描述

2.2.1、acquire

acquire()方法是AQS中的方法,这里就是线程加锁占位资源不成功,把线程放到CLH队列等待通知唤醒的核心入口处
在这里插入图片描述

核心流程步骤如下:

  • tryAcquire()尝试直接去获取资源,如果成功则直接返回(这里体现了非公平锁,每个线程获取锁时会尝试直接抢占加塞一次,而CLH队列中可能还有别的线程在等待);
  • addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
  • acquireQueued()使线程阻塞在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  • 如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

2.2.2、tryAcquire

FairSync.tryAcquire的实现如下:

final boolean nonfairTryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    if (c == 0) {  
        if (compareAndSetState(0, acquires)) {  
            setExclusiveOwnerThread(current);  
            return true;  
        }  
    }  
    else if (current == getExclusiveOwnerThread()) {  
        int nextc = c + acquires;  
        if (nextc < 0) // overflow  
            throw new Error("Maximum lock count exceeded");  
        setState(nextc);  
        return true;  
    }  
    return false;  
}

这段代码是 AbstractQueuedSynchronizer (AQS) 中的 nonfairTryAcquire 方法的实现。该方法用于非公平地尝试获取同步器的状态。

  • 首先,获取当前线程并获取当前的状态值 c。
  • 如果当前状态值 c 为 0,表示同步器当前没有被其他线程占用。在这种情况下,直接使用 compareAndSetState 方法将状态从 0 修改为 acquires(期望值为 0,更新值为 acquires)。如果修改成功,表示当前线程成功获取了同步器,并将当前线程设置为独占式锁的拥有者,然后返回 true。
  • 如果当前状态值 c 不为 0,表示同步器已经被其他线程占用。此时需要判断当前线程是否为同步器的独占式锁拥有者,如果是,则将当前状态值加上 acquires(增加锁的持有次数),并返回 true。
  • 如果以上条件都不满足,则表示当前线程无法获取同步器的状态,返回 false。

2.2.3、addWaiter

此方法用于将当前线程加入到等待队列CLH的队尾,并返回当前线程所在的结点

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;  
    if (pred != null) {  
        node.prev = pred;  
        if (compareAndSetTail(pred, node)) {  
            pred.next = node;  
            return node;  
        }  
    }  
    enq(node);  
    return node;  
}
  • 首先,创建一个新的节点 Node,该节点包含当前线程和指定的等待模式 mode。
  • 尝试使用快速路径 (fast path) 将新节点添加到队列中。首先获取当前队列的尾节点 pred,如果尾节点不为 null,则将新节点的 prev 指向 pred,并尝试使用 compareAndSetTail 方法将尾节点更新为新节点。如果更新成功,则将原尾节点 pred 的 next 指向新节点,并返回新节点。
  • 如果快速路径失败,即尾节点为 null 或者 compareAndSetTail 失败,表示有其他线程在并发修改队列。这时候需要使用完整的入队操作 (enq) 将新节点添加到队列中。
  • 在 enq 方法中,首先通过自旋操作将新节点添加到队列的尾部。
  • 返回新节点。

该方法的作用是将当前线程作为等待线程添加到同步队列中。它采用了一种乐观的策略,首先尝试使用快速路径将新节点添加到队列尾部,如果失败则使用完整的入队操作。通过这种方式,可以在大多数情况下避免竞争和线程阻塞,提高并发性能。

2.2.4、acquireQueued

该方法用于在同步队列中获取锁,当无法直接获取锁时,将当前线程加入到等待队列中并进行自旋等待。

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())  
                interrupted = true;  
            }  
    } finally {  
        if (failed)  
            cancelAcquire(node);  
    }  
}
  • 首先,设置一个标志位 failed 为 true,用于记录获取锁的过程中是否出现异常。
  • 在 try-catch-finally 块中进行循环,直到成功获取到锁或被中断。
  • 在循环中,首先获取当前节点的前驱节点 p,判断当前节点的前驱节点是否为头节点且尝试获取锁成功(通过调用 tryAcquire 方法)。如果是,则将当前节点设置为新的头节点,断开原头节点和当前节点的连接,将前驱节点的 next 指针置为 null,最后将标志位 failed 设为 false,并返回中断状态 interrupted。
  • 如果前驱节点 p 不满足条件,调用 shouldParkAfterFailedAcquire 方法判断是否需要阻塞当前线程,并调用 parkAndCheckInterrupt 方法进行线程阻塞,并检查线程是否被中断。如果被中断,则将中断状态 interrupted 设置为 true。
  • 循环回到第2步,继续尝试获取锁或阻塞。
  • 如果循环结束时仍然无法获取锁,即获取锁的过程中出现异常,则调用 cancelAcquire 方法取消当前节点的获取操作。

2.2.5、shouldParkAfterFailedAcquire

该方法用于判断在获取锁失败后,当前线程是否应该进行阻塞:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
    int ws = pred.waitStatus;  
    if (ws == Node.SIGNAL)  
        return true;  
    if (ws > 0) {  
        do {  
            node.prev = pred = pred.prev;  
        } while (pred.waitStatus > 0);  
        pred.next = node;  
    } else {  
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
    }  
    return false;  
}
  • 首先,获取前驱节点 pred 的等待状态 waitStatus。
  • 如果等待状态 waitStatus 等于 Node.SIGNAL,表示前驱节点已经设置了状态要求释放锁来发出信号,因此当前节点可以安全地进行阻塞,返回 true。
  • 如果等待状态 waitStatus 大于 0,表示前驱节点已被取消。循环遍历跳过前驱节点及其之前的已被取消的节点,直到找到一个等待状态小于等于 0 的前驱节点 pred。然后更新当前节点的 prev 指针为找到的前驱节点 pred,将前驱节点 pred 的 next 指针指向当前节点 node。
  • 如果等待状态 waitStatus 为 0 或 Node.PROPAGATE,则表明需要一个信号来唤醒当前节点,但不立即进行阻塞。调用 compareAndSetWaitStatus 方法将前驱节点 pred 的等待状态修改为 Node.SIGNAL,以表示需要一个信号。
  • 返回 false,表示当前线程不需要进行阻塞。

2.3、解锁源码分析

2.3.1、unlock

NonfairSync(NonfairSync继承自AQS)的unlock()方法:

public void unlock() {  
    sync.release(1);  
}

2.3.2、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;  
}

2.3.3、tryRelease

该方法用于释放锁资源。

protected final boolean tryRelease(int releases) {  
    int c = getState() - releases;  
    if (Thread.currentThread() != getExclusiveOwnerThread())  
        throw new IllegalMonitorStateException();  
    boolean free = false;  
    if (c == 0) {  
        free = true;  
        setExclusiveOwnerThread(null);  
    }  
    setState(c);  
    return free;  
}
  • 首先,获取当前锁的状态值 c = getState() - releases,表示要释放锁后的新状态。

  • 判断当前线程是否是独占锁的拥有者,如果不是则抛出 IllegalMonitorStateException 异常。

  • 根据新状态 c 进行处理:

    • 如果新状态 c 等于 0,表示锁已完全释放。将 exclusiveOwnerThread 设置为 null,表示当前没有拥有者,同时将 free 标志设置为 true。
    • 否则,更新锁的状态为新状态 c。
  • 返回 free 标志,表示锁是否已完全释放。

2.3.4、unparkSuccessor

如果上面释放资源成功返回true,那么就会执行unparkSuccessor()唤醒等待队列里的下一个线程

private void unparkSuccessor(Node node) {  
    /*  
    * If status is negative (i.e., possibly needing signal) try  
    * to clear in anticipation of signalling. It is OK if this  
    * fails or if status is changed by waiting thread.  
    */  
    int ws = node.waitStatus;  
    if (ws < 0)  
        compareAndSetWaitStatus(node, ws, 0);  
  
    /*  
    * Thread to unpark is held in successor, which is normally  
    * just the next node. But if cancelled or apparently null,  
    * traverse backwards from tail to find the actual  
    * non-cancelled successor.  
    */  
    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);  
}
  • 首先,判断节点的等待状态 ws,如果 ws 小于 0,则尝试将其置为 0。这是为了预先清除可能需要信号的状态。即使此操作失败或等待线程更改了状态,也不会影响后续的操作。
  • 获取节点的后继节点 s,通常后继节点就是当前节点的下一个节点。但如果后继节点被取消或为空,就从尾部向前遍历找到实际未被取消的后继节点。这是为了找到真正需要唤醒的节点。
  • 如果找到了需要唤醒的后继节点 s,则调用 LockSupport.unpark(s.thread) 方法唤醒该节点对应的线程。

三、公平锁&非公平锁

ReentrantLock分为公平锁和非公平锁,默认构造方法是非公平锁,如果我们需要构建公平锁,只需要传参true即可:

Lock lock = new ReentrantLock(true);

其加锁解锁流程几乎跟上面非公平锁一样,一些细节有些不同:

protected final boolean tryAcquire(int acquires) {  
    final Thread current = Thread.currentThread();  
    int c = getState();  
    if (c == 0) {  
        if (!hasQueuedPredecessors() &&  
            compareAndSetState(0, acquires)) {  
            setExclusiveOwnerThread(current);  
            return true;  
        }  
    }  
    else if (current == getExclusiveOwnerThread()) {  
        int nextc = c + acquires;  
        if (nextc < 0)  
            throw new Error("Maximum lock count exceeded");  
        setState(nextc);  
        return true;  
    }  
    return false;  
}

这里lock()没有和非公平锁的一样先使用CAS尝试加锁占有资源,是直接调用acquire(1)进行入队操作。加锁这里多了hasQueuedPredecessors的逻辑

public final boolean hasQueuedPredecessors() {   
    Node t = tail; // Read fields in reverse initialization order  
    Node h = head;  
    Node s;  
    return h != t &&  
        ((s = h.next) == null || s.thread != Thread.currentThread());  
}

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

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

相关文章

实验篇(7.2) 17. 站对站安全隧道 - FortiGate作为SSL客户端(SSL) ❀ 远程访问

【简介】虽然常用的站到站的连接用的是IPsec VPN&#xff0c;但是在某些特殊情况下&#xff0c;UDP500或4500端口被阻断&#xff0c;IPsec VPN无法连接&#xff0c;那么还有其它办法实现站到站的连接吗&#xff1f;SSL VPN也可以的。 实验要求与环境 OldMei集团深圳总部部署了域…

NodeJS File Upload⑩

文章目录 ✨文章有误请指正&#xff0c;如果觉得对你有用&#xff0c;请点三连一波&#xff0c;蟹蟹支持&#x1f618;前言文件上传 后端接口 Form表单上传 Axios前后端分离上传 实现效果演示 记录 读取图片文件总结 ✨文章有误请指正&#xff0c;如果觉得对你有用&a…

RK3568平台开发系列讲解(外设篇)四线风扇驱动实验

🚀返回专栏总目录 文章目录 一、硬件连接二、原理图分析三、驱动适配3.1、内核配置3.2、修改设备树3.3、实验沉淀、分享、成长,让自己和他人都能有所收获!😄 📢本篇我们将讲解四线风扇的使用。 一、硬件连接 风扇模块如下所示,黑线是负,红线是正,黄线是测速,蓝线是…

算法——字符串匹配算法——BM(Boyer-Moore) 算法

字符串匹配算法——BM &#xff08;Boyer-Moore&#xff09; 算法 概述场景一 坏字符场景且模式串中没有匹配字符场景二 坏字符场景且模式串中有匹配字符场景三 好后缀场景且模式串中没有匹配字符场景四 好后缀场景且模式串中有匹配字符场景五 好后缀场景且模式串中有匹配子串后…

EfficientDet-pytorch目标检测训练

目录 1. EfficientDet-pytorch版本代码下载 2.数据集准备 2.1数据集格式 2.2 定义自己数据集的yml文件 3. 训练配置 4.模型评估 5.测试模型性能 1. EfficientDet-pytorch版本代码下载 GitHub - zylo117/Yet-Another-EfficientDet-Pytorch: The pytorch re-implement…

chatgpt赋能python:Python提取指定数据的方法与技巧

Python提取指定数据的方法与技巧 在SEO优化中&#xff0c;数据的提取和分析是非常重要的环节之一。而Python具有方便易用的数据处理能力&#xff0c;成为了SEO优化工程师们的重要工具之一。本文将介绍Python中提取指定数据的方法与技巧&#xff0c;以及实现的具体案例。 数据…

python:使用Scikit-image库进行单波段遥感图像颜色直方图特征提取(histogram)

作者:CSDN @ _养乐多_ 本文记录了使用Scikit-image库对单波段遥感图像做颜色直方图特征提取的代码。 文章目录 一、颜色直方图特征详解二、代码一、颜色直方图特征详解 颜色直方图是一种用于描述图像中颜色分布的特征表示方法。它将图像中每个像素的颜色值作为输入,统计并显…

Java 基础进阶篇(十七):反射概述及获取对象的方式

文章目录 一、反射概述二、反射获取类对象三、反射获取构造器对象四、反射获取成员变量对象五、反射获取方法对象六、 反射的作用6.1 绕过编译阶段为集合添加数据6.2 通用框架的底层原理 一、反射概述 反射是指对于任何一个Class类&#xff0c;在 “运行的时候”&#xff0c;不…

PHP实战开发23-PHP结合Nginx获取用户真实IP地址

文章目录 一、前言二、关于用户IP的背景知识2.1 HTTP请求2.2 HTTP代理服务器2.3 X-Forwarded-For头部 三、代码实现3.1 Nginx配置3.2 PHP代码处理 总结 一、前言 本文已收录于PHP全栈系列专栏&#xff1a;PHP快速入门与实战 在Web应用程序中&#xff0c;IP地址是常见的数据项…

Flink 学习四 Flink 基础架构

Flink 学习四 Flink 基础架构&算子链&槽位 文章大部分数据来源 : https://nightlies.apache.org/flink/flink-docs-release-1.14/docs/concepts/flink-architecture/ Flink 是一个分布式系统,需要有效的分配和管理计算资源才可以执行流式程序; 集成了常见的资源管理…

chatgpt赋能python:Python简介

Python简介 Python是一种高级编程语言&#xff0c;具有易读性和简洁性的特点。它被广泛使用于Web开发、数据科学、人工智能、机器学习和自动化测试等领域。Python也是一种非常适合新手学习编程的语言。 在本篇文章中&#xff0c;我们将讨论如何使用Python提取指定内容以进行S…

【BMS】电池包硬件方案选型指南

🔋电池包硬件方案选型指南🔋 BMS硬件系统需求主要包括:测温模块、测流模块、测压模块、系统电源、保护电路、故障检测电路,本文阐述各个功能模块在不同场景下的电池包硬件系统方案选择。 一、测温 NTC(热敏电阻) 电池包测温一般包括表皮温度、内部温度、PCB温度(极片布…

[自定义组件]微信小程序自定义组件实现缩略图和原图分离及可缩放效果

目录 目标及基础环境背景 实现原理左右滑动缩放图片菜单 开发实现自定义组件wxml组件结构wxss 样式控制js定义属性及回调json声明为组件 使用添加组件声明及地址声明为全局组件(也可声明为局部)声明为全局组件&#xff08;也可以声明为全局组件&#xff09;使用组件 效果展示 附…

pycharm安装, 汉化 , 使用教程

目录 1.下载安装包 2.汉化 3.使用 1.下载安装包 访问Pycharm官网 根据自己的操作系统下载对应版本的Pycharm Community或Professional Edition。 2.汉化 点击“file”选项&#xff0c;然后点击“setting”&#xff0c;再点击“plugins”选项&#xff1b; 输入“Chinese”找…

使用Frp进行反向代理实现远程桌面控制[teamviewer/nomachine]

.使用Frp进行反向代理实现远程桌面控制 V1.0.0 – by Holden Date : 2023-06-20 文章目录 .使用Frp进行反向代理实现远程桌面控制1. 简介2. 工具准备3. 服务器端搭建4. 受控端配置&&运行teamviewer5. 控制机端运行teamviewer6. 切换成nomachine 1. 简介 ​ frp 是一…

winform多语言资源管理

SailingEase WinForm Framework WinForm开发框架开发手册&#xff1a;http://docs.shengxunwei.com/Home/Browser/sewinformfw/ 这是我2010年左右&#xff0c;写 Winform IDE &#xff08;使用 .NET WinForm 开发所见即所得的 IDE 开发环境&#xff0c;实现不写代码直接生成应用…

什么是算法

有人说程序算法数据结构&#xff0c;虽说这样的认为有失偏颇&#xff0c;一个程序决定的东西实在太多&#xff0c;但某些方面也说明了算法是很重要的&#xff08;数据结构承上启下&#xff0c;最终也是要为算法服务&#xff09;。 算法是用来解决问题的&#xff0c;要理解什么是…

AI Image Codec技术落地实践

AI Codec自2016年首次提出以来&#xff0c;众多海内外高校、企业研究院等机构对此展开了广泛研究。6年时间里&#xff0c;AI Codec 的SOTA方案的压缩性能已经超越了H.266(最新的传统Codec标准)&#xff0c;展现了强大的技术潜力。但受限于计算复杂度、非标等原因&#xff0c;AI…

Vue中的JSX的特性

JSX简介 JSX是一种Javascript的语法扩展&#xff0c;即具备了Javascript的全部功能&#xff0c;同时又兼具html的语义化和直观性。它可以让我们在JS中写模板语法&#xff1a; const el <div>Vue 2</div>; 复制代码上面这段代码既不是 HTML 也不是字符串&#xf…

java阿里云sls基于LoghubAppender自定义日志上传

1、背景&#xff1a;阿里sls日志提供快捷日志平台&#xff0c;平替elk公司使用这个日志服务&#xff0c;需要对接写入日志 目前日志集成有3种 1&#xff09;基于封装manager手动写日志手动send 弊端&#xff1a;本地日志和阿里云日志共用日志代码很臃肿 2&#xff09;基于云服…