扔掉源码,15张图带你彻底理解java AQS

简介: 扔掉源码,15张图带你彻底理解java AQS

java中AQS是AbstractQueuedSynchronizer,AQS依赖FIFO队列来提供一个框架,这个框架用于实现锁以及锁相关的同步器,比如信号量、事件等。

在AQS中,主要有两部分功能,一部分是操作state变量,第二部分是实现排队和阻塞机制。

注意,AQS并没有实现任何同步接口,它只是提供了类似acquireInterruptible的方法,调用这些方法可以实现锁和同步器。

管程模型

java使用MESA管程模型来管理类的成员变量和方法,让这个类的成员变量和方法的操作是线程安全的。下图是MESA管程模型,里面除了定义共享变量外,还定义了条件变量和条件变量等待队列:

微信图片_20221212185848.pngjava中的MESA模型有一点改进,就是管程内部只有一个条件变量和一个等待队列。下图是AQS的管程模型:

微信图片_20221212185920.png

AQS的管程模型依赖AQS中的FIFO队列实现入口等待队列,而ConditionObject则实现了条件队列,这个队列可以创建多个。本文主要讲解入口等待队列获取锁的几种方式。参考1[1]

获取独占锁

独占, 忽略interrupts

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

这里的tryAcquire是抽象方法,有AQS的子类来实现,因为每个子类实现的锁是不一样的。

入队

上面的代码可以看到,获取锁失败后,会先执行addWaiter方法加入队列,然后执行acquireQueued方法自旋地获取锁直到成功。

addWaiter代码逻辑如下图,简单说就是把node入队,入队后返回node参数给acquireQueued方法:

微信图片_20221212185942.png这里有一个点需要注意,如果队列为空,则新建一个Node作为队头。

入队后获取锁

acquireQueued自旋获取锁逻辑如下图:

微信图片_20221212190002.png


这里有几个细节:

1.waitStatus

  • CANCELLED(1):当前节点取消获取锁。当等待超时或被中断(响应中断),会触发变更为此状态,进入该状态后节点状态不再变化。
  • SIGNAL(-1):后面节点等待当前节点唤醒。
  • CONDITION(-2):Condition中使用,当前线程阻塞在Condition,如果其他线程调用了Condition的signal方法,这个结点将从等待队列转移到同步队列队尾,等待获取同步锁。
  • PROPAGATE(-3):共享模式,前置节点唤醒后面节点后,唤醒操作无条件传播下去。
  • 0:中间状态,当前节点后面的节点已经唤醒,但是当前节点线程还没有执行完成。

2.获取锁失败后挂起

如果前置节点不是头节点,或者前置节点是头点但当前节点获取锁失败,这时当前节点需要挂起,分三种情况,

前置节点waitStatus=-1,如下图:

微信图片_20221212190026.png

前置节点waitStatus > 0,如下图:

微信图片_20221212190045.png

前置节点waitStatus < 0 但不等于 -1,如下图:

微信图片_20221212190107.png

3.取消获取锁

如果获取锁抛出异常,则取消获取锁,如果当前节点是tail节点,分两种情况如下图:

微信图片_20221212190127.png

如果当前节点不是tail节点,也分两种情况,如下图:

微信图片_20221212190150.png

4.对中断状态忽略

5.如果前置节点的状态是 0 或 PROPAGATE,会被当前节点自旋过程中更新成-1,以便之后通知当前节点。

独占 + 响应中断

对应方法acquireInterruptibly(int arg)。

跟忽略中断(acquire方法)不同的是要响应中断,下面两个地方响应中断:

  • 获取锁之前会检查当前线程是否中断。
  • 获取锁失败入队,在队列中自旋获取锁的过程中也会检查当前线程是否中断。

如果检查到当前线程已经中断,则抛出InterruptedException,当前线程退出。

独占 + 响应中断 + 考虑超时

对应方法tryAcquireNanos(int arg, long nanosTimeout)。

这个方法具备了独占 + 响应中断 + 超时的功能,下面2个地方要判断是否超时:

  • 自旋获取锁的过程中每次获取锁失败都要判断是否超时
  • 获取锁失败park之前要判断超时时间是否大于自旋的阈值时间**(spinForTimeoutThreshold = 1ns)**

另外,park线程的操作使用parkNanos传入阻塞时间。

释放独占锁

独占锁释放分两步:释放锁,唤醒后继节点。

释放锁的方法 tryRelease 是抽象的,由子类去实现。

我们看一下唤醒后继节点的逻辑,首先需要满足两个条件:

  • head节点不等于 null
  • head节点waitStatus不等于0

这里有两种情况(在方法unparkSuccessor):

  • 情况一,后继节点waitStatus <= 0,直接唤醒后继节点,如下图:

微信图片_20221212190213.png

情况二:后继节点为空或者waitStatus > 0,从后往前查找最接近当前节点的节点进行唤醒,如下图:

微信图片_20221212190232.png


获取共享锁

之前我们讲了独占锁,这一小节我们谈共享锁,有什么不同呢?

共享,忽略interrupts

对应方法acquireShared,代码如下:

public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        doAcquireShared(arg);
}

tryAcquireShared

这里获取锁使用的方法是tryAcquireShared,获取的是共享锁。获取共享锁跟获取独占锁不同的是,会返回一个整数值,说明如下:

  • 返回负数:获取锁失败。
  • 返回0:获取锁成功但是之后再由线程来获取共享锁时就会失败。
  • 返回正数:获取锁成功而且之后再有线程来获取共享锁时也可能会成功。所以需要把唤醒操作传播下去。

tryAcquireShared获取锁失败后(返回负数),就需要入队后自旋获取,也就是执行方法doAcquireShared。

doAcquireShared

怎么判断队列中等待节点是在等待共享锁呢?nextWaiter == SHARED,这个参数值是入队新建节点的时候构造函数传入的。

自旋过程中,如果获取锁成功(返回正数),首先把自己设置成新的head节点,然后把通知传播下去。如下图:net/anlian523/article/details/106319294/

微信图片_20221212190256.png


之后会唤醒后面节点并保证唤醒操作可以传播下去。但是需要满足四个条件中的一个:

  • tryAcquireShared返回值大于0,有多余的锁,可以继续唤醒后继节点
  • 旧的head节点waitStatus < 0,应该是其他线程释放共享锁过程中把它的状态更新成了-3
  • 新的hade节点waitStatus < 0,只要不是tail节点,就可能是-1

这里会造成不必要的唤醒,因为唤醒后获取不到锁只能继续入队等待

  • 当前节点的后继节点是空或者非空但正在等待共享锁

唤醒后面节点的操作,其实就是释放共享锁,对应方法是doReleaseShared,见释放共享锁一节。

共享 + 响应中断

对应方法acquireSharedInterruptibly(int arg)。

跟共享忽略中断(acquireShared方法)不同的是要响应中断,下面两个地方响应中断:

  • 获取锁之前会检查当前线程是否中断。
  • 获取锁失败入队,在队列中自旋获取锁的过程中也会检查当前线程是否中断。

如果检查到当前线程已经中断,则抛出InterruptedException,当前线程退出。

共享 + 响应中断 + 考虑超时

对应方法tryAcquireSharedNanos(int arg, long nanosTimeout)。

这个方法具备了共享 + 响应中断 + 超时的功能,下面2个地方要判断是否超时:

  • 自旋获取锁的过程中每次获取锁失败都要判断是否超时
  • 获取锁失败park之前要判断超时时间是否大于自旋的阈值时间(spinForTimeoutThreshold = 1ns)

另外,park线程的操作使用parkNanos传入阻塞时间。

释放共享锁

释放共享锁代码如下:

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

首先尝试释放共享锁,tryReleaseShared代码由子类来实现。释放成功后执行AQS中的doReleaseShared方法,是一个自旋操作。

自旋的条件是队列中至少有两个节点,这里分三种情况。

情况一:当前节点waitStatus是-1,如下图:

微信图片_20221212190323.png情况二:当前节点waitStatus是0(被其他线程更xin新成了中间状态),如下图:

微信图片_20221212190342.png

情况三:当前节点waitStatus是-3,为什么会这样呢?需要解释一下,head节点唤醒后继节点之前waitStatus已经被更新中间态0了,唤醒后继节点动作还没有执行,又被其他线程更成了-3,也就是其他线程释放锁执行了上面情况二。这时需要先把waitStatus再更成0(在方法unparkSuccessor),如下图:

微信图片_20221212190402.png

抽象方法

上面的讲解可以看出,如果要基于AQS来实现并发锁,可以根据需求重写下面四个方法来实现,这四个方法在AQS中没有具体实现:

  • tryAcquire(int arg):获取独占锁
  • tryRelease(int arg):释放独占锁
  • tryAcquireShared(int arg):获取共享锁
  • tryReleaseShared(int arg):释放共享锁 参考2[2]

AQS的子类需要重写上面的方法来修改state值,并且定义获取锁或者释放锁时state值的变化。子类也可以定义自己的state变量,但是只有更新AQS中的state变量才会对同步起作用。

还有一个判断当前线程是否持有独占锁的方法 isHeldExclusively,也可以供子类重写后使用。

获取/释放锁的具体实现放到下篇文章讲解。

总结

AQS使用FIFO队列实现了一个锁相关的并发器模板,可以基于这个模板来实现各种锁,包括独占锁、共享锁、信号量等。

AQS中,有一个核心状态是waitStatus,这个代表节点的状态,决定了当前节点的后续操作,比如是否等待唤醒,是否要唤醒后继节点。




相关文章
|
7月前
|
设计模式 人工智能 安全
AQS:Java 中悲观锁的底层实现机制
AQS(AbstractQueuedSynchronizer)是Java并发包中实现同步组件的基础工具,支持锁(如ReentrantLock、ReadWriteLock)和线程同步工具类(如CountDownLatch、Semaphore)等。Doug Lea设计AQS旨在抽象基础同步操作,简化同步组件构建。 使用AQS需实现`tryAcquire(int arg)`和`tryRelease(int arg)`方法以获取和释放资源,共享模式还需实现`tryAcquireShared(int arg)`和`tryReleaseShared(int arg)`。
431 32
AQS:Java 中悲观锁的底层实现机制
|
8月前
|
前端开发 Java 关系型数据库
基于Java+Springboot+Vue开发的鲜花商城管理系统源码+运行
基于Java+Springboot+Vue开发的鲜花商城管理系统(前后端分离),这是一项为大学生课程设计作业而开发的项目。该系统旨在帮助大学生学习并掌握Java编程技能,同时锻炼他们的项目设计与开发能力。通过学习基于Java的鲜花商城管理系统项目,大学生可以在实践中学习和提升自己的能力,为以后的职业发展打下坚实基础。技术学习共同进步
571 7
|
8月前
|
消息中间件 算法 安全
JUC并发—1.Java集合包底层源码剖析
本文主要对JDK中的集合包源码进行了剖析。
|
3月前
|
存储 小程序 Java
热门小程序源码合集:微信抖音小程序源码支持PHP/Java/uni-app完整项目实践指南
小程序已成为企业获客与开发者创业的重要载体。本文详解PHP、Java、uni-app三大技术栈在电商、工具、服务类小程序中的源码应用,提供从开发到部署的全流程指南,并分享选型避坑与商业化落地策略,助力开发者高效构建稳定可扩展项目。
|
7月前
|
JavaScript Java 关系型数据库
家政系统源码,java版本
这是一款基于SpringBoot后端框架、MySQL数据库及Uniapp移动端开发的家政预约上门服务系统。
240 6
家政系统源码,java版本
|
7月前
|
供应链 JavaScript 前端开发
Java基于SaaS模式多租户ERP系统源码
ERP,全称 Enterprise Resource Planning 即企业资源计划。是一种集成化的管理软件系统,它通过信息技术手段,将企业的各个业务流程和资源管理进行整合,以提高企业的运营效率和管理水平,它是一种先进的企业管理理念和信息化管理系统。 适用于小微企业的 SaaS模式多租户ERP管理系统, 采用最新的技术栈开发, 让企业简单上云。专注于小微企业的应用需求,如企业基本的进销存、询价,报价, 采购、销售、MRP生产制造、品质管理、仓库库存管理、财务应收付款, OA办公单据、CRM等。
446 23
|
6月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
355 3
|
8月前
|
Java 关系型数据库 MySQL
Java汽车租赁系统源码(含数据库脚本)
Java汽车租赁系统源码(含数据库脚本)
218 4
|
安全 Java
Java并发编程笔记之CopyOnWriteArrayList源码分析
并发包中并发List只有CopyOnWriteArrayList这一个,CopyOnWriteArrayList是一个线程安全的ArrayList,对其进行修改操作和元素迭代操作都是在底层创建一个拷贝数组(快照)上进行的,也就是写时拷贝策略。
19662 0
|
Java 安全
Java并发编程笔记之读写锁 ReentrantReadWriteLock 源码分析
我们知道在解决线程安全问题上使用 ReentrantLock 就可以,但是 ReentrantLock 是独占锁,同时只有一个线程可以获取该锁,而实际情况下会有写少读多的场景,显然 ReentrantLock 满足不了需求,所以 ReentrantReadWriteLock 应运而生,ReentrantReadWriteLock 采用读写分离,多个线程可以同时获取读锁。
3281 0