by emanjusaka from https://www.emanjusaka.top/archives/8 彼岸花开可奈何

本文欢迎分享与聚合,全文转载请留下原文地址。

前言

AbstractQueuedSynchronizer抽象同步队列简称AQS,它是实现同步器的基础组件,并发包中锁的底层就是使用AQS实现的。大多数开发者可能永远不会直接使用AQS,但是知道其原理对于架构设计还是很有帮助的。AQS是Java中的一个抽象类,全称是AbstractQueuedSynchronizer,即抽象队列同步器。它定义了两种资源共享模式:独占式和共享式。独占式每次只能有一个线程持有锁,例如ReentrantLock实现的就是独占式的锁资源;共享式允许多个线程同时获取锁,并发访问共享资源,ReentrantReadWriteLock和CountDownLatch等就是实现的这种模式。AQS维护了一个volatile的state变量和一个FIFO(先进先出)的队列。其中state变量代表的是竞争资源标识,而队列代表的是竞争资源失败的线程排队时存放的容器 。

一、原理

AQS的核心思想是通过一个FIFO的队列来管理线程的等待和唤醒,同时维护了一个state变量来表示同步状态,可以通过getState、setState、compareAndSetState函数修改其值。当一个线程想要获取锁时,如果state为0,则表示该线程获取锁成功,否则表示该线程获取锁失败。它将被放入等待队列中,直到满足特定条件才能再次尝试获取。当一个线程释放锁时,如果state为1,则表示该线程释放锁成功,否则表示该线程释放锁失败。AQS通过CAS操作来实现加锁和解锁。

1.1 CLH队列

AQS中的CLH队列锁是CLH锁的一种变体,将自旋操作改成了阻塞线程操作。AQS 中的对 CLH 锁数据结构的改进主要包括三方面:扩展每个节点的状态、显式的维护前驱节点和后继节点以及诸如出队节点显式设为 null 等辅助 GC 的优化。

在 AQS(AbstractQueuedSynchronizer)中使用的 CLH 队列,head 指针和 tail 指针分别指向 CLH 队列中的两个关键节点。

  1. head 指针:head 指针指向 CLH 队列中的首个节点,该节点表示当前持有锁的线程。当一个线程成功地获取到锁时,它就成为了持有锁的线程,并且会将该信息记录在 head 指针所指向的节点中。
  2. tail 指针:tail 指针指向 CLH 队列中的最后一个节点,该节点表示队列中最后一个等待获取锁的线程。当一个线程尝试获取锁时,它会生成一个新的节点,并将其插入到 CLH 队列的尾部,然后成为 tail 指针所指向的节点。这样,tail 指针的作用是标记当前 CLH 队列中最后一个等待获取锁的线程。

通过 head 指针和 tail 指针,CLH 队列能够维护一种有序的等待队列结构,保证线程获取锁的顺序和互斥访问的正确性。当一个线程释放锁时,它会修改当前节点的状态,并唤醒后继节点上的线程,让后续的线程能够及时感知锁的释放,并争夺获取锁的机会。

1.2 线程同步

对于AQS来说,线程同步的关键是对状态值state进行操作。state为0时表示没有线程持有锁,大于0时表示有线程持有锁。根据state是否属于一个线程,操作state的方式分为独占方式和共享方式。

在独占方式下获取和释放资源使用的方法为:

  • void acquire(int arg)
  • void acquireInterruptibly(int arg)
  • boolean release(int arg)

使用独占方式获取的资源是与具体线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取到了,其他线程再尝试操作state获取资源时会发现当前该资源不是自己持有的,就会在获取失败后被阻塞。

在共享方式下获取和释放资源的方法为:

  • void acquireShared(int arg)
  • voidacquireSharedInterruptibly(int arg)
  • boolean releaseShared(int arg)。

对应共享方式的资源与具体线程是不相关的,当多个线程去请求资源时通过CAS方式竞争获取资源,当一个线程获取到了资源后,另外一个线程再次去获取时如果当前资源还能满足它的需要,则当前线程只需要使用CAS方式进行获取即可。

二、资源获取与释放

2.1 独占式

  1. 当一个线程调用acquire(int arg)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。

        public final void acquire(int arg) {
    if (! tryAcquire(arg) &&
    acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
    selfInterrupt();
    }
  1. 当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

        public final boolean release(int arg) {
    if (tryRelease(arg)) {
    Node h = head;
    if (h ! = null && h.waitStatus ! = 0)
    unparkSuccessor(h);
    return true;
    }
    return false;
    }

2.2 共享式

  1. 当线程调用acquireShared(int arg)获取共享资源时,会首先使用tryAcquireShared尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.SHARED的Node节点后插入到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己。

        public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
    doAcquireShared(arg);
    }
  1. 当一个线程调用releaseShared(int arg)时会尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用LockSupport.unpark(thread)激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryReleaseShared查看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。

        public final boolean releaseShared(int arg) {
    if (tryReleaseShared(arg)) {
    doReleaseShared();
    return true;
    }
    return false;
    }

三、基于AQS实现自定义同步器

基于AQS实现一个不可重入的独占锁,自定义AQS需要重写一系列函数,还需要定义原子变量state的含义。这里定义,state为0表示目前锁没有被线程持有,state为1表示锁已经被某一个线程持有,由于是不可重入锁,所以不需要记录持有锁的线程获取锁的次数。

package top.emanjusaka;

import java.io.Serializable;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock; public class UnReentrantLock implements Lock, Serializable {
// 借助 AbstractQueuedSynchronizer 实现
private static class Sync extends AbstractQueuedSynchronizer {
// 查看是否有线程持有锁
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 尝试获取锁
public boolean tryAcquire(int acquires) {
assert acquires == 1;
// 使用CAS 设置state
if (compareAndSetState(0, 1)) {
// 如果 CAS 操作成功,表示成功获得了锁。这时,通过 setExclusiveOwnerThread 方法将当前线程设置为独占锁的拥有者
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
// 如果 CAS 操作失败,即无法将 state 的值从 0 设置为 1,表示锁已被其他线程占用,无法获取锁,于是返回 false。
return false;
}
// 尝试释放锁
protected boolean tryRelease(int releases) {
assert releases == 1;
if (getState() == 0)
throw new IllegalMonitorStateException();
// 释放成功,将独占锁的拥有者设为null
setExclusiveOwnerThread(null);
// 将state的值设为0
setState(0);
return true;
} Condition newCondition() {
return new ConditionObject();
}
} private final Sync sync = new Sync(); @Override
public void lock() {
sync.acquire(1);
} @Override
public boolean tryLock() {
return sync.tryAcquire(1);
} @Override
public void unlock() {
sync.release(1);
} @Override
public Condition newCondition() {
return sync.newCondition();
} public boolean isLocked() {
return sync.isHeldExclusively();
} public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
} public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
} }

在释放锁时并没有使用 CAS(Compare and Swap)操作,而是直接使用了 setState​ 方法来将 state​ 的值设置为 0。

这是因为在释放锁的过程中,并不需要涉及到多线程并发的问题。只有持有锁的线程才能够释放锁,其他线程无法对锁进行操作。因此,不需要使用 CAS 来进行原子性的状态更新。

在这种情况下,可以直接使用普通的方法来设置 state​ 的值为 0,将独占锁的拥有者设为 null。因为只有一个线程可以操作这个锁,不存在并发竞争的情况,也就不需要使用 CAS 来保证原子性。

需要注意的是,当调用 tryRelease​ 方法时,应该保证当前线程是持有锁的线程,否则会抛出 IllegalMonitorStateException​ 异常。这是为了确保只有拥有锁的线程才能释放锁,防止误释放其他线程的锁。

四、参考资料

  1. 《并发编程之美》
  2. ​AbstractQueuedSynchronizer​​抽象类的源码

本文原创,才疏学浅,如有纰漏,欢迎指正。尊贵的朋友,如果本文对您有所帮助,欢迎点赞,并期待您的反馈,以便于不断优化。

原文地址: https://www.emanjusaka.top/archives/8

微信公众号:emanjusaka的编程栈

探索抽象同步队列 AQS的更多相关文章

  1. Java 同步锁ReentrantLock与抽象同步队列AQS

    AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它.AQS继承了AbstractOwnableSynchronizer类, ...

  2. 抽象同步队列AQS原理和实践

    AQS简述 AQS是一个FIFO的双向队列,队列元素类型为Node(也就是Thread).AQS有一个state属性,ReentrantLock可以用来便是当前线程获取锁的可重入次数:对于samaph ...

  3. Java并发编程3-抽象同步队列AQS详解

    AQS是AtractQueuedSynchronizer(队列同步器)的简写,是用来构建锁或其他同步组件的基础框架.主要通过一个int类型的state来表示同步状态,内部有一个FIFO的同步队列来实现 ...

  4. 【死磕Java并发】-----J.U.C之AQS:CLH同步队列

    此篇博客全部源代码均来自JDK 1.8 在上篇博客[死磕Java并发]-–J.U.C之AQS:AQS简单介绍中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个F ...

  5. 5. AQS(AbstractQueuedSynchronizer)抽象的队列式的同步器

    5.1 AbstractQueuedSynchronizer里面的设计模式--模板模式 模板模式:父类定义好了算法的框架,第一步做什么第二步做什么,同时把某些步骤的实现延迟到子类去实现. 5.1.1 ...

  6. AQS独占式同步队列入队与出队

    入队 Node AQS同步队列和等待队列共用同一种节点结构Node,与同步队列相关的属性如下. prev 前驱结点 next 后继节点 thread 入队的线程 入队节点的状态 INITIAl 0 初 ...

  7. J.U.C之AQS:CLH同步队列

    此篇博客所有源码均来自JDK 1.8 在上篇博客[死磕Java并发]—–J.U.C之AQS:AQS简介中提到了AQS内部维护着一个FIFO队列,该队列就是CLH同步队列. CLH同步队列是一个FIFO ...

  8. 学习JUC源码(1)——AQS同步队列(源码分析结合图文理解)

    前言 最近结合书籍<Java并发编程艺术>一直在看AQS的源码,发现AQS核心就是:利用内置的FIFO双向队列结构来实现线程排队获取int变量的同步状态,以此奠定了很多并发包中大部分实现基 ...

  9. Java线程同步之一--AQS

    Java线程同步之一--AQS 线程同步是指两个并发执行的线程在同一时间不同时执行某一部分的程序.同步问题在生活中也很常见,就比如在麦当劳点餐,假设只有一个服务员能够提供点餐服务.每个服务员在同一时刻 ...

  10. Java并发包之同步队列SynchronousQueue理解

    1 简介 SynchronousQueue是这样一种阻塞队列,其中每个put必须等待一个take,反之亦然.同步队列没有任何内部容量,甚至连一个队列的容量都没有.不能在同步队列上进行peek,因为仅在 ...

随机推荐

  1. Adobe 构建 IDP 之路的经验与教训

    在过去的25年多时间里,我创建了软件组件和分布式框架,建立并领导了相关团队.近几年我致力于推动 Adobe 服务开发.部署和管理系统的开发人员生产力. 抽象陷阱 在云时代早期,Adobe 的每个团队都 ...

  2. RedHat7.4配置yum源(原创!详细易懂)

    redhat7 .4配置centOS yum源(自带yum文件) 1.定位到yum的配置文件 root@192.168.6.129:/etc# cd yum.repos.d 2.检查yum是否安装,以 ...

  3. CSS中常见的场景实现

    如何实现两栏布局 实现两栏布局一般指的是左边固定,右边自适应,这里给出几个案例给大家参考 直接使用 calc 计算 right 宽度 .left { width: 200px; background: ...

  4. 【Java技术专题】「攻破技术盲区」带你攻破你很可能存在的Java技术盲点之动态性技术原理指南(反射技术专题)

    @ 目录 带你攻破你很可能存在的Java技术盲点之动态性技术原理指南 编程语言的类型 静态类型语言 动态类型语言 技术核心方向 反射API 反射案例介绍 反射功能操作 获取构造器 长度可变的参数 - ...

  5. MyBatis-plus乐观锁

    什么是乐观锁呢?为什么要使用这个功能?这个功能能做什么呢?如何使用这个? 1.乐观锁( Optimistic Locking ) 是相对悲观锁而言的,乐观锁是假设认为数据一般情况下不会造成冲突,所以在 ...

  6. Linux系统运维之zabbix配置tomcat监控

    一.介绍 半年前安装的zabbix监控,当时配合异地的测试人员给A项目做压力测试,主要监控项目部署的几台服务器的内存.CPU信息,以及后来网络I/O等,也没考虑JVM:最近闲下来,想完善下监控,故留此 ...

  7. Kubernetes(k8s) Web-UI界面(一):部署和访问仪表板(Dashboard)

    目录 一.系统环境 二.前言 三.仪表板(Dashboard)简介 四.部署Kubernetes仪表板(Dashboard) 五.访问Kubernetes仪表板(Dashboard) 5.1 使用to ...

  8. CANopen转ProfiNet网关在大跨径门机起重设备同步纠偏控制应用案例

    大型门机起重设备纠偏控制系统采用CanOpen通讯协议,而PLC使用的是ProfiNet协议,看似不兼容的两种协议如何实现互通?今天我们来看一下这个案例. 通过捷米特JM-COP-PN设置纠偏系统的参 ...

  9. 【技术积累】JavaScript中的基础语法【一】

    Math对象 JavaScript中的Math对象是一个内置的数学对象,表示对数字进行数学运算的方法和属性的集合. Math对象不是一个构造函数,所以不能使用new关键字来创建一个Math对象的实例. ...

  10. 【调制解调】PM 调相

    说明 学习数字信号处理算法时整理的学习笔记.同系列文章目录可见 <DSP 学习之路>目录,代码已上传到 Github - ModulationAndDemodulation.本篇介绍 PM ...