【JAVA多线程】AQS,JAVA并发包的核心

news2024/9/20 9:29:29

目录

1.概述

1.1.什么是AQS

1.2.AQS和BlockQueue的区别

1.3.AQS的结构

2.源码分析

2.1.CLH队列

2.2.模板方法的实现

2.2.1.独占模式

1.获取资源

2.释放资源

2.2.2.共享模式


1.概述

1.1.什么是AQS

AQS非常非常重要,可以说是JAVA并发包(java.util.concurrent)的核心。

以下是一个常见的多线程场景:

当多个线程对同一个资源进行争抢时,没有抢到的线程怎么办?直接拒绝然后丢弃?最可行最合适的方式当然是让没有抢到资源的线程排队阻塞起来,这样即让出了CPU的资源,又方便当资源被释放、可争抢的时候唤醒线程来继续争抢。

我们发现一个信号量+一个线程安全的用来存放线程的队列,是并发编程体系的一个底座,这个底座有一个名字叫——同步器。

AQS(AbstractQueuedSynchronizer),一个JDK中提供的同步器,提供一系列线程的控制和同步机制,是JDK中诸多需要进行线程控制和同步的地方,诸如可重入锁ReentrantLock,以及诸多同步工具Semaphore、CountDownLatch、CyclicBarrier等,其底层都是使用的AQS来实现线程控制和同步。

1.2.AQS和BlockQueue的区别

阻塞队列(Blocking Queue)是一种特殊的队列,它的特殊之处在于它具有阻塞特性。当队列为空时,从队列中获取元素的线程会被阻塞,直到队列中有新的元素被加入;同样地,当队列满时,试图向队列中添加元素的线程也会被阻塞,直到队列中有空余的空间。这种特性使得阻塞队列非常适用于生产者-消费者模式中。

阻塞队列:

  • 元素的读写是线程安全的

  • 里面存的任务

  • 阻塞的是外部的生产者线程/消费者线程

AQS,同步器,是用来进行线程控制和同步的一个基础组件,用来让现场排队、阻塞、唤醒等操作。

AQS:

  • 元素的读写是安全的

  • 里面存的是线程

  • 阻塞的是内部存的线程

1.3.AQS的结构

AQS提供了什么?

一套API和一套数据结构。

可以看到AQS是一个抽象类:

既然是抽象类而不是直接是个接口就说明,其提供的不只是一个标准,而是一套资源。其底层用链表来组织其管理的线程,包含:

  • 链表的节点Node

  • 链表的头尾指针

  • 状态state,这是个volatile变量用于表示同步状态,在不同的实现类中,它的含义不同,有可能表示锁是否被持有、持有者持有锁的次数、信号量可用的许可数量等。

  • 条件变量ConditionObject

AQS提供了一套标准的API,来完成两方便的操作:

  • 状态控制

  • 资源争抢

状态控制:

  • getState():返回同步状态

  • setState(int newState):设置同步状态

  • compareAndSetState(int expect, int update):使用CAS设置同步状态

  • isHeldExclusively():当前线程是否持有资源 独占资源(不响应线程中断)

资源争抢和释放一共两种,共享模式、独占模式:

  • tryAcquire(int arg):独占式获取资源,子类实现

  • acquire(int arg):独占式获取资源模

  • tryRelease(int arg):独占式释放资源,子类实现

  • release(int arg):独占式释放资源模板 共享资源(不响应线程中断)

  • tryAcquireShared(int arg):共享式获取资源,返回值大于等于0则表示获取成功,否则获取失败,子类实现

  • acquireShared(int arg):共享式获取资源模板

  • tryReleaseShared(int arg):共享式释放资源,子类实现

  • releaseShared(int arg):共享式释放资源模板

独占模式出现在对一把锁进行争抢、释放的场景中,如ReentrantLock中;共享模式出现在对多个资源的争抢、释放的场景中,如ReadWriteLock、CountDownLatch中。

2.源码分析

2.1.CLH队列

在AQS的源码开头,就描述了其是使用CLH队列来组织没争抢到资源的线程的:

该队列是用链表实现的,没被争抢到资源的线程会被封装成Node,被挂入链表中。node的入队和出队都用的CAS(自旋锁)来保证效率和安全的兼得。

这个Node类源码很简单,主要是要注意Node是有状态的,用状态来控制该节点该不该被唤醒,用waitStatus来表示状态:

Node 类中的静态常量定义了以下状态:

  • SIGNAL (-1):表示当前节点的后续节点需要被唤醒。这是默认状态,当一个节点被添加到队列中时,它的 waitStatus 会被设置为 -1。

  • PROPAGATE (-3):类似于 SIGNAL,但在某些情况下,如释放共享模式下的锁时,可能会设置为 -3,以帮助传播唤醒信

  • CONDITION (-2):表示节点位于条件队列中,而不是同步队列中。这通常与 Condition 对象相关联。

  • 0:表示没有特别的状态。当节点被初始化时,它的 waitStatus 默认为 0。

  • CANCELLED (1):表示节点已经被取消。这通常发生在线程被中断或者尝试获取锁失败时。一旦节点被标记为 CANCELLED,它就会从队列中移除。

waitStatus 的使用:

  • 线程阻塞:当一个线程被插入到队列中时,它的 waitStatus 通常会被设置为 SIGNAL。这意味着当前节点的后续节点应该被唤醒,当当前节点释放时。

  • 线程唤醒:当一个线程释放锁或其他同步状态时,它会检查自己的 waitStatus,并根据情况唤醒后继节点。例如,在独占模式下,当线程释放锁时,它会检查其后继节点的 waitStatus,如果是 SIGNAL,则会唤醒该节点对应的线程。

  • 条件队列:当线程等待某个条件满足时,它会被移到条件队列中,并且其 waitStatus 会被设置为 CONDITION。当条件满足时,线程会被放回同步队列,并且 waitStatus 会被设置为 SIGNAL。

  • 取消节点:如果线程被中断或超时,或者由于其他原因无法继续等待,那么该节点会被标记为 CANCELLED,并且从队列中移除。

整个CLH队列可以理解为:

2.2.模板方法的实现

2.2.1.独占模式

1.获取资源

AQS的实现类很多,此处我们以ReentrantLock的FairSync(公平锁)为例。

lock()其实是去,调用AQS的acquire():

AQS的acquire()会去调用实现类的tryAcquire()来判断当前线程是否能抢到线程,如果抢不到就入队CLH,并且阻塞:

至此我们就能体会到了,AQS实现了核心的阻塞+入队以及唤醒+出队的一些列对CLH中线程的操作,但具体的争抢策略作为模板开放出去了,也就是上文我们说的一系列模板方法。

我们来看看FairSync的tryAcquire()实现:

protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();//获取锁的状态
            if (c == 0) {//如果锁没有被持有
                //就去判断CLH队列中还有没有线程,没有的话就去CAS获取锁
                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;
        }
2.释放资源

释放资源应该做些什么?这里我们想一下就能想出来:

  • 首先释放锁

  • 然后唤醒后面的线程。

释放锁:

可以看到ReentrantLock的unlock调用的AQS的release()去释放锁,AQS的release里会先去tryRelease()尝试释放锁,tryRelease()是交给子类重写的:

所以调用的就是reentrantLock的释放策略:

ReentrantLock的尝试释放的过程很简单,就是线程安全的去减少锁的持有次数+清除锁的持有者的信息:

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

唤醒后面的线程:

继续回到AQS的release,继续往下看,tryRelease()后确定可以释放锁后,unparkSuccessor()唤醒CLH队列中需要被唤醒的第一个线程。为什么说是第一个需要被唤醒的喃?前文已经说过了Node节点有状态,状态用来表示该节点是否需要被唤醒,有些状态是表示节点不需要被唤醒的。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);//唤醒后面的需要被唤醒的线程
            return true;
        }
        return false;
    }

unparkSuccessor()很简单,修改当前节点状态,唤醒后面需要唤醒的状态:

private void unparkSuccessor(Node node) {
        //将当前的节点从待唤醒状态变为已经唤醒
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
​
        //找到后续第一个需要被唤醒的节点,然后唤醒它
        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);
    }

2.2.2.共享模式

之所以有共享模式,是因为有时候资源不一定是唯一的,比如除了锁之外,可能要求能有10条线程同时工作,又或者读写之间不互斥等需求。在这种允许多条线程同时工作的情景中就要用到共享模式,标志位state不再表示一个意思(是否被持有),而是用来表示多个意思,有可能是线程的数量,也有可能是高低位分别表示是否允许读或者写。

共享模式下,资源的获取是通过acquireShared和tryAcquireShared来实现的,释放是通过releaseShared和tryReleaseShared来实现的。具体代码实现的话其实和独占模式大差不差,只是在对标志位state的操作上略有不同,以及可能会有一些位操作。以下是一些常见的使用共享模式的场景:

  • 读写锁 (ReadWriteLock): 读写锁是一种常用的同步工具,它允许多个线程同时读取共享资源,但在写入时需要独占资源。 读锁通常使用共享模式实现,因为多个读线程可以同时访问资源。 写锁则使用独占模式,因为写入操作通常需要独占资源以避免数据不一致。

  • 信号量 (Semaphore): 信号量用于限制可以同时访问共享资源的线程数量。 它可以通过减少或增加一个计数器来管理资源的可用性,这个计数器通常就是 AQS 的 state 字段。 当线程尝试获取信号量时,它会减少 state 的值;当线程释放信号量时,它会增加 state 的值。 因为多个线程可以同时获取信号量,所以信号量通常使用共享模式。

  • 循环屏障 (CyclicBarrier): 循环屏障允许一组线程相互等待,直到所有线程都到达了一个共同的屏障点。 当最后一个线程到达屏障点时,所有线程都会被释放继续执行。 循环屏障通常使用共享模式来管理线程的等待和释放。

  • CountDownLatch: CountDownLatch 是一种同步辅助类,它允许一个或多个线程等待其他线程完成操作。 它维护了一个计数器,每当一个操作完成时,计数器就会减少。 当计数器达到零时,所有等待的线程都会被释放。 CountDownLatch 通常使用共享模式来管理计数器的减少。

  • Exchanger: Exchanger 允许两个线程交换对象。 两个线程各自提供一个对象并等待另一个线程到达交换点。 一旦两个线程都到达交换点,它们就可以交换对象并继续执行。 Exchanger 通常使用共享模式来管理线程的交换过程。 示例代码

由于这是一个系列文章,后面几篇文章我们就将聊到CountDownLatch、Semaphore、CyclicBarrier之类的同步工具类、ReadWriteLock之类的读写锁,到时候分析源码的时候,再来看看各自的具体实现。

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

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

相关文章

MES是什么?MES系统主要包括哪些功能?

一、MES系统是什么&#xff1f; MES是&#xff08;Manufacturing Execution System&#xff09;的缩写&#xff0c;中文名称叫做制造企业生产过程执行管理系统&#xff0c;是一套整体的面向制造企业车间执行层生产信息化管理的解决方案。MES系统经历了若干个发展阶段&#xff…

PHP简单零售收银台系统源码小程序

&#x1f6d2;轻松上手&#xff01;简单零售收银台系统&#xff0c;让经营更省心&#x1f4b8; &#x1f680; 开篇&#xff1a;告别繁琐&#xff0c;拥抱高效收银新时代 嘿&#xff0c;小店主们&#xff01;&#x1f44b; 还在为每天繁琐的收银工作头疼吗&#xff1f;是时候…

探索腾讯云AI代码助手:智能编程的新时代

智能编程的新时代 前言开发环境介绍腾讯云 AI 代码助手使用实例生成文档解释代码生成测试修复代码人工智能技术对话 智能编程获得的帮助与提升对腾讯云AI代码助手的建议结语 前言 hello&#xff0c;大家好我是恒川&#xff0c;今天我来给大家安利一款非常好用的AI 代码助手&…

JVM(面试用)

目录 一、JVM运行时数据区 二、JVM类加载 类加载过程 1、加载&#xff08;loading&#xff09; 2、验证&#xff08;Verification&#xff09; 3、准备&#xff08;Perparation&#xff09; 4、解析&#xff08;Resolution&#xff09; 5、初始化&#xff08;Initializ…

Linux 驱动开发究竟在开发什么?

文章目录 1 Linux 驱动开发架构图2 更具体的例子&#xff1a;LED 驱动程序2.1 硬件层&#xff08;Hardware Layer&#xff09;2.2 固件层&#xff08;Firmware Layer&#xff09;2.3 驱动程序层&#xff08;Driver Layer&#xff09;2.4 操作系统内核&#xff08;Kernel Layer&…

【全国大学生电子设计竞赛】2021年A题

&#x1f970;&#x1f970;全国大学生电子设计大赛学习资料专栏已开启&#xff0c;限时免费&#xff0c;速速收藏~

2024年睿抗机器人开发者大赛(RAICOM)国赛题解

目录 RC-u1 大家一起查作弊 分数 15 RC-u2 谁进线下了&#xff1f;II 分数 20 RC-u3 势均力敌 分数 25 RC-u4 City 不 City 分数 30 RC-u5 贪心消消乐 分数 30 RC-u1 大家一起查作弊 分数 15 简单模拟题&#xff0c;对于多行读入使用while(getline(cin…

切割 Nginx 日志

目录 方式一&#xff1a;自定义脚本 方式二&#xff1a;logrotate crontab 讲解 centos 容器安装 crontab centos 容器 systemctl 命令执行异常 切割理由&#xff1a;假设一个网站访问量特别大&#xff0c;每天 access_log 文件有 2 个 G&#xff0c;如果想从文件中查找…

基于QCustomPlot实现色条(ColorBar)

一、简介 通过QCustomPlot实现ColorBar&#xff0c;直观显示各个位置的异常情况。实现效果如下&#xff0c; 二、源码 CPColorBar.hpp // CPColorBar.hpp #pragma once #include "qcustomplot.h"#include <QHash>class QCP_LIB_DECL CPColorBarData { pub…

使用 MRI 构建的大脑连接网络预测帕金森病萎缩进展模式| 文献速递-基于深度学习的乳房、前列腺疾病诊断系统

Title 题目 Brain Connectivity Networks Constructed Using MRI for Predicting Patterns of Atrophy Progression in Parkinson Disease 使用 MRI 构建的大脑连接网络预测帕金森病萎缩进展模式 Background 背景 Whether connectome mapping of structural and across …

全志T527-TP9930-Camera

一、简介 1、TP9930 TP9930 驱动模块主要实现将 4 路的 Camera 的数据转换为 BT656/BT1120 数据&#xff0c;从而实现在 T527 端来对数据进行处理和送显。 2、BT656/BT1120简介 BT656主要是针对PAL/NTSC等标清视频。随着高清视频的发展需要&#xff0c;又推出了BT1120标准&…

AI + Coding:可以有多少种玩法?

在当今快速发展的科技时代&#xff0c;人工智能&#xff08;AI&#xff09;和编程已经成为不可分割的两大领域。AI赋予了计算机更多的智能&#xff0c;使其能够处理复杂的数据、执行高级任务&#xff0c;而编程是实现这一切的基础。当AI与编程结合在一起时&#xff0c;会带来无…

图片懒加载与预加载(原生)

1、懒加载。 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title> </head>…

【开端】JAVA Mono<Void>向前端返回没有登陆或登录超时 暂无权限访问信息组装

一、绪论 JAVA接口返回信息ServerHttpResponse response 等登录接口token过期时需要给前端返回相关状态码和状态信息 二、Mono<Void>向前端返回没有登陆或登录超时 暂无权限访问信息组装 返回Mono对象 public abstract class Mono<T> implements CorePublisher…

2024最新Mysql事务原理与优化最佳实践

概述 我们的数据库一般都会并发执行多个事务&#xff0c;多个事务可能会并发的对相同的一批数据进行增删改查操作&#xff0c;可能就会导致我们说的脏写、脏读、不可重复读、幻读这些问题。 这些问题的本质都是数据库的多事务并发问题&#xff0c;为了解决多事务并发问题&…

在java中通过subString方法来截取字符串中的文本

1、subString&#xff08;&#xff09;常规用法可以通过下标来进行获取&#xff0c;在java中是从0开始&#xff0c;前包括后不包括。 String str “Hello Java World!”; 用法一: substring(int beginIndex) 返回从起始位置&#xff08;beginIndex&#xff09;至字符串末尾…

供应链库存管理面临什么问题?全面解析安全库存和周转库存!

在当今这个快速变化的商业世界中&#xff0c;供应链管理已成为企业获取竞争优势的核心领域。库存管理&#xff0c;作为供应链中的关键环节&#xff0c;直接关系到企业的成本控制、客户服务水平以及市场响应速度。然而&#xff0c;面对市场竞争的加剧和客户需求的多变&#xff0…

事务性邮件调用接口如何配置灵活调用策略?

事务性邮件调用接口性能怎么优化&#xff1f;如何使用接口调用&#xff1f; 如何配置灵活调用策略&#xff0c;不仅可以提升邮件发送的效率和可靠性&#xff0c;还能增强用户体验。AokSend将详细介绍事务性邮件调用接口的配置方法和策略&#xff0c;以便企业在实际应用中取得最…

深度学习读书笔记(1)--机器学习、人工智能、深度学习的关系

声明&#xff1a;本文章是根据网上资料&#xff0c;加上自己整理和理解而成&#xff0c;仅为记录自己学习的点点滴滴。可能有错误&#xff0c;欢迎大家指正。 阅读的书籍主要为《UnderstandingDeepLearning》《动手学深度学习》 1956 年提出 AI 概念&#xff0c;短短3年后&…

【初阶数据结构题目】14.随机链表的复制

随机链表的复制 点击链接做题 思路&#xff1a; 浅拷贝&#xff1a;拷贝值 深拷贝&#xff1a;拷贝空间 在原链表的基础上继续复制链表置random指针复制链表和原链表断开 代码&#xff1a; /*** Definition for a Node.* struct Node {* int val;* struct Node *next…