JUC高级十-并发加锁原理之AbstractQueuedSynchronizer(AQS)

news2024/12/25 9:22:56

1. 前置知识

  • 公平锁和非公平锁
  • 可重入锁
  • 自旋锁
  • LockSupport
  • 数据结构之双向链表
  • 设计模式之模板设计模式

AQS重要性

JAVA ------>JVM

AQS ------>AQS

2. AQS入门级别理论知识

2.1 是什么?

2.1.1 字面意思

Abstract Queued Synchronizer----抽象的队列同步器

源码位置:

AbstractQueuedSynchronizer和AbstractQueuedLongSynchronizer是AbstractOwnableSynchronizer的子类

image-20230415100107662

并且是一个抽象类:

image-20230415100320554

2.1.2 技术解释

image-20230415102708930

  • 是用来构建锁或者其它同步器组件的重量级基础框架及整个JUC体系的基石,主要用于解决锁分配给"谁"的问题
  • 通过内置的FIFO队列来完成资源获取线程的排队工作,并通过一个int类变量
    表示持有锁的状态

CLH队列(FIFO)

CLH:Craig、Landin and Hagersten 队列,是一个单向链表,AQS中的队列是CLH变体的虚拟双向队列FIFO

image-20230415100847071

2.2 AQS为什么是JUC内容中最重要的基石?

2.2.1 和AQS关联的技术

AQS

JUC的以下技术都是以AQS为基石

  • Semaphore
  • CycleBarrier
  • ReentranReadWriteLock
  • CountDownLatch
  • ReentranLock

AQS子类:

image-20230415101859403

2.2.2 锁和同步器的关系

  • 锁,面向锁的使用者
    • 定义了程序员和锁交互的使用层API,隐藏了实现细节,你调用即可。
  • 同步器,面向锁的实现者
    • 比如Java并发大神DougLee,提出统一规范并简化了锁的实现,
      屏蔽了同步状态管理、阻塞线程排队和通知、唤醒机制等。

2.3 AQS能干嘛?

因为:加锁会导致阻塞

所以:有阻塞就需要排队,实现排队必然需要队列

如果共享资源被占用,就需要一定的阻塞等待唤醒机制来保证锁分配。这个机制主要用的是CLH队列的变体实现的,将暂时获取不到锁的线程加入到队列中,这个队列就是AQS的抽象表现。它将请求共享资源的线程封装成队列的结点(Node),通过CAS自旋以及LockSupport.park()的方式,维护state变量的状态,使并发达到同步的效果。

3 AQS内部体系架构

image-20230415103008431

image-20230415102854436

3.1 AQS自身

3.1.1 AQS的int变量state-volatile修饰

代表同步状态标志位

  • 0代表资源没有被线程占用,资源现在是空闲状态
  • 1代表资源被线程占用,资源现在不是空闲状态

3.2 内部类Node(Node类在AQS类内部)

image-20230415112629037

image-20230415112746065

3.2.1 Node对象两种模式

  • SHARED(共享模式)
    • 标志节点正在等待共享模式的标记
  • EXCLUSIVE(独占模式)
    • 标志节点正在独占模式等待的标记

3.2.2 Node的Node的int变量waitState-volatile修饰

每个Node节点在AQS队列中的等待状态,Node初始化时waitState为0

  • CANCELLED(取消)=1
    • 表示该Node节点为取消状态,需要出队
  • SIGNAL(标志)=-1
    • 表示该节点的后一个节点需要unparking(LockSupport里的知识)
  • CONDITION(条件)=-2
    • 表示该节点正在等待某种条件激活
  • PROPAGATE(传播)=-3
    • 指示下一个 acquireShared 应该无条件传播的 waitStatus 值

只有CANCELLED状态是大于0的,判断时有需要先记住

3.2.3 prev&next 双向链表

记录该节点的前一个节点和后一个节点

3.3 以上得出结论:AQS同步队列的基本结构

CLH:Craig、Landin and Hagersten 队列,是个单向链表,AQS中的队列是CLH变体的虚拟双向队列(FIFO)

image-20230415113031991

4 AQS源码分析之ReentranLock

image-20230415132318457

4.1 架构

image-20230415115416750

上图可知:

  • ReentranLock实现了Lock接口
  • ReentranLock中的内部类NonfairSync(非公平锁)和FairSync(公平锁)继承了Sync
  • Sync又继承了AQS抽象类

4.2 ReentranLock公平锁与非公平锁

image-20230415120249600

上图可以得知:

  • new ReentranLock()和new ReentranLock(false)都是非公平锁
  • new ReentranLock(true) 表示公平锁

4.2.1 公平锁和非公平锁

4.2.1.1 公平锁和非公平锁的lock方法

提前了解acquire方法会调用tryAcquire,不管公平锁还是非公平锁最后都会调用acquire方法

image-20230415132026967

  • 因为非公平的的tryAcquire不讲武德,不排队所以我们在抢锁之前需要看看锁的状态status是否为0,不然队列中所有节点不管status是否为0都去强太耗费性能.
  • 而公平锁在tryAcquire的时候将武德,只有成为头结点才会去抢,所以直接acquire

image-20230415122841881

4.2.1.2 非公平锁的tryAcquire

非公平锁tryAcquire返回值:

  1. 当AQS status为0返回true
  2. 是重入锁的情况下返回true

image-20230415123058397

nonfairTryAcquire()方法

image-20230415124133044

4.2.1.3 公平锁的tryAcquire

公平锁tryAcquire返回值:

  1. 当AQS status为0并且当前节点是头结点时返回true
  2. 是重入锁的情况下返回true

image-20230415124400940

参考下图及结论:

!hasQueuedPredecessors()的意思就是如果在当前线程之前有排队的线程就不尝试去抢锁了,老老实实的排队,这不就是公平锁吗?

非公平锁没有这个条件约束,所以不管是排在那个位置的线程都会去抢一抢

hasQueuedPredecessors()方法

  • 如果在当前线程之前有排队的线程,则为true ;
  • 如果当前线程位于队列的头部或队列为空,则为false

image-20230415125536570

总结:

  • 可以明显看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:
    hasQueuedPredecessors():
    hasQueuedPredecessors是公平锁加锁时判断等待队列中是否存在有效节点的方法

image-20230415125807397

4.3 以非公平锁为例继续后面的源码分析

tryAcquire方法在4.2章节以及分析后续我们主要针对

  • addWaiter
  • acquireQueued
  • selfInterrupt

以上三个方法进行分析

image-20230415132026967

4.3.0 acquire方法总流程

acquire

4.3.1 addWaiter(Node.EXCLUSIVE)

将加锁失败的节点加入队列

当tryAcquire()返回false的情况下才会执行addWaiter

  • 公平锁:

    以下条件只要包含一个就返回false

    1. status不等于0
    2. 当前节点不是头结点
    3. 也不是可重入锁
  • 非公平锁

    以下条件只要包含一个就返回false

    1. status不等于0
    2. 也不是可重入锁

image-20230415141207437

enq(Node node)

image-20230415140908057

4.3.2 acquireQueued(final Node node, int arg)

  • 将队列中各节点的等待状态waitStatus进行管理
    • waitStatus为-1的节点park
    • waitStatus为1的移出队列;
    • 其他状态值会逐步修改为-1,
    • 最后将waitStatus为-1的节点对应的线程park
  • 在addWaiter的大自旋中会不断tryAcquire(),检测共享资源是否解锁

addWaiter后会返回最新的尾节点

image-20230415145653986

predecessor()方法 返回尾结点的前置节点

image-20230415141624556

shouldParkAfterFailedAcquire(Node pred, Node node)

该方法主要作用就是将修改抢锁失败的节点waitStatus为-1等待状态,或者将想要取消的节点从队列中移除
如果前驱节点的 waitStatus 是 SIGNAL状态,即 shouldParkAfterFailedAcquire 方法会返回 true 程序会继续向下执行 parkAndCheckInterrupt 方法,用于将当前线程挂起

image-20230415145208528

image-20230415150527046

parkAndCheckInterrupt()

阻塞当前节点

image-20230415145734077

image-20230415150547678

4.3.3 selfInterrupt()

抢锁失败,加入队列成功那么就中断当前线程

image-20230415150048568

4.4 unlock解锁源码分析

解锁操作,主要作用是将头结点后为等待状态-1的节点unpark,然后唤醒阻塞的线程,以及一些异常处理

unlock主要就是调用AQS的释放方法release

image-20230415151000969

release开始会tryAcquire尝试解锁

image-20230415152139945

tryAcquire尝试解锁

image-20230415151810148

unparkSuccessor方法将中断节点恢复至正常状态

image-20230415154326651

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

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

相关文章

【Microsoft Edge】如何彻底卸载 Edge

文章目录 一、问题描述二、卸载 Edge2.1 卸载正式版 Edge2.2 卸载非正式版 Edge2.2.1 卸载通用的 WebView22.2.2 卸载 Canary 版 Edge2.2.3 卸载其他版本2.3 卸载 Edge Update 2.4 卸载 Edge 的 Appx 额外安装残留2.5 删除日志文件2.6 我就是想全把 Edge 都删了 一、问题描述 …

预测模型的局部评价?

预测模型的局部评价 为什么要进行局部评价? 首先是临床决策曲线分析通常会给预测模型的使用规定一个阈值范围,相应地预测模型的评价也应该局限在这个范围之内才是合理的; 其次,全局性地评价往往不够敏感,即好的模型和坏…

本地搭建属于自己的ChatGPT:基于Python+ChatGLM-6b+Streamlit+QDrant+DuckDuckGo

本地部署chatglm及缓解时效性问题的思路: 模型使用chatglm-6b 4bit,推理使用hugging face,前端应用使用streamlit或者gradio。 微调对显存要求较高,还没试验。可以结合LoRA进行微调。 缓解时效性问题:通过本地数据库…

C语言实现银行家算法

一.银行家算法 1.由来 银行家算法最初是由荷兰计算机科学家艾兹赫尔迪杰斯特拉(Edsger W. Dijkstra)于1965年提出的。当时他正致力于解决多道程序设计中产生的死锁问题。在多道程序设计中,由于不同进程之间共享有限的系统资源,如…

【JavaEE初阶】多线程(一)认识线程 线程的创建 Thread的用法

摄影分享! 文章目录 认识线程(Thread)概念执行多线程编程创建线程的写法1.继承Thread,重写run2.实现Runnable接口3.使用匿名内部类,继承Thread4.使用匿名内部类,实现Runable5.使用Lambda表达式 Thread用法…

C语言模拟银行排队叫号(链队)

一.队列 队列是一种具有先进先出(FIFO)特性的线性数据结构,它只允许在队列的两端进行插入和删除操作。队列的一端称为队尾(rear),另一端称为队头(front)。新元素总是插入在队列的队…

怎么把m4a转换成mp3,分享几个方法给大家!

录音文件中经常出现m4a后缀的音频格式,但通常只能在特定的音频播放器中播放。如果你想把m4a转换成mp3,下面是四种简单易行的方法,适用于Windows 10操作系统。 方法一:使用记灵在线工具转换m4a成mp3 工具地址:记灵在线…

elsticsearch入门

查看所有索引(表) 向索引(表)中添加数据: 自定义id添加数据: 自定义id添加数据:方式二 查询数据: 查询索引(表)中全部数据: 全量修改单条数据&…

File类与IO流

1. java.io.File类的使用 1.1 概述 File类及本章下的各种流,都定义在java.io包下。一个File对象代表硬盘或网络中可能存在的一个文件或者文件目录(俗称文件夹),与平台无关。(体会万事万物皆对象)File 能新…

【Leetcode -剑指Offer 22.链表中倒数第k个结点 -203.移除链表元素】

Leetcode Leetcode -剑指Offer 22.链表中倒数第k个结点Leetcode -203.移除链表元素 Leetcode -剑指Offer 22.链表中倒数第k个结点 题目:输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表…

数据结构(三)—— 哈希表

文章目录 一、哈希表积累1.1 哈希map1.2 哈希set 二、哈希表基础三、题3.1 242 有效的字母异位词3.2 349 两个数组的交集3.3 202 快乐数3.4 1 两数之和3.5 54 四数相加II 一、哈希表积累 什么时候想到用哈希法:当要需要查询一个元素是否出现过、判断一个元素是否出…

awvs安装批量扫描

文章目录 安装批量扫描 安装 1.2.AWVS下载   该工具可在官方网站下载,但免费下载的是14天试用版本。   官网下载:AWVS   百度云下载:百度云下载https://pan.baidu.com/s/1UO7GzL0CMemJ_TMQnHNOuA?pwdg1bm 提取码:g1bm 批…

浏览器状态同步和路由-前端路由和服务端路由原理

目录 前端路由和History API 浏览文境(Browser Context) 会话历史(Session History) History API history.go()切换当前会话,并不改变会话栈 history.back() history.go(-1) & history.forward() history.…

5.4、服务器编程基本框架和两种高效的事件处理模式

5.4、服务器编程基本框架和两种高效的事件处理模式 1.服务器编程基本框架2.两种高效的事件处理模式①Reactor模式②Proactor模式③模拟Proactor模式 1.服务器编程基本框架 模块功能I/O 处理单元处理客户连接,读写网络数据逻辑单元业务进程或线程网络存储单元数据库、…

18.网络爬虫—Scrapy实战演示

网络爬虫—Scrapy实战演示 Scrapy Shell简介进入shell调试网站启动Scrapy Shell 查看目标网站获取网站源代码常用方法调试xpath提取数据Scrapy请求子页面请求及返回处理创建项目创建爬虫 数据解析写入csv文件后记 前言: 🏘️🏘️个人简介&…

Java,jdbc,jvm

1、数据删除 物理删除 直接发送delete语句 就是物理删除 这种删除 删除之后不可恢复逻辑删除 本质是更新 0 表示不可用 1 可用状态 update emp set is_active where id ?2、数据更新 1 显示所有数据 2. 点击修改按钮 此时 应该执行的动作-查询该用户信息 目的是将当前用户…

并发编程的那些事

目录 一、并发编程的目的 二、线程和进程2.1 什么是线程2.2 进程2.3 一个普通Java 程序包含哪些线程 三、并发、并行四、线程的六个状态五、wait 和sleep的区别5.1 位于不同的类5.2 关于锁的释放 一、并发编程的目的 并发编程的目的是为了让程序运行得更快,但是&…

类ChatGPT逐行代码解读(1/2):从零起步实现Transformer、ChatGLM-6B

前言 最近一直在做类ChatGPT项目的部署 微调,关注比较多的是两个:一个LLaMA,一个ChatGLM,会发现有不少模型是基于这两个模型去做微调的,说到微调,那具体怎么微调呢,因此又详细了解了一下微调代…

ggplot中坐标轴和图例的相关处理

文章目录 改变坐标轴和图例的名称方法1, labs()方法2,scale_xxx_discrete/continuous() 删除坐标轴和图例的名称方法1, labs()方法2,scale_xxx_discrete/continuous()方法3,theme()方法4,guides()可以去图例名称 改变图…

osgwidget 使用 方法以及案例分享

osgwidget 使用 方法以及案例分享 按钮 一个常见的 osg::Widget 就是按钮。下面的代码展示了如何使用 osg::Switch 和 osgText 创建一个简单的按钮&#xff1a; osg::ref_ptr<osg::Switch> buttonSwitch new osg::Switch(); osg::ref_ptr<osgText::Text> buttonTe…