JUC并发编程 09——队列同步器AQS

目录

一.Lock接口

1.1Lock的使用

1.2Lock接口提供的 synchronized 不具备的主要特性

1.3Lock接口的所有方法

二.队列同步器(AQS)

2.1队列同步器的接口与示例

2.2AQS实现源码分析

①同步队列

②获取锁

③释放锁


一.Lock接口

说起锁,你肯定会想到 synchronized 关键字, 没错,这是在jdk1.5之前java程序用来实现锁功能的。而 jdk1.5 之后,并发包中增加了 Lock 接口用来实现锁功能,它的功能和 synchronized 类似,不过使用时需要显示的获取和释放锁。虽然它缺少了(通过synchronized 块或方法所提供的)隐式获取释放锁的便捷性,但是却拥有了锁获取与释放的可操作性可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步特性。

1.1Lock的使用

Lock 的使用也很简单,如下所示:

在 finally 块中释放锁,目的是保证在获取到锁之后,最终能够被释放。注意:

  • 不要将获取锁的过程 lock.lock() 写在 try 块中,因为在于加锁(自定义锁的实现)操作可能会抛出异常
    • 如果加锁操作在try块之前,那么出现异常时try-finally块不会执行,程序直接因为异常而中断执行;
    • 如果加锁操作在try块中,由于已经执行到了try块,那么finally在try中出现异常时仍然会执行,此时try中的加锁操作出现异常,finally依然会执行解锁操作,而此时并没有获取到锁,执行解锁操作会抛出另外一个异常
    • 虽然都是抛出异常结束,但是此时finally解锁抛出的异常信息会将加锁的异常信息覆盖,导致信息丢失。因此应该将加锁操作放在try块之前执行。
  • 加锁 lock.lock() 之后以及 try 块之前,最好不要插入语句,如果在predo的操作中出现异常,程序会因为异常而终止,而由于并未执行try块,因此finally也不会执行,此时锁并没有释放掉,因此可能会出现死锁。所以,加锁之后直接执行try块,不要执行predo操作。

1.2Lock接口提供的 synchronized 不具备的主要特性

  • 尝试性的非阻塞的获取锁:当前线程可以尝试性的获取锁,如果当前锁没有被其它线程获取到,则成功获取并持有。如果获取失败则立刻返回,非阻塞。
  • 能被中断的获取锁:与 synchronized 不同的是,Lock接口中的 lockInterruptibly() 方法是可中断的获取锁。即获取不到锁的线程能够响应中断,不是死等,当获取不到锁的线程被其它线程中断时,中断异常被抛出。而用synchronized修饰的话,当一个线程处于等待某个锁的状态,是无法被中断的,只有一直等待下去。
  • 超时获取锁:在指定时间之前获取锁,超时无法获取则返回。

1.3Lock接口的所有方法

  • void lock():获取锁,当前线程获取到锁后,从该方法返回,该方法获取锁过程中阻塞
  • void lockInterruptibly() throws InterruptedException:和lock() 的区别在于该方法会响应中断
  • boolean tryLock():非阻塞尝试获取锁,方法立即返回,获取到返回true,否则返回false
  • boolean tryLock(long time, TimeUnit unit) throws InterruptedException:超时的获取锁,当超时、中断、获取未超时获取到了锁这三种场景都会返回
  • void unlock():释放锁
  • Condition newCondition():获取等待通知组件,该组件与当前锁绑定,当前线程只有获得了锁,才能调用该组件的 await() 方法,而调用后,当前线程将释放锁

二.队列同步器(AQS)

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

队列同步器的主要使用方式是继承,子类通过继承队列同步器并实现它的抽象方法来管理同步状态,在抽象方法的实现过程中免不了要对同步状态进行更改,这时就需要使用队列同步器提供的3个方法(getState()、setState(int newState)和compareAndSetState(int expect,int update))来进行操作,因为它们能够保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类,队列同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来 供自定义同步组件使用,队列同步器既可以支持独占式地获取同步状态,也可以支持共享式地获取同步状态,这样就可以方便实现不同类型的同步组件(ReentrantLock、ReentrantReadWriteLock和CountDownLatch等)。

队列同步器是实现锁(也可以是任意同步组件)的关键,在锁的实现中聚合队列同步器,利用队列同步器实现锁的语义。可以这样理解二者之间的关系:

  • 锁是面向使用者的,它定义了使用者与锁交 互的接口(比如可以允许两个线程并行访问),隐藏了实现细节;
  • 同步器面向的是锁的实现者, 它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒等底层操作。锁和同步器很好地隔离了使用者和实现者所需关注的领域。

2.1队列同步器的接口与示例

队列同步器AQS的设计是基于模板方法的,即使用者需要继承队列同步器并重写指定的方法,然后将队列同步器组合在自定义同步组件中,并调用同步器提供的模板方法,这些模板方法会调用使用者的重写方法。

同步器为了让使用者重写指定的方法,提供了三个基础方法:

  1. getState():获取当前同步状态
  2. setState(int newState):设置当前同步状态
  3. compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态 设置的原子性

同步器可重写的方法分为 独占式获取锁 和 共享式获取锁如下图所示:

实现自定义同步组件时,将会调用同步器提供的模板方法(这些模板方法会调用使用者的重写方法),部分模板方法如下:

同步器提供的模板方法基本上分为3类:独占式获取与释放同步状态共享式获取与释放同步状态查询同步队列中的等待线程情况。自定义同步组件将使用同步器提供的模板方法 来实现自己的同步语义。

独占锁就是在同一时刻只能有一个线程获取到锁,而其他获取锁的线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取锁。

接下来,我们自己实现一个独占锁,采用组合自定义同步器AQS的方式,只有搞懂了AQS才能更加深入的去学习理解 并发包中的其它同步组件。

public class Mutex implements Lock {private static class Sync extends AbstractQueuedSynchronizer {//是否处于独占状态@Overrideprotected boolean isHeldExclusively() {return getState() == 1;}//当状态为 0 时获取锁@Overrideprotected boolean tryAcquire(int arg) {if(compareAndSetState(0,1)){setExclusiveOwnerThread(Thread.currentThread());return true;}return false;}//释放锁,将状态置为 0@Overrideprotected boolean tryRelease(int arg) {if(getState()==0||getExclusiveOwnerThread()!=Thread.currentThread()){throw new IllegalMonitorStateException();}setExclusiveOwnerThread(null);setState(0);return true;}//返回一个 Condition,每个 condition 都包含了一个 condition 队列Condition newCondition(){return new ConditionObject();}}//仅需要将操作代理到 Sync 上即可private final Sync sync=new Sync();@Overridepublic void lock() {sync.acquire(1);}@Overridepublic void lockInterruptibly() throws InterruptedException {sync.acquireInterruptibly(1);}@Overridepublic boolean tryLock() {return sync.tryAcquire(1);}@Overridepublic boolean tryLock(long time, TimeUnit unit) throws InterruptedException {return sync.tryAcquireNanos(1, unit.toNanos(time));}@Overridepublic void unlock() {sync.release(1);}@Overridepublic Condition newCondition() {return sync.newCondition();}public boolean isLocked(){return sync.isHeldExclusively();}public boolean hasQueuedThreads(){return sync.hasQueuedThreads();}
}

如示例代码所示,Mutex中定义了一个静态内部类,它继承了同步器实现了独占式获取和释放同步状态。  

在 tryAcquire(int acquires) 方法中,如果经过CAS设置成功(同步状态设置为1),则代表获 取了同步状态,而在 tryRelease(int releases) 方法中只是将同步状态重置为0。  

用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的 lock() 方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args) 即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样大大简化了实现一个可靠自定义同步组件的门槛。

2.2AQS实现源码分析

接下来将从实现角度分析队列同步器是如何完成线程同步的,主要包括:同步队列独占式同步状态获取与释放共享式同步状态获取与释放以及超时获取同步状态等同步器的核心数据结构与模板方法

①同步队列

我们先看看AQS中的一些重要属性:

同步器依赖内部的同步队列(一个FIFO双向队列)来完成同步状态管理,流程是这样的:当线程获取同步状态失败时,同步器会将当前线程以及等待状态构造成为一个节点(Node),将其加入到队列,同时阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取锁同步状态。

同步队列中的节点用来保存获取同步状态失败的线程的引用、等待状态以及前驱和后继节点,我们来看下代码:

其实就是5个属性:thread(获取同步状态的线程) + waitStatus(等待状态) + prev(前驱结点) + next(后继结点) + nextWaiter(等待队列中的后继结点)

节点是构成同步队列的基础,同步器拥有首尾节点,获取同步失败的线程将会成为节点加入到该队列尾部,同步队列的基本结构如下图:

同步器包含了两个节点类型的引用,一个指向头节点,而另一个指向尾节点。试想一下,当一个线程成功地获取了同步状态(或者锁),其他线程将无法获取到同步状态,转而被构造成为节点并加入到同步队列中,而这个加入队列的过程必须要保证线程安全,因此同步器提供了一个基于CAS的设置尾节点的方法compareAndSetTail(Node expect,Node update)它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。

同步队列遵循FIFO,首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,将会唤醒后继节点,而后继节点将会在获取同步状态成功时将自己设置为首节点。

设置首节点是通过获取同步状态成功的线程来完成的,由于只有一个线程能够成功获取到同步状态,因此设置头节点的方法并不需要使用CAS来保证,它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。

②获取锁

获取锁分为独占式和共享式,这里我们只讲独占式获取锁模式。通过调用AbstractQueuedSynchronizer的模板方法 public final void acquire(int arg){}可以获取同步状态,该方法对中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中,后续对线程进行中断操作时,线程不会从同步队列中移出。

如下图所示:

  1. 首先会调用 tryAcquire(arg) 方法,上面也提到了,这个方法是需要同步组件自己实现的,比如上面我们自己实现的Mutex锁。该方法保证线程安全的获取同步状态,tryAcquire(arg) 返回 true 表示获取成功也就正常退出了。
  2. 否则会构造同步节点(独占式Node.EXCLUSIVE)并通过 addWaiter(Node mode) 方法将加入到同步队列的尾部。源码中通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加。在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。
  3. 最后调用acquireQueued(final Node node, int arg) 通过 “死循环”的方式获取同步状态。如果获取不到则阻塞节点中对应的线程,而被阻塞后的唤醒只能依靠前驱节点出队或者阻塞线程被中断来实现。

如上,假如当前node本来就不是队头或者就是 tryAcquire(arg) 没有抢赢别人,就是走到下一个分支判断:shouldParkAfterFailedAcquire(p, node) 当前线程没有抢到锁,是否需要挂起当前线程?

这里我们分析下private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) 这个方法返回值的情况:  

  • 如果返回true, 说明前驱节点的 waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了。这个方法返回后,是true 则执行 parkAndCheckInterrupt() 方法:
  • 如果返回false, 说明当前不需要被挂起。仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,前驱节点的 waitStatus=-1 是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。

在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,如下:

  • 第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。
  • 第二,维护同步队列的FIFO原则。该方法中,节点自旋获取同步状态的行为,如下图所示:

由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程 由于中断而被唤醒)。 独占式同步状态获取流程,也就是acquire(int arg)方法调用流程,如图所示:

③释放锁

当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。

该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport 来唤醒处于等待状态的线程。

总结

分析了独占式同步状态获取和释放过程后,适当做个总结:在获取同步状态时,同步器维
护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列
(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步
器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。

参考:

《java并发编程的艺术》

并发编程 6:AQS很难? (qq.com)

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

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

相关文章

Android Studio 如何实现软件英文变中文教程

目录 前言 一、确认版本号 二、下载汉化包 三、汉化包安装 四、如何实现中英文切换 五、更多资源 前言 Android Studio是一款功能强大的集成开发环境(IDE),用于开发Android应用程序。默认情况下,Android Studio的界面和…

js中Math.min(...arr)和Math.max(...arr)的注意点

当arr变量为空数组时,这两个函数和不传参数时的结果是一样的 Math.max() // -Infinity Math.max(...[]) // -InfinityMath.min() // Infinity Math.min(...[]) // Infinity

如何编写高效清晰的嵌入式C程序

作为嵌入式工程师,怎么写出效率高、思路清晰的C语言程序呢? 要用C语言的思维方式来进行程序的构架构建 要有良好的C语言算法基础,以此来实现程序的逻辑构架 灵活运用C语言的指针操作 虽然看起来以上的说法很抽象,给人如坠雾里的感觉&…

内存之-LeakCanary

关于作者:CSDN内容合伙人、技术专家, 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 目录 一、导读二、概览三、使用四、原理分析4.1 自动初始化4.1.1 初始化…

人工智能_机器学习074_SVM支持向量机_软间隔与优化目标函数构建_C参数由来_惩罚误差点的惩罚度---人工智能工作笔记0114

然后我们接着上一节再来看一下这里我们说有个 min_faces_per_person = 0 这个可以看到如果我们写上0,就意味着要加载所有的人脸图片,就会花费的时间久对吧 我们可以试试,这里我们 min_faces_per_person = 0 改成0然后 我们等一会加载完了以后,我们用 display(X.shape,faces.sh…

Jenkins安装与设置(插件安装失败,版本问题解决)

早期的使用docker安装jenkins的方法会出现插件无法安装的问题,是由于docker拉取的jenkins版本太低了 jdk安装 Linux系统安装JDK1.8 详细流程 maven安装: centos7下安装Maven 使用docker进行安装jenkins: 先把镜像和容器卸干净 docker ps -a…

vue data变量不能以“_”开头,否则会产生很多怪异问题

1、 比如给子组件赋值&#xff0c;子组件无法得到这个值&#xff08;也不是一直无法得到&#xff0c;设置后this.$forceUpdate() 居然可以得到&#xff09;&#xff0c; 更无法watch到 <zizujian :config"_config1"> </zizujian>this._config1 { ...…

短视频矩阵系统的崛起和影响

近年来&#xff0c;短视频矩阵系统已经成为了社交媒体中的一股新势力。这个新兴的社交媒体形式以其独特的魅力和吸引力&#xff0c;迅速吸引了大量的用户。这个系统简单来说就是将海量短视频整合在一个平台上&#xff0c;使用户可以方便地观看和分享好玩有趣的短视频。 短视频…

50个免费的 AI 工具,提升工作效率(附网址)

上次我们已经介绍了20个精选的提高工作效率的免费AI工具&#xff0c;但如果你觉得这些AI工具还不过瘾的话&#xff0c;想进一步成为职场中最了解AI的人&#xff0c;本文将汇总介绍免费最新的50个AI工具。 DeepSwap DeepSwap 是一个基于 AI 的工具&#xff0c;适用于想要制作令人…

【内存泄漏】内存泄漏及常见的内存泄漏检测工具介绍

内存泄漏介绍 什么是内存泄漏 内存泄漏是指程序分配了一块内存&#xff08;通常是动态分配的堆内存&#xff09;&#xff0c;但在不再需要这块内存的情况下未将其释放。内存泄漏会导致程序浪费系统内存资源&#xff0c;持续的内存泄漏还导致系统内存的逐渐耗尽&#xff0c;最…

android内存管理机制概览

关于作者&#xff1a;CSDN内容合伙人、技术专家&#xff0c; 从零开始做日活千万级APP。 专注于分享各领域原创系列文章 &#xff0c;擅长java后端、移动开发、人工智能等&#xff0c;希望大家多多支持。 目录 一、导读二、概览三、相关概念3.1 垃圾回收3.2 应用内存的分配与回…

插入排序详解(C语言)

前言 插入排序是一种简单直观的排序算法&#xff0c;在小规模数据排序或部分有序的情况下插入排序的表现十分良好&#xff0c;今天我将带大家学习插入排序的使用。let’s go ! ! ! 插入排序 插入排序的基本思想是将待排序的序列分为已排序和未排序两部分。初始时&#xff0c…

商务大厦安装电气火灾监控系统,从源头监控电气火灾

安科瑞电气股份有限公司 上海嘉定 201801 摘要&#xff1a;介绍分析剩余电流式电气火灾监控系统的特点、组成、设计依据、监控原理和实施方案&#xff0c;并结合上海市某大厦工程设计实例探讨该系统在高层建筑中的设置要求和应用方式&#xff0c;供设计人员参考。 关键词&…

蓝桥杯嵌入式LED

1.LED原理图 2.CubeMX的LED的GPIO 3.创建LED.c和.h文件添加到bsp文件 添加bsp文件路径在c/c中 4.LED相关代码

Django之DRF框架三,序列化组件

一、序列化类的常用字段和字段参数 常用字段 字段名字段参数CharFieldmax_lengthNone, min_lengthNone, allow_blankFalse, trim_whitespaceTrueIntegerFieldmax_valueNone, min_valueNoneFloatFieldmax_valueNone, min_valueNoneBooleanFieldNullBooleanFieldFloatFieldmax_…

【隐私保护】使用Python从文本中删除个人信息:第一部分

自我介绍 做一个简单介绍&#xff0c;酒架年近48 &#xff0c;有20多年IT工作经历&#xff0c;目前在一家500强做企业架构&#xff0e;因为工作需要&#xff0c;另外也因为兴趣涉猎比较广&#xff0c;为了自己学习建立了三个博客&#xff0c;分别是【全球IT瞭望】&#xff0c;【…

【开源】基于JAVA语言的大学生相亲网站

目录 一、摘要1.1 项目介绍1.2 项目录屏 二、功能模块三、系统展示四、核心代码4.1 查询会员4.2 查询相亲大会4.3 新增留言4.4 查询新闻4.5 新增新闻 五、免责说明 一、摘要 1.1 项目介绍 基于JAVAVueSpringBootMySQL的大学生相亲网站&#xff0c;包含了会员管理模块、新闻管…

图灵日记之java奇妙历险记--输入输出方法数组

目录 输入输出输出到控制台从键盘输入使用 Scanner 读取字符串/整数/浮点数使用 Scanner 循环读取 猜数字方法方法定义方法调用的执行过程实参和形参的关系(重要)方法重载 数组数组的创建数组的初始化动态初始化静态初始化 数组的使用元素访问遍历数组 数组是引用类型null数组应…

龙芯杯个人赛串口——做一个 UART串口——RS-232

文章目录 Async transmitterAsync receiver1. RS-232 串行接口的工作原理DB-9 connectorAsynchronous communicationHow fast can we send data? 2.波特率时钟生成器Parameterized FPGA baud generator 3.RS-232 transmitter数据序列化完整代码&#xff1a; 4.RS-232 receiver…

CEC2013(python):六种算法(RFO、PSO、CSO、WOA、DBO、ABC)求解CEC2013

一、六种算法简介 1、红狐优化算法RFO 2、粒子群优化算法PSO 3、鸡群优化算法CSO 4、鲸鱼优化算法WOA 5、蜣螂优化算法DBO 6、人工蜂群算法 &#xff08;Artificial Bee Colony Algorithm, ABC&#xff09; 二、6种算法求解CEC2013 &#xff08;1&#xff09;CEC2013简…