【9036期】JUC多线程---AQS抽象队列同步器原理

技术【9036期】JUC多线程---AQS抽象队列同步器原理 【9036期】JUC多线程---AQS抽象队列同步器原理AQS 的工作原理
什么是 AQS
AQS,Abstract Queued Sync

[9036期] JUC多线程- AQS抽象队列同步器原理

AQS的工作原则

什么是 AQS

s,抽象排队同步器,抽象队列同步器,是J.U.C .中锁定和同步组件的基础,工作原理是如果被请求的共享资源空闲,则将当前被请求资源的线程设置为有效工作线程,并将共享资源设置为锁定状态。如果请求的共享资源被占用,那么无法获得锁的线程将被添加到等待队列中。此时,当线程阻塞等待和唤醒时,我们需要一个锁分配机制,而 AQS 是通过 CLH 队列实现锁分配的机制.

CLH 同步队列的模型

CLH队列是一个由内部类Nodes组成的同步队列,是一个双向队列(没有队列实例,只有Nodes之间的关系)。将请求共享资源的线程封装为一个节点节点,实现锁分配。同时,内部类ConditionObject用于构造等待队列。当调用ConditionObject的await()方法时,线程将加入等待队列。当调用ConditionObject的signal()方法时,线程将从等待队列移动到同步队列进行锁竞争。AQS只能有一个同步队列,但可以有多个等待队列。AQS CLH同步队列模型如下:

AQ有三个主要变量,即头、尾和状态,其中头指向同步队列的头,注意头是一个空节点,不存储信息。尾部是同步队列的尾部,为了方便查找队列,同步队列采用双向链表的结构。当Node节点设置为head时,其线程信息和前置节点都会被清除,因为线程已经获取了同步状态,正在执行,所以不需要存储相关信息。head只需要保存后继节点的指针,便于head节点在释放同步状态后唤醒后继节点。

的入队和出队操作是无锁操作。基于CAS自旋锁的实现,AQS保持一个易失性修改的int型状态同步状态。volatile保证线程之间的可见性,通过CAS对同步状态进行原子操作,修改其值。当state=0时,意味着没有线程持有共享资源的锁。当state=1时,意味着一些线程当前正在使用共享变量,其他线程必须加入同步队列等待。

内部类节点数据结构分析

静态最终类节点

//共享模式

静态最终节点SHARED=new Node();

//独占模式

静态最终节点EXCLUSIVE=null

//标识线程处于结束状态。

静态最终int CANCELED=1;

//等待被唤醒

静态最终int SIGNAL=-1;

//条件状态

静态最终int CONDITION=-2;

//通过使用共享模式中的表示获得的同步状态将被传播。

静态最终int PROPACT=-3;

//等待状态,有取消、信号、条件和传播四个值。

可变整数等待状态;

//同步队列中的前置节点

易失节点前一个;

//同步队列中的后续节点

易失节点下一个;

//请求锁定的线程

线程易失性线程;

//等待队列中的后续节点,与条件相关,稍后分析。

节点下一个服务员;

//确定是否为共享模式。

最终布尔值Isshared(){ 0

return nextWaiter==SHARED

}

//.

}

AQS可以分为两种模式:独占模式和共享模式。例如,可重入锁和循环屏障是基于独占模式实现的,而信号量和计数监视是基于共享模式实现的。

waitStatus变量指示当前封装为节点节点的线程的等待状态,有四个值:取消、信号、条件和传播:

取消:值为1表示在同步队列中等待的线程已经超时或被中断,并且处于结束状态,因此需要从同步队列中删除Node节点。

信号:值-1表示

继结点在等待当前结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL,当该节点释放了同步锁之后,就会唤醒该节点的后继节点

  • CONDITION:值为-2,与 Condition 相关,表示该结点在 condition 等待队列中阻塞,当其他线程调用了Condition 的 signal() 方法后,CONDITION 状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE:值为-3时,在共享模式下使用,表示该线程以及后继线程进行无条件传播。前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
  • AQS 的设计模式

    AQS 的模板方法模式

    AQS 的基于模板方法模式设计的,在 AQS 抽象类中已经实现了线程在等待队列的维护方式(如获取资源失败入队/唤醒出队等),而对于具体共享资源 state 的获取与释放(也就是锁的获取和释放)则交由具体的同步器来实现,具体的同步器需要实现以下几种方法:

    • isHeldExclusively():该线程是否正在独占资源,只有用到 condition 才需要去实现它
    • tryAcquire(int):独占模式,尝试获取资源,成功则返回 true,失败则返回 false
    • tryRelease(int):独占方式,尝试释放资源,成功则返回 true,失败则返回 false
    • tryAcquireShared(int):共享方式,尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源
    • tryReleaseShared(int):共享方式,尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false

    JUC 中提供的同步器

    • 闭锁 CountDownLatch:用于让主线程等待一组事件全部发生后继续执行。
    • 栅栏 CyclicBarrier:用于等待其它线程,且会阻塞自己当前线程,所有线程必须全部到达栅栏位置后,才能继续执行;且在所有线程到达栅栏处之后,可以触发执行另外一个预先设置的线程。
    • 信号量 Semaphore:用于控制访问资源的线程个数,常常用于实现资源池,如数据库连接池,线程池。在 Semaphore 中,acquire 方法用于获取资源,有的话,继续执行
    • 没有资源的话将阻塞直到有其它线程调用 release 方法释放资源;
    • 交换器 Exchanger:用于线程之间进行数据交换;当两个线程都到达共同的同步点(都执行到exchanger.exchange 的时刻)时,发生数据交换,否则会等待直到其它线程到达;

    CountDownLatch 和 CyclicBarrier 的区别

    两者都可以用来表示代码运行到某个点上,二者的区别在于:

    ① CyclicBarrier 的某个线程运行到某个位置之后就停止运行,直到所有的线程都到达了这个点,所有线程才重新运行;CountDownLatch 的某线程运行到某个位置之后,只是给计数值-1而已,该线程继续运行;

    ② CyclicBarrier 可重用,CountDownLatch 不可重用,计数值 为 0 时该 CountDownLatch 就不可再用了。

    ReentranLock 中独占模式下非公平锁的获取流程

    获取独占锁的过程是定义在 tryAcquire() 中的,当前线程尝试获取同步状态,如果获取失败,就将线程封装成 Node 节点插入到 CLH 同步队列中。插入同步队列后,线程并没有放弃获取同步状态,而是根据前置节点状态状态判断是否继续获取,如果前置节点是 head 结点,继续尝试获取,否则就将线程挂起。如果成功获取同步状态则将自己设置为 head 结点。当持有同步状态的线程释放资源后,也会唤醒队列中的后继线程。

    ConditionObject 阻塞队列

    什么是 Condition 接口

    AQS 的阻塞队列是基于内部类 ConditionObject 实现的,而 ConditionObject 实现了 Condition 接口。那 Condition 接口是什么呢Condition 主要用于线程的等待和唤醒,在JDK5之前,线程的等待唤醒是用 Object 类的 wait/notify/notifyAll 方法实现的,这些方法必须配合 synchronized 关键字使用,使用起来不是很方便,为了解决这个问题,在 JDK5 之后,J.U.C 提供了Condition。

    • Condition.await 对应于 Object.wait;
    • Condition.signal 对应于 Object.notify;
    • Condition.signalAll 对应于 Object.notifyAll;

    与 synchronized 的等待唤醒机制相比,Condition 能够精细的控制多线程的休眠与唤醒,具备更多的灵活性, 通过多个 Condition 实例对象建立不同的等待队列,从而实现同一个锁拥有多个等待队列。而 synchronized 关键字只能有一组等待唤醒队列,使用 notify() 唤醒线程时只能随机唤醒队列中的一个线程。

    ConditionObject 阻塞队列实现原理

    Condition 的具体实现之一是 AQS 的内部类 ConditionObject,每个 Condition 都对应着一个等待队列,也就是说如果一个锁上创建了多个 Condition 对象,那么也就存在多个等待队列。当调用 ConditionObject 的 await() 方法后,线程将会加入等待队列中,当调用 ConditionObject 的 signal() 方法后,线程将从等待队列转移动同步队列中进行锁竞争。AQS 的 ConditionObject 中的等待队列模型如下:

    AQS 的线程唤醒机制原理

    AQS 的线程唤醒是通过 singal() 方法实现的,我们先看下 singal() 方法线程唤醒的流程图:

    signal() 方法主要调用了 doSignal(),而 doSignal() 方法中做了两件事:

    (1)从条件等待队列移除被唤醒的节点,然后重新维护条件等待队列的 firstWaiter 和 lastWaiter 的指向。
    (2)将从等待队列移除的结点加入同步队列(在 transferForSignal() 方法中完成的),如果进入到同步队列失败并且条件等待队列还有不为空的节点,则继续循环唤醒后续其他结点的线程。

    注意:无论是同步队列还是等待队列,使用的 Node 数据结构都是同一个,不过是使用的内部变量不同罢了

    所以 signal() 的流程可以概述为:

    • signal() 被调用后,先判断当前线程是否持有独占锁
    • 如果有,那么唤醒当前 Condition 等待队列的第一个结点的线程,并从等待队列中移除该结点,添加到同步队列中
    • 如果加入同步队列失败,那么继续循环唤醒等待队列中的其他结点的线程
    • 如果成功加入同步队列,那么如果其前驱结点已结束或者设置前驱节点状态为 Node.SIGNAL 状态失败,则通过 LockSupport.unpark() 唤醒被通知节点代表的线程。

    到此 signal() 任务完成,被唤醒后的线程,将调用 AQS 的 acquireQueued() 方法加入获取同步状态的竞争中,这就是等待唤醒机制的整个流程实现原理。

    吃水不忘挖井人:

    • JUC多线程:AQS抽象队列同步器原理

    内容来源网络,如有侵权,联系删除,本文地址:https://www.230890.com/zhan/155876.html

    (0)

    相关推荐

    • sqlite和mysql的区别有哪些

      技术sqlite和mysql的区别有哪些这篇文章主要为大家展示了“sqlite和mysql的区别有哪些”,内容简而易懂,条理清晰,希望能够帮助大家解决疑惑,下面让小编带领大家一起研究并学习一下“sqlite和mysql的

      攻略 2021年12月2日
    • 开发React应用的实用技巧有哪些

      技术开发React应用的实用技巧有哪些这篇文章将为大家详细讲解有关开发React应用的实用技巧有哪些,文章内容质量较高,因此小编分享给大家做个参考,希望大家阅读完这篇文章后对相关知识有一定的了解。背景Hooks 自推出以

      攻略 2021年11月25日
    • python如何链接数据库

      技术python如何链接数据库小编给大家分享一下python如何链接数据库,相信大部分人都还不怎么了解,因此分享这篇文章给大家参考一下,希望大家阅读完这篇文章后大有收获,下面让我们一起去了解一下吧!MySQLdb模块是为

      攻略 2021年11月24日
    • ABAP正则表达式和SPLIT INTO有什么区别?

      技术ABAP正则表达式与SPLIT INTO有什么不同这篇文章主要介绍“ABAP正则表达式与SPLIT INTO有什么不同”,在日常操作中,相信很多人在ABAP正则表达式与SPLIT INTO有什么不同问题上存在疑惑,小

      攻略 2021年12月24日
    • 火柴英文,火柴的发展历史是怎样的

      技术火柴英文,火柴的发展历史是怎样的火柴(英文名火柴英文:Match),也称“安全火柴”。根据记载最早的火柴是由中国人在公元577年发明的(一种引火的材料,不是火柴),当时是南北朝时期火柴,战事四起,北齐腹背受敌,物资短

      生活 2021年10月25日
    • C++程序员应聘常见面试题有哪些

      技术C++程序员应聘常见面试题有哪些本篇内容主要讲解“C++程序员应聘常见面试题有哪些”,感兴趣的朋友不妨来看看。本文介绍的方法操作简单快捷,实用性强。下面就让小编来带大家学习“C++程序员应聘常见面试题有哪些”吧!试题

      攻略 2021年11月29日