AQS抽象同步队列核心原理

news2024/11/15 19:25:26

CLH自旋锁

JUC中显式锁基于AQS抽象队列同步器,而AQS是CLH锁的一个变种。队列头结点可以获得锁,其他节点排队等候。

在这里插入图片描述

在争夺锁激烈的情况下,为了减少CAS空自旋(CAS需要CPU进行内部通信保证缓存一致性造成流量过大引起总线风暴),Java轻量级锁会升级为重量级锁,那么JUC基于CAS实现的轻量级锁如何避免总线风暴呢?答案是:使用队列对抢锁线性排队,最大程度上减少CAS操作数量。

CLH锁其实就是一种是基于队列(具体为单向链表)排队的自旋锁,申请加锁的线程首先会通过CAS操作在单向链表的尾部增加一个节点,之后该线程只需要在其前驱节点上进行普通自旋 ,等待前驱节点释放锁即可。由于CLH锁只有在节点入队时进行一下CAS的操作,在节点在加入队列之后,抢锁线程不需要进行CAS自旋,只需普通自旋即可。因此,在争用激烈的场景下, CLH锁能大大减少的CAS操作的数量,以避免CPU的总线风暴。

面试回答:抢锁线程在队列尾部加入一个节点,然后仅在前驱节点上做普通自旋,它不断轮询前一个节点状态,如果发现前一个节点释放锁,当前节点抢锁成功。

CLH 加锁过程

首先明确,CLH是指向前节点的单链表,每个节点Node包括至少三个参数:前向指针、locked 状态变量、线程引用。 当一个线程加入抢锁队列时,创建新Node,然后通过CAS加入到CLH队列的尾部,前向指针指向前一个节点,尾指针指向新加入的节点。然后,这个节点的线程会对前向的Node进行普通自旋,循环判断前驱节点的locked属性是否为false,如果为false就表示前驱节点释放了锁 ,当前线程抢锁成功。此线程抢到锁后locked一直为true,直到释放锁为false。

在这里插入图片描述

注意:以上普通自旋与CAS的区别是 while 循环中是否有 Thread.yield(); 让出CPU时间片,CAS是没有yield的。另外还有一个尾指针,tail属性使用AtomicReference类型是为了使得多个线程并发操作tail时不会发生线程安全问题。 locked 为 true 表示此线程自旋等待中或者正在执行临界区代码,下一个节点需要等待,直到释放了锁 locked 为false。

//CAS自旋:将当前节点插入到队列的尾部
while (!tail.compareAndSet(preNode, curNode)){
	preNode = tail.get();
}

// 普通自旋,监听前驱节点的locked变量,直到其值为false
// 若前继节点的locked状态为true,则表示前一个线程还在抢占或者占有锁
while (curNode.getPrevNode().isLocked()){
	//让出CPU时间片,提高性能
	Thread.yield();
}

CLH 释放锁的过程

当一个线程执行完临界区代码后释放锁,首先将前向指针指向null,然后将locked设置为false。此时前面的节点没有引用,将会被GC。此时它后面的节点捕获了前面的locked为false立即抢占锁执行临界区代码。
在这里插入图片描述

AQS抽象同步器核心原理

为什么需要AQS?

  • CAS 恶性空循环浪费大量CPU资源
  • SMP架构CPU会导致总线风暴

在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;队列的队首节点(队列的头部)表示占有锁的节点,新加入的抢锁线程则需要等待,会插入到队列的尾部。AQS是JUC提供的一个用于构建锁和同步容器的基础类。 AQS是CLH队列的一个变种,主要原理和CLH队列差不多。AQS队列内部维护的是一个FIFO的双向链表,这种结构的特点是每个数据结构都有两个指针,分别指向直接的前驱节点和直接的后驱节点。每个节点其实是由线程封装的,当线程争抢锁失败后会封装成Node加入到AQS队列中去;当获取锁的线程释放锁以后,会从队列中唤醒一个阻塞的节点。

在这里插入图片描述

AQS核心成员

状态标志位 state

volatile 修饰的 int state ,任何线程都可以回去state的最新值,通过getState() 和 setState() 设置同步状态值。一般通过CAS设置state值。调用的是Unsafe的compareAndSwapInt()方法实现CAS。以ReentrantLock为例,初始时 state = 0,表示未锁定状态。当一个线程调用tryAcquire() 独占该锁并将state+1,此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其他线程才有机会获取该锁。当然,释放锁之前, A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态。

Node节点状态 waitStatus

在Node节点中定义了waitStatus来表示节点的状态,

  • cancelled,取消的,一般中断的或者超时的线程是这个状态,需要从等待队列中删除节点,此状态不参与竞争。
  • signal,表示后驱节点处于等待状态,一旦此节点释放锁或者被取消了,通知后驱节点运行。
  • condition,表示该节点处在条件队列中阻塞,表示此节点处于等待队列中,当调用了condition的singal方法才会从等待队列转移到同步队列中去竞争锁。
  • propagate,共享锁的延续,当自己的状态是这个就通知后续节点也共享状态获取锁。
  • 为0,表示当前节点处于初始状态。

**注意:**有人不知道等待队列和同步队列,等待队列就是阻塞了的线程,例如调用wait()方法就进入到了等待队列。只有在同步队列中的线程才有资格竞争锁,处于等待队列中的线程如果被 notify 或者 signal 唤醒进入到同步队列,并不是被唤醒了就立马能获得锁运行!同步队列简单讲就是已经具备运行条件,只差一把锁的资源,抢到了立马就运行。而等待队列中的线程没有抢锁资格。

注意,以上两个都是状态,但是state是全局唯一的,标识的是AQS队列的锁状态,而waitStatus是每个节点都有的内部属性。

thread成员

Node 的thread用来存放线程的引用,next指针指向后续节点。

抢占式常量标识
  • shared,表示线程因为获取共享资源时阻塞而添加到队列中。
  • exclusive,表示线程因为获取独占资源阻塞而被添加到队列中。

双向同步队列

AQS的内部队列是CLH队列的变种,每当线程通过AQS获取锁失败时,线程将被封装成一个Node节点,通过CAS原子操作插入队列尾部。当有线程释放锁时, AQS会尝试让队首的后驱节点占用锁。

在这里插入图片描述

AQS是一个同步器实现了所得基本功能,JUC的的显式锁如ReentrantLock、 ReentrantReadWriteLock,线程同步工具如Semaphore,内部都使用了AQS作为等待队列。

AQS抢占锁的原理

新线程来了,使用AQS的Acquire尝试获取锁,如果失败了则构造独占式节点Node,通过CAS方式自旋地将节点加入到同步队列队尾。构造Node时需要设置 thread 为当前线程。当节点进入到了队列后,本来应该自旋地判断前置节点是否为头节点并尝试获取锁。但是为了不浪费资源,如果前置节点不是头节点则自旋过程中会阻塞线程。而只有它的前驱节点成为头节点后才会唤醒后置线程尝试获取锁。自旋是节点中的线程自己完成的。AQS不像CLH节点那样做空旋转浪费资源,而是会被挂起park进入阻塞状态。如果头节点获取了锁,那么此线程会停止自旋而去执行临界区的代码。

其实判断前置节点还需要判断节点的状态,例如只有前驱节点状态为signal,则它的后续节点进行自我阻塞。一开始一个线程加入到队尾waitStatu肯定是0表示初始状态,此时会进行CAS自旋获取锁。在自旋中检测是否挂起,找到自己的有效前驱(指的是不是取消的节点,一般为初始状态0或者共享状态)然后将其设置为signal,之后自己马上进入阻塞。可以理解为当前面的节点成为头节点后记得唤醒我。 如果前面的节点为取消状态的节点,就继续往前找,并建立唤醒关系。只有是signal状态才会唤醒后续节点。

在这里插入图片描述

释放锁的过程,当头节点释放锁后就唤醒后面的节点,后面的节点成为新的头结点。如果遇见某个节点是无效节点则直接删除,也就是说无效节点的出队操作是在唤醒后驱节点的线程之后。

ReentrantLock 如何借助AQS实现的?

ReentrantLock把所有Lock接口的操作都委派到一个Sync类上,该类继承了AQS。NonfairSync为非公平(或者不公平)同步器, FairSync为公平同步器。这两个同步器都继承Sync,然后Sync继承AQS。ReentrantLock的lock()和unlock()调用的其实是Sync的lock()和release()方法。ReentrantLock的显式锁操作是委托(或委派)给一个Sync内部类的实例完成的。而Sync内部类只是AQS的一个子类,所以本质上ReentrantLock的显式锁操作是委托(或委派)给AQS完成的。

ReentrantLock AQS 抢锁原理

公平锁与非公平锁区别:

  • 加锁区别:**加锁时是否判断前面有节点在排队。**公平锁是先判断是否有节点在排队,如果有则加入到队列后面CAS方式。非公平的是新节点来了直接去抢锁,两次抢锁失败了才加入到队列中,并且阻塞。
  • 解锁区别:如果新来了一个线程Thread-4,此时直接抢占锁,如下图1所示;如果没有新来的线程抢占,则按照队列的顺序公平地唤醒头结点后面的节点并持有锁。如下图图2所示。

img

img

非公平式抢锁 (同步器是NonfairSync)

首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0就调用AQS的acquire方法以CAS方式把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时, CAS操作只能保证一个线程操作成功,剩下的只能乖乖去排队。如果发现当前线程和独占锁的线程是同一个就重入,state++;如果两次抢锁都失败了,那么就老实入队

ReentrantLock“非公平”性即体现在这里:如果占用锁的线程刚释放锁, state置为0,而排队等待锁的线程还未唤醒,新来的线程就直接抢占了该锁,那么就“插队”了。

非公平同步器ReentrantLock.NonfairSync的核心思想就是当前进程尝试获取锁的时候,如果发现锁的状态位是0,就直接尝试将锁拿过来,然后执行setExclusiveOwnerThread(),根本不管同步队列中的排队节点。

公平式抢锁 (同步器是FairSync)

首先获取锁的状态,如果state为0则判断头节点是否有后驱节点,如果有后驱节点就立即去排队。否则CAS将state设置为1进行抢占,抢占成功后将当前线程设置为锁占有线程。

AQS条件队列

Condition与Object的wait()/notify()作用是相似的,都是使得一个线程等待某个条件( Condition),只有当该条件具备signal()或者signalAll()方法被调用时等待线程才会被唤醒,从而重新争夺锁。不同的是, Object的wait()/notify()由JVM底层实现,而Condition接口与实现类完全使用Java代码实现。通过Condition的await()和signal()方法进行线程间的阻塞与唤醒。

一个Condition对象是一个单条件的等待队列 :

在这里插入图片描述

在一个显式锁上,我们可以创建多个等待任务队列,这点和内置锁不同, Java内置锁上只有唯一的一个等待队列。

await()等待方法原理

当线程调用await()方法时,说明当前线程的节点为当前AQS队列的队首节点,正好处于占有锁的状态, await()方法需要把该线程从AQS队列挪到Condition等待队列里。然后执行while循环,将该节点的线程阻塞,直到该节点离开等待队列,重新回到同步队列成为同步节点后,线程才退出while循环。

在这里插入图片描述

signal()唤醒方法原理

上调用signal()方法后,等待队列中的firstWaiter会被加入到同步队列中,等待节点被唤醒。

在这里插入图片描述

提一嘴

AQS 是一个同步容器与队列锁的模板类,用户可以通过AQS模板类自定义自己的锁和同步队列。只需要实现核心的获取锁、释放锁的排队和出队过程。分为模板方法和构造方法,钩子方法用户的子类自己实现。

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

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

相关文章

【代码随想录day21】二叉搜索树中的众数

题目 给你一个含重复值的二叉搜索树(BST)的根节点 root ,找出并返回 BST 中的所有 众数(即,出现频率最高的元素)。 如果树中有不止一个众数,可以按 任意顺序 返回。 假定 BST 满足如下定义&am…

Git移除commit过的大文件

前言:在提交推送本地更改至仓库时,误将大文件给提交了,导致push时报错文件过大,因此需要将已经commit的大文件移除后再push 若已知要删除的文件或文件夹路径,则可以从第4步开始 1.对仓库进行gc操作 $ git gc 2.查询…

ThinkPHP 一对多关联

用一对多关联的前提 多的一方的数据库表有一的一方数据库表的外键。 举例,用户获取自己的所有文章 数据表结构如下 // 用户表 useruser_id - integer // 用户主键name - varchar // 用户名称// 文章表 articlearticle_id - integer // 文章主键title - varchar …

WSL2安装google chrome浏览器

一. 环境: Windows 11 Ubuntu-22.04 二. 安装google-chrome步骤(官方文档): 1. 创建文件夹:mkdir chrome 2. 进入目录:cd chrome/ 3. 下载chrome压缩包:sudo wget https://dl.google.com/linux/direct/go…

学习 NestJs 的第一步

安装 NestJS 的先决条件和安装 NestJS NodeJS 的版本需要大于等于 16。 安装 NestJS 的命令是&#xff1a;npm i -g nestjs/cli。 使用命令创建项目 使用 nest new <项目名称> 来创建项目&#xff0c;假如要开启 TS 的严格语法功能的话&#xff0c;可以把--strict 标…

【雕爷学编程】Arduino动手做(93)--- 0.96寸OLED液晶屏模块15

37款传感器与执行器的提法&#xff0c;在网络上广泛流传&#xff0c;其实Arduino能够兼容的传感器模块肯定是不止这37种的。鉴于本人手头积累了一些传感器和执行器模块&#xff0c;依照实践出真知&#xff08;一定要动手做&#xff09;的理念&#xff0c;以学习和交流为目的&am…

OSPF的拓展配置

OSPF的拓展配置 1.手工认证 --- 在OSPF数据包交互中&#xff0c;邻居之间的数据报中将携带认证口令&#xff0c;两边认证口令相同&#xff0c;则意味着身份合法 OSPF的手工认证总共分为三种&#xff1a; 1.接口认证 [r5-GigabitEthernet0/0/0]ospf authenticati…

GB/T 25000.51解读——软件产品的性能效率怎么测?

GB/T 25000.51-2016《软件产品质量要求和测试细则》是申请软件检测CNAS认可一定会用到的一部国家标准。在前面的文章中&#xff0c;我们为大家整体介绍了GB/T 25000.51-2016《软件产品质量要求和测试细则》国家标准的结构和所涵盖的内容以及对软件产品的八大质量特性中的功能性…

fastposter v2.16.0 让海报开发更简单

fastposter v2.16.0 让海报开发更简单 &#x1f525;&#x1f525;&#x1f525; fastposter海报生成器是一款快速开发海报的工具。只需上传一张背景图&#xff0c;在对应的位置放上组件&#xff08;文字、图片、二维&#x1f434;、头像&#xff09; 点击代码直接生成各种语言…

个人信息的编写以及头像的联动

下面这个是导航栏通过on触发的事件 与图片联动 <template><div><ul><li>{{obj.account}}</li><li>{{obj.ctime|dataFormat}}</li><li>{{obj.id}}</li><li>{{obj.userGroup}}</li><div><!-- acti…

VIOOVI精益管理:实现高效运营和持续改进的关键

关于什么是精益化管理这个问题&#xff0c;从字面上理解&#xff0c;“精”为“精良”&#xff0c;“益”为“利益”&#xff0c;意在产品更加精良&#xff0c;利益更加丰厚。而从丰田精益生产中我们可以看出精益化管理绝不是以偏概全的管理&#xff0c;而是全面的结合内部、外…

Linux操作系统~必考面试题⑧

1、pwd 命令 pwd 命令用于查看当前工作目录路径。 实例&#xff1a; 查看当前路径 pwd 查看软链接的实际路径 pwd -P 2、rmdir 命令 从一个目录中删除一个或多个子目录项&#xff0c;删除某目录时也必须具有对其父目录的写权限。 注意&#xff1a;不能删除非空目录实例&…

Java面试笔记

JAVA基础知识 语法结构 1.类 2.属性 3.方法 4.静态代码块 构造器 构造函数&#xff0c;构造类的对象&#xff0c;默认隐式&#xff0c;创建对象&#xff0c;先执行父类构造函数&#xff0c;再执行子类构造函数 父类的super必须在第一行 代码块 优先级最高&#xff0c;只…

pyspark笔记:读取 处理csv文件

pyspark cmd上的命令 1 读取文件 1.1 基本读取方式 注意读取出来的格式是Pyspark DataFrame&#xff0c;不是DataFrame&#xff0c;所以一些操作上是有区别的 1.1.1 format DataFrame spark.read.format("csv").option(name,value).load(path) format表示读取…

硬盘数据恢复怎么做?5步快速恢复数据!

“我的电脑刚买回来没多久&#xff0c;不知为啥硬盘就出现问题了&#xff0c;我很多的数据都丢失了&#xff0c;这种情况进行硬盘数据恢复还有希望吗&#xff1f;希望各位老师给我点意见&#xff01;感谢&#xff01;” 在数字化时代&#xff0c;数据已经成为我们生活中不可或缺…

美团面试官热爱考察的问题:你真的会判断链表环吗?

大家好&#xff0c;我是小米&#xff01;今天我要和大家一起来解析美团面试中经常会遇到的一道经典问题&#xff1a;如何判断链表是否为环形链表&#xff1f;这是一道考察数据结构与算法基础的问题&#xff0c;也是面试中的常客。相信通过这篇文章的学习&#xff0c;你将能够更…

【决策树-鸢尾花分类】

决策树算法简介 决策树是一种基于树状结构的分类与回归算法。它通过对数据集进行递归分割&#xff0c;将样本划分为多个类别或者回归值。决策树算法的核心思想是通过构建树来对数据进行划分&#xff0c;从而实现对未知样本的预测。 决策树的构建过程 决策树的构建过程包括以…

C#,中国福利彩票《刮刮乐》的数学算法(02)——时来运转

1 中国福利彩票 中国福利彩票始于1987年7月27日&#xff0c;以“团结各界热心社会福利事业的人士&#xff0c;发扬社会主义人道主义精神&#xff0c;筹集社会福利资金&#xff0c;兴办残疾人、老年人、孤儿福利事业和帮助有困难的人”、即“扶老、助残、救孤、济困”为宗旨。随…

儿童小学生护眼灯选哪个牌子经济好用?分享五款好用的台灯

近期&#xff0c;经济好用的护眼台灯赶上了热潮&#xff0c;许多家长反应不知道怎么选一款合适有经济的护眼台灯&#xff1f;面对市场上很多鱼龙混杂的台灯&#xff0c;真的是眼花缭乱&#xff0c;选台灯不一定要选贵的&#xff0c;但一定要选对的&#xff0c;今天小编就分享五…

网页开发基础——HTML

一、flask框架 Flask是一种轻量级的Python web应用程序框架&#xff0c;可以帮助使用者快速构建Web应用程序和API。由于其简洁、灵活和易于上手的特点&#xff0c;Flask被广泛用于开发小型到中型的Web应用程序和后端API。本次我们主要是使用flask框架&#xff0c;进行一个小型w…