队列同步器(AQS)详解

2024-02-13 16:08
文章标签 详解 队列 aqs 同步器

本文主要是介绍队列同步器(AQS)详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

队列同步器(AQS)

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

队列同步器的基本结构

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态的管理。同步队列中的节点(Node)用来保存"获取同步状态失败的线程"引用、等待状态以及前驱和后继节点。

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer  
  2.     implements java.io.Serializable {  
  3.     ......  
  4.     private transient volatile Node head;//头节点  
  5.     private transient volatile Node tail;//尾节点  
  6.     private volatile int state;//*同步状态*  
  7.     ......  
  8.     static final class Node {  
  9.         volatile int waitStatus;//等待状态  
  10.         volatile Node prev;//前驱  
  11.         volatile Node next;//后继  
  12.         volatile Thread thread;//线程引用  
  13.         ......  
  14.     }  
  15.     ......  
  16. }   

:Node类型的prev、next属性以及AbstractQueuedSynchronizer类型的head 、tail属性都设置为volatile,保证可见性。

自定义同步组件的设计思路

同步器的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。

子类推荐被定义为自定义同步组件的静态内部类,同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用,同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

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

同步器的设计是基于模板方法模式的,也就是说,使用者需要继承同步器并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,并调用同步器提供的模板方法,而这些模板方法将会调用使用者重写的方法
重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。
getState():获取当前同步状态。
setState(int newState):设置当前同步状态。
compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。

独占式同步组件的设计

可重写的方法

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /*Attempts to acquire in exclusive mode. This method should query if the state of the object  
  2. permits it to be acquired in the exclusive mode, and if so to acquire it.*/  
  3. //独占式获取同步状态,实现该方法需要查询当前状态并判断同步状态是否符合预期,然后再进行CAS设置同步状态  
  4. protected boolean tryAcquire(int arg)  
  5.   
  6. /*Attempts to set the state to reflect a release in exclusive mode.*/  
  7. //独占式释放同步状态,等待获取同步状态的线程将有机会获取同步状态  
  8. protected boolean tryRelease(int arg)  
  9.   
  10. /*Returns true if synchronization is held exclusively with respect to the current (calling) thread. */  
  11. //当前同步器是否在独占模式下被线程占用,一般该方法表示是否被当前线程所独占  
  12. protected boolean isHeldExclusively()  

同步器提供的模板方法

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /*Acquires in exclusive mode, ignoring interrupts.*/  
  2. //独占式获取同步状态,如果当前线程获取同步状态成功,立即返回。否则,将会进入同步队列等待,  
  3. //该方法将会重复调用重写的tryAcquire(int arg)方法  
  4. public final void acquire(int arg)  
  5.   
  6. /*Acquires in exclusive mode, aborting if interrupted.*/  
  7. //与acquire(int arg)基本相同,但是该方法响应中断。  
  8. public final void acquireInterruptibly(int arg)  
  9.   
  10. /* Releases in exclusive mode.  Implemented by unblocking one or more threads if {@link #tryRelease} returns true. 
  11. This method can be used to implement method {@link Lock#unlock}.*/  
  12. //独占式释放同步状态,该方法会在释放同步状态后,将同步队列中第一个节点包含的线程唤醒  
  13. public final boolean release(int arg)  

acquire(int arg)模板方法

通过调用同步器的acquire(int arg)方法可以获取同步状态。该方法对中断不敏感,也就是说,由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移除。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. public final void acquire(int arg) {//**该方法是模板方法**  
  2.     if (!tryAcquire(arg) &&//先通过tryAcquire获取同步状态  
  3.         acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//获取同步状态失败则生成节点并加入同步队列  
  4.         selfInterrupt();  
  5. }  

独占式同步状态获取流程

主要逻辑:首先调用自定义同步器实现tryAcquire(int arg)方法,该方法保证线程安全的获取同步状态,如果同步状态获取失败,则构造同步节点(独占式Node.EXCLUSIVE,同一时刻只能有一个线程成功获取同步状态)并通过addWaiter(Node node)方法将该节点加入到同步队列的尾部,最后调用acquireQueued(Node node,int arg)方法,使得该节点以“死循环”的方式获取同步状态。



将节点加入同步队列

当前线程获取同步状态失败时,同步器会将当前线程等待状态等信息构造成为一个节点(Node)并将其加入同步队列,同时会阻塞当前线程。

试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全

因此,同步器提供了一个基于CAS的设置尾节点的方法:compareAndSetTail(Nodeexpect,Nodeupdate),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. //将节点加入到同步队列的尾部  
  2. private Node addWaiter(Node mode) {  
  3.       Node node = new Node(Thread.currentThread(), mode);//生成节点(Node)  
  4.       // Try the fast path of enq; backup to full enq on failure  
  5.       //快速尝试在尾部添加  
  6.       Node pred = tail;  
  7.       if (pred != null) {  
  8.           node.prev = pred;//先将当前节点node的前驱指向当前tail  
  9.           if (compareAndSetTail(pred, node)) {//CAS尝试将tail设置为node  
  10.               //如果CAS尝试成功,就说明"设置当前节点node的前驱"与"CAS设置tail"之间没有别的线程设置tail成功  
  11.               //只需要将"之前的tail"的后继节点指向node即可  
  12.               pred.next = node;  
  13.               return node;  
  14.           }  
  15.       }  
  16.       enq(node);//否则,通过死循环来保证节点的正确添加  
  17.       return node;  
  18.   }  
[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. private Node enq(final Node node) {  
  2.     for (;;) {//通过死循环来保证节点的正确添加  
  3.         Node t = tail;  
  4.         if (t == null) { // Must initialize 同步队列为空的情况  
  5.             if (compareAndSetHead(new Node()))  
  6.                 tail = head;  
  7.         } else {  
  8.             node.prev = t;  
  9.             if (compareAndSetTail(t, node)) {//直到CAS成功为止  
  10.                 t.next = node;  
  11.                 return t;//结束循环  
  12.             }  
  13.         }  
  14.     }  
  15. }  

在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。

串行化的优点

如果通过加锁同步的方式添加节点,线程必须获取锁后才能添加尾节点,那么必然会导致其他线程等待加锁而阻塞,获取锁的线程释放锁后阻塞的线程又会被唤醒,而线程的阻塞和唤醒需要依赖于系统内核完成,因此程序的执行需要从用户态切换到核心态,而这样的切换是非常耗时的操作。如果我们通过”循环CAS“来添加节点的话,所有线程都不会被阻塞,而是不断失败重试,线程不需要进行锁同步,不仅消除了线程阻塞唤醒的开销而且消除了加锁解锁的时间开销。但是循环CAS也有其缺点,循环CAS通过不断尝试来添加节点,如果说CAS操作失败那么将会占用处理器资源。

节点的自旋

节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说是线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. final boolean acquireQueued(final Node node, int arg) {  
  2.     boolean failed = true;  
  3.     try {  
  4.         boolean interrupted = false;  
  5.         for (;;) {//无限循环  
  6.             final

这篇关于队列同步器(AQS)详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/705978

相关文章

Oracle的to_date()函数详解

《Oracle的to_date()函数详解》Oracle的to_date()函数用于日期格式转换,需要注意Oracle中不区分大小写的MM和mm格式代码,应使用mi代替分钟,此外,Oracle还支持毫... 目录oracle的to_date()函数一.在使用Oracle的to_date函数来做日期转换二.日

Java实现任务管理器性能网络监控数据的方法详解

《Java实现任务管理器性能网络监控数据的方法详解》在现代操作系统中,任务管理器是一个非常重要的工具,用于监控和管理计算机的运行状态,包括CPU使用率、内存占用等,对于开发者和系统管理员来说,了解这些... 目录引言一、背景知识二、准备工作1. Maven依赖2. Gradle依赖三、代码实现四、代码详解五

Mysql 中的多表连接和连接类型详解

《Mysql中的多表连接和连接类型详解》这篇文章详细介绍了MySQL中的多表连接及其各种类型,包括内连接、左连接、右连接、全外连接、自连接和交叉连接,通过这些连接方式,可以将分散在不同表中的相关数据... 目录什么是多表连接?1. 内连接(INNER JOIN)2. 左连接(LEFT JOIN 或 LEFT

Java中switch-case结构的使用方法举例详解

《Java中switch-case结构的使用方法举例详解》:本文主要介绍Java中switch-case结构使用的相关资料,switch-case结构是Java中处理多个分支条件的一种有效方式,它... 目录前言一、switch-case结构的基本语法二、使用示例三、注意事项四、总结前言对于Java初学者

Linux内核之内核裁剪详解

《Linux内核之内核裁剪详解》Linux内核裁剪是通过移除不必要的功能和模块,调整配置参数来优化内核,以满足特定需求,裁剪的方法包括使用配置选项、模块化设计和优化配置参数,图形裁剪工具如makeme... 目录简介一、 裁剪的原因二、裁剪的方法三、图形裁剪工具四、操作说明五、make menuconfig

详解Java中的敏感信息处理

《详解Java中的敏感信息处理》平时开发中常常会遇到像用户的手机号、姓名、身份证等敏感信息需要处理,这篇文章主要为大家整理了一些常用的方法,希望对大家有所帮助... 目录前后端传输AES 对称加密RSA 非对称加密混合加密数据库加密MD5 + Salt/SHA + SaltAES 加密平时开发中遇到像用户的

Springboot使用RabbitMQ实现关闭超时订单(示例详解)

《Springboot使用RabbitMQ实现关闭超时订单(示例详解)》介绍了如何在SpringBoot项目中使用RabbitMQ实现订单的延时处理和超时关闭,通过配置RabbitMQ的交换机、队列和... 目录1.maven中引入rabbitmq的依赖:2.application.yml中进行rabbit

C语言线程池的常见实现方式详解

《C语言线程池的常见实现方式详解》本文介绍了如何使用C语言实现一个基本的线程池,线程池的实现包括工作线程、任务队列、任务调度、线程池的初始化、任务添加、销毁等步骤,感兴趣的朋友跟随小编一起看看吧... 目录1. 线程池的基本结构2. 线程池的实现步骤3. 线程池的核心数据结构4. 线程池的详细实现4.1 初

Python绘制土地利用和土地覆盖类型图示例详解

《Python绘制土地利用和土地覆盖类型图示例详解》本文介绍了如何使用Python绘制土地利用和土地覆盖类型图,并提供了详细的代码示例,通过安装所需的库,准备地理数据,使用geopandas和matp... 目录一、所需库的安装二、数据准备三、绘制土地利用和土地覆盖类型图四、代码解释五、其他可视化形式1.

SpringBoot使用Apache POI库读取Excel文件的操作详解

《SpringBoot使用ApachePOI库读取Excel文件的操作详解》在日常开发中,我们经常需要处理Excel文件中的数据,无论是从数据库导入数据、处理数据报表,还是批量生成数据,都可能会遇到... 目录项目背景依赖导入读取Excel模板的实现代码实现代码解析ExcelDemoInfoDTO 数据传输