聊聊并发编程——多线程之AQS

目录

队列同步器(AQS)

独占锁示例

AQS之同步队列结构

解析AQS实现


队列同步器(AQS)

队列同步器AbstractQueuedSynchronizer(以下简称同步器),是用来构建锁或者其他同步组 件的基础框架,它使用了一个int成员变量表示同步状态,通过内置的FIFO队列来完成资源获 取线程的排队工作,并发包的作者(Doug Lea)期望它能够成为实现大部分同步需求的基础。

  • 同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。

  • 使用同步器提供的3 个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))对同步状态进行更改,因为它们能够保证状态的改变是安全的。

同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合同步器,利用同步器实现锁的语义。可以这样理解二者之间的关系:锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

独占锁示例

Lock接口的实现基本都是通过聚合了一个同步器的子类来完成线程访问控制的,通过独占锁了解下队列同步器(AQS)。

public class Mutex implements Lock {
​// 子类推荐被定义为自定义同步组件的静态内部类// 同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用private static class Sync extends AbstractQueuedSynchronizer {// 是否处于独占状态protected boolean isHeldExclusively() {return getState() == 1;}// 当状态为0的时候获取锁public boolean tryAcquire(int acquire) {if (compareAndSetState(0, 1)) {return true;}return false;}// 释放锁,将状态设置为0protected boolean tryRelease(int release) {if (getState() == 0) {throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null);setState(0);return true;}// 返回一个Condition,每一个condition都包含了一个condition队列Condition newCondition() {return new ConditionObject();}}
​
​// 通过Sync进行代理操作,实现Lock接口的APIprivate final Sync sync = new Sync();
​// 获取锁@Overridepublic void lock() {sync.acquire(1);}
​// 可中断地获取锁@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}
​// 尝试非阻塞的获取锁@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}
​/*超时的获取锁,当前线程在以下3种情况下会返回:1.当线程在超时时间获得了锁2.当线程在超时时间被中断3.超时时间结束,返回false*/@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(time));}
​// 释放锁@Overridepublic void unlock() {sync.release(1);}
​/*获取等待通知组件,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件的wait()方法,而调用用,当前线程将释放锁*/@Overridepublic Condition newCondition() {return sync.newCondition();}
}
AQS之同步队列结构

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成为一个节点(Node)并将其 加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。

节点属性:

属性类型与名称描述
int waitStatus等待状态,包含如下状态: CANCELLED:值为1,表示节点已取消,通常是因为线程被中断或者等待超时而被取消。 SIGNAL:值为-1,表示后继节点需要被唤醒,即当前节点的释放(signal)会通知后继节点继续尝试获取锁或资源。 CONDITION:值为-2,表示节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中 PROPAGATE:值为-3,表示释放共享锁时需要向后继节点传播共享性质,以确保后继节点可以被唤醒。这在CountDownLatch等场景中会使用到。 INITIAL:值为0,初始状态。
Node prev前驱节点,当节点加入同步队列时被设置(尾部添加)
Node next后继节点
Node nextWaiter等待队列中的后继节点。如果当前节点是共享的,那么这个字段将是一个SHARED常量,也就是说节点类型(独占和共享)和等待队列中的后继节点共用同一个字段
Thread thread获取同步状态的线程

节点是构成同步队列的基础,同步器拥有首节点(head)和尾结点(tail),没有成功获取同步状态的线程会成为节点加入该队列的尾部。

  • 当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

        

  • 首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点,设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可

解析AQS实现

以ReentrantLock的非公平锁为例,看看lock的实现。

  1. ReentrantLock.lock()—获取锁的入口

        public void lock() {sync.lock();}

    sync 实际上是一个抽象的静态内部类,它继承了 AQS 来实现重入锁的逻辑。

    Sync 有两个具体的实现类,分别是: NofairSync:表示可以存在抢占锁的功能,也就是说不管当前队列上是否存在其他 线程等待,新线程都有机会抢占锁 FailSync: 表示所有线程严格按照 FIFO 来获取锁。

    ReentrantLock的无参构造函数默认创建的是非公平锁。

    public ReentrantLock() {sync = new NonfairSync();}
  2. NonfairSync.lock()—获取同步状态/锁。

    static final class NonfairSync extends Sync {private static final long serialVersionUID = 7316153563782823691L;
    ​/*** Performs lock.  Try immediate barge, backing up to normal* acquire on failure.*/final void lock() {if (compareAndSetState(0, 1))setExclusiveOwnerThread(Thread.currentThread());elseacquire(1);}
    ​protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}}
    1. 非公平锁的特点:抢占锁的逻辑,不管有没有线程排队,上来先CAS抢占一下。

    2. CAS成功,表示成功获得锁。

    3. CAS失败,调用获取独占锁acquire()走锁竞争逻辑。

  3. AQS.acquire(1)—尝试获取独占锁or加入同步队列自旋获取锁。

    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}
    1. 通过 tryAcquire 尝试获取独占锁,如果成功返回 true,失败返回 false

    2. 如果 tryAcquire 失败,则会通过 addWaiter 方法将当前线程封装成 Node 添加 到 AQS 队列尾部。

    3. acquireQueued(),将 Node 作为参数,通过自旋去尝试获取锁。

  4. NonfairSync.tryAcquire(1)—尝试获取独占锁

    protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);}

    它是重写 AQS 类中的 tryAcquire 方法

  5. ReentrantLock.nofairTryAcquire(1)—尝试获取独占锁

    final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread(); // 获取当前执行的线程int c = getState(); // 获取state的值if (c == 0) { // 表示无锁状态if (compareAndSetState(0, acquires)) { // CAS替换state的值,case成功表示获取锁成功setExclusiveOwnerThread(current); // 保存当前获得锁的线程,下次再来的时候不用尝试竞争锁return true;}}else if (current == getExclusiveOwnerThread()) { // 如果同一线程竞争锁,直接增加重入次数int nextc = c + acquires;if (nextc < 0) // overflowthrow new Error("Maximum lock count exceeded");setState(nextc);return true;}return false;}
    1. 获取当前线程,判断当前的锁的状态

    2. 如果 state=0 表示当前是无锁状态,通过 cas 更新 state 状态的值

    3. 当前线程是属于重入,则增加重入次数

  6. AQS.addWaiter(Node.EXCLUSIVE) —线程构造成节点加入同步队列 (static final Node EXCLUSIVE = null;)

        private Node addWaiter(Node mode) {Node node = new Node(Thread.currentThread(), mode);// Try the fast path of enq; backup to full enq on failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}enq(node);return node;}

    当 tryAcquire 方法获取锁失败以后,则会先调用 addWaiter 将当前线程封装成 Node.

    入参 mode 表示当前节点的状态,传递的参数是 Node.EXCLUSIVE,表示独占状 态。意味着重入锁用到了 AQS 的独占锁功能

    1. 将当前线程封装成 Node

    2. 当前链表中的 tail 节点是否为空,如果不为空,则通过 cas 操作把当前线程的 node 添加到 AQS 队列

    3. 如果为空或者 cas 失败,调用 enq 将节点添加到 AQS 队列

  7. enq(node)—通过自旋把当前节点加入到队列中

    private Node enq(final Node node) {for (;;) {Node t = tail;if (t == null) { // Must initializeif (compareAndSetHead(new Node()))tail = head;} else {node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

    图解分析:

    8.AQS.acquireQueued(node, 1)—把node加入到链表去争抢锁

    1. 获取当前节点的 prev 节点

    2. 如果 prev 节点为 head 节点,那么它就有资格去争抢锁,调用 tryAcquire 抢占锁

    3. 抢占锁成功以后,把获得锁的节点设置为 head,并且移除原来的初始化 head 节点

    4. 如果获得锁失败,则根据 waitStatus 决定是否需要挂起线程

    5. 最后,通过 cancelAcquire 取消获得锁的操作

        final boolean acquireQueued(final Node node, int arg) {boolean failed = true;try {boolean interrupted = false;for (;;) {final Node p = node.predecessor(); // 获取当前节点的prev节点if (p == head && tryAcquire(arg)) { // 如果是head节点,说明有资格去争抢锁setHead(node); // 获取锁成功,也就是ThreadA已经释放了锁,然后设置head为ThreadB获得执行权限p.next = null; // help GCfailed = false;return interrupted;}// ThreadA可能还没释放锁,使得ThreadB在执行tryAcquire返回falseif (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {if (failed)cancelAcquire(node);}}

    9.shouldParkAfterFailedAcquire—竞争锁失败后应该挂起

    这个方法的主要作用是,通过 Node 的状态来判断,ThreadA 竞争锁失败以后是 否应该被挂起。

    1. 如果 ThreadA 的 pred 节点状态为 SIGNAL,那就表示可以放心挂起当前线程

    2. 通过循环扫描链表把 CANCELLED 状态的节点移除

    3. 修改 pred 节点的状态为 SIGNAL,返回 false.

    4. 返回 false 时,也就是不需要挂起,返回 true,则需要调用 parkAndCheckInterrupt 挂起当前线程

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { // pred是前置节点int ws = pred.waitStatus; // 前置节点的waitStatusif (ws == Node.SIGNAL) // 如果前置节点为 SIGNAL,意味着只需要等待其前置节点的线程被释放return true;if (ws > 0) { // ws大于 0,意味着prev节点取消了排队,直接移除这个节点就行do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 利用cas设置prev节点的状态为SIGNAL(-1)}return false;}

    图解分析:

    waitStatus = -1(SIGNAL:值为-1,表示后继节点需要被唤醒,即当前节点的释放会通知后继节点继续尝试获取锁或资源。)

    10.parkAndCheckInterrupt

    Thread.interrupted,返回当前线程是否被其他线程触发过中断请求,也就是thread.interrupt(); 如果有触发过中断请求,那么这个方法会返回当前的中断标识 true,并且对中断标识进行复位标识已经响应过了中断请求。如果返回 true,意味着在acquire方法中会执行 selfInterrupt()。

        private final boolean parkAndCheckInterrupt() {LockSupport.park(this);return Thread.interrupted(); // 1.中断 2.复位}

    selfInterrupt: 标识如果当前线程在 acquireQueued 中被中断过,则需要产生一 个中断请求,原因是线程在调用 acquireQueued 方法的时候是不会响应中断请求的。

    11.ReentrantLock.unlock()—锁释放

        public void unlock() {sync.release(1);}
        public final boolean release(int arg) {if (tryRelease(arg)) { // 释放锁成功Node h = head; // 获取aqs中的head节点if (h != null && h.waitStatus != 0) // 如果head节点不为空且状态!=0.调用unparkSuccessor(h)唤醒后续节点unparkSuccessor(h);return true;}return false;}

    12.ReentrantLock.tryRelease()—设置锁状态

    这个方法可以认为是一个设置锁状态的操作,通过将 state 状态减掉传入的参数值 (参数是 1),如果结果状态为 0,就将排它锁的 Owner 设置为 null,以使得其它 的线程有机会进行执行。 在排它锁中,加锁的时候状态会增加 1(当然可以自己修改这个值),在解锁的时 候减掉 1,同一个锁,在可以重入后,可能会被叠加为 2、3、4 这些值,只有 unlock() 的次数与 lock()的次数对应才会将 Owner 线程设置为空,而且也只有这种情况下才会返回 true.

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

    13.AQS.unparkSuccessor()—唤醒后续节点

    private void unparkSuccessor(Node node) {int ws = node.waitStatus; // 获取head节点的状态if (ws < 0)compareAndSetWaitStatus(node, ws, 0); // 设置head节点状态为0Node s = node.next; // 得到head节点的下一个节点//如果下一个节点为 null 或者 status>0 表示 cancelled 状态.if (s == null || s.waitStatus > 0) { s = null;//通过从尾部节点开始扫描,找到距离head最近的一个waitStatus<=0 的节点for (Node t = tail; t != null && t != node; t = t.prev)if (t.waitStatus <= 0)s = t;}if (s != null) // next节点不为空,直接唤醒这个线程即可LockSupport.unpark(s.thread);}

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

26591-2011 粮油机械 糙米精选机

声明 本文是学习GB-T 26591-2011 粮油机械 糙米精选机. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了糙米精选机的有关术语和定义、工作原理、型号及基本参数、技术要求、试验方法、检 验规则、标志、包装、运输和储存要求。 …

Python二级 每周练习题20

练习一: 日期计算器 设计一款日期计算程序&#xff0c;能否实现下面的功能&#xff1a; (1)要求用户分别输入年、月、日&#xff08;分三次输入&#xff09;&#xff1b; (2)程序自动会根据输入的年月日计算出这一天是这一年的第几天&#xff1b; (3)输出格式为&#xff1a;这…

C++——namespace std

命名空间&#xff08;namespace&#xff09; 0.使用方法 namespace 命名空间名 {... } 1. 每个命名空间都是一个作用域 同其他作用域类似&#xff0c;命名空间中的每个名字都必须表示该空间内的唯一实体。因为不同命名空间的作用域不同&#xff0c;所以在不同命名空间内可以…

巨人互动|Facebook海外户Facebook的特点优势

Facebook作为全球最大的社交媒体平台之一&#xff0c;同时也是最受欢迎的社交网站之一&#xff0c;Facebook具有许多独特的特点和优势。本文小编将说一些关于Facebook的特点及优势。 1、全球化 Facebook拥有数十亿的全球用户&#xff0c;覆盖了几乎所有国家和地区。这使得人们…

ios证书类型及其作用说明

ios证书类型及其作用说明 很多刚开始接触iOS证书的开发者可能不是很了解iOS证书的类型功能和概念。下面对iOS证书的几个方面进行介绍。 apple开发账号分类&#xff1a; 免费账号&#xff1a; 无需支付费用给apple&#xff0c;使用个人信息注册的账号 可以开发测试安装&…

文件内容显示

目录 1.浏览普通文件 1.1. 文件内容查看 1.1.1. cat 命令 例&#xff1a; 1.1.2 扩展tac命令&#xff1a; 1.1.3. more 命令 1.1.4. less命令 1.1.5. head命令 1.1.6. tail命令 1.2. 文件属性信息查看 1.2.1. file 命令 1.2.2. stat 命令 2. 文件内容过滤…

计算机竞赛 深度学习OCR中文识别 - opencv python

文章目录 0 前言1 课题背景2 实现效果3 文本区域检测网络-CTPN4 文本识别网络-CRNN5 最后 0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; **基于深度学习OCR中文识别系统 ** 该项目较为新颖&#xff0c;适合作为竞赛课题方向&#xff0c;…

DSOMEIP丢数据问题分析和总结:

&#xff08;1&#xff09;问题现象 无论使用arm64硬件、x86 ubuntu电脑、ubuntu docker哪种组合进行DSOMEIP event通信&#xff0c;接收端都会在event payload长度增加到一定程度时udp方式出现丢数据现象。 总体上arm64硬件略优于x86 ubuntu电脑&#xff0c;x86 ubuntu电脑略优…

CorelDRAW Graphics Suite2023绿色中文版本下载教程

CorelDRAW Graphics Suite2023版是领先的一体化软件包&#xff0c;它包括多个程序&#xff0c;如CorelDRAW、Corel PHOTO-PAINT、Corel CAPTURE、Corel Font Manager、Duplexing Wizard等&#xff0c;可全部安装&#xff0c;也可根据实际需要选择进行安装&#xff0c;都是最新版…

linux权限机制,

目录 用户与组,id,passwd 查看登录用户whomi,who,w 创建用户 useradd 修改用户信息usermod 删除指定用户userdel 组 ​编辑创建修改删除组groupadd groupmod groupdel 权限 ls-l 修改文件所属用户&#xff0c;所属组 chown,chgrp(change group) 修改权限 chmod 默认权…

CSS 模糊效果 CSS 黑白效果 CSS调整亮度 对比度 饱和度 模糊效果 黑白效果反转颜色

CSS 模糊效果 CSS 黑白效果 CSS调整亮度 饱和度 模糊效果 黑白效果 实现 调整亮度 饱和度 模糊效果 黑白效果 使用 filter1、模糊2、亮度3、对比度4、饱和度5、黑白效果6、反转颜色7、组合使用8、 filer 完整参数 实现 调整亮度 饱和度 模糊效果 黑白效果 使用 filter 1、模糊…

2023软工作业(一)——计算器

班级班级社区作业要求软件工程实践第一次作业-CSDN社区作业目标完成一个具有可视化界面的科学计算器参考文献Fyne 目录 作业要求 项目源码地址 作业目标 0. 界面及功能展示 1. PSP表格 2. 解题思路描述 3. 核心代码 4. 设计与实现过程 5. 程序性能改进 6. 单元测试展…

Qt扫盲-QSqlRelationalTableModel 理论总结

QSqlRelationalTableModel 理论总结 一、概述二、使用概述三、常用 一、概述 QSqlRelationalTableModel的行为类似于QSqlTableModel&#xff0c;但允许将列设置为进入其他数据库表的外键。 二、使用概述 在上面左边的截图显示了 QTableView 中的普通 QSqlTableModel。外键(…

分类预测 | MATLAB实现WOA-CNN-GRU-Attention数据分类预测

分类预测 | MATLAB实现WOA-CNN-GRU-Attention数据分类预测 目录 分类预测 | MATLAB实现WOA-CNN-GRU-Attention数据分类预测分类效果基本描述模型描述程序设计参考资料 分类效果 基本描述 1.MATLAB实现WOA-CNN-GRU-Attention数据分类预测&#xff0c;运行环境Matlab2021b及以上&…

Ubuntu 安装PostgreSQL

网上有各种版本的&#xff0c;也可以去官网看官方的文档。我是下载的PostgreSQL-11.4版本的。找到以后直接复制网上的压缩包链接就可以。 $ mkdir /opt/postgresql && cd /opt/postgresql $ wget https://ftp.postgresql.org/pub/source/v11.4/postgresql-11.4.tar.gz…

基于规则架构-架构案例2019(三十九)

电子商务 某电子商务公司为了更好地管理用户&#xff0c;提升企业销售业绩&#xff0c;拟开发一套用户管理系统。该系统的基本功能是根据用户的消费级别、消费历史、信用情况等指标将用户划分为不同的等级&#xff0c;并针对不同等级的用户提供相应的折扣方案。在需求分析与架…

运行软件mfc100u.dll缺失是怎么办?mfc100u.dll丢失解决方法分享

Mfc100u.dll 丢失的问题可能困扰着许多使用计算机的用户。Mfc100u.dll 是一个重要的动态链接库文件&#xff0c;它包含了许多功能模块&#xff0c;如字符串处理、数学计算、文件操作等。当 Mfc100u.dll 文件丢失或损坏时&#xff0c;可能会导致许多应用程序无法正常运行&#x…

OS 模拟进程状态转换

下面的这个博主写的很好 但是他给的代码print部分和语言风格python三识别不了 这个特别感谢辰同学帮我调好了代码 我放在主页上了 估计过两天就可以通过了 《操作系统导论》实验一&#xff1a;模拟进程状态转换_process-run.py-CSDN博客 这个补充一下他没有的&#xff1a;OS…

基于yum制作kylin系统docker镜像

注意&#xff0c;由于线上源版本与iso源存在差异&#xff0c;应采用iso源制作docker镜像 [rootlocalhost yeqiang]# yum install --installroot/home/yeqiang/kylin-docker/ yum 无法找到发布版本&#xff08;可用 --releasever 指定版本&#xff09; 警告&#xff1a;加载 /e…

正交对角化,奇异值分解

与普通矩阵对角化不同的是&#xff0c;正交对角化是使用正交矩阵对角化&#xff0c;正交矩阵是每列向量都是单位向量&#xff0c;正交矩阵*它的转置就是单位矩阵 与普通矩阵对角化一样&#xff0c;正交对角化的结果也是由特征值组成的对角矩阵 本质还是特征向量对原矩阵的拉伸…