文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

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

2024-12-03 05:02

关注

本文转载自微信公众号「程序员jinjunzhu」,作者jinjunzhu。转载本文请联系程序员jinjunzhu公众号。

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

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

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

管程模型

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

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

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

获取独占锁

独占, 忽略interrupts

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

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

入队

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

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

这里有一个点需要注意,如果队列为空,则新建一个Node作为队头。

入队后获取锁

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

这里有几个细节:

waitStatus

获取锁失败后挂起

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

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

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

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

3.取消获取锁

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

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

对中断状态忽略

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

独占 + 响应中断

对应方法acquireInterruptibly(int arg)。

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

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

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

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

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

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

释放独占锁

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

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

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

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

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

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

获取共享锁

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

共享,忽略interrupts

对应方法acquireShared,代码如下:

  1. public final void acquireShared(int arg) { 
  2.     if (tryAcquireShared(arg) < 0) 
  3.         doAcquireShared(arg); 

tryAcquireShared

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

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

doAcquireShared

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

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

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

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

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

共享 + 响应中断

对应方法acquireSharedInterruptibly(int arg)。

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

获取锁之前会检查当前线程是否中断。

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

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

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

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

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

自旋获取锁的过程中每次获取锁失败都要判断是否超时

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

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

释放共享锁

释放共享锁代码如下:

  1. public final boolean releaseShared(int arg) { 
  2.     if (tryReleaseShared(arg)) { 
  3.         doReleaseShared(); 
  4.         return true
  5.     } 
  6.     return false

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

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

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

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

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

抽象方法

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

参考2[2]

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

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

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

总结

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

 

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

 

来源:程序员jinjunzhu 内容投诉

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

软考中级精品资料免费领

  • 历年真题答案解析
  • 备考技巧名师总结
  • 高频考点精准押题
  • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

    难度     813人已做
    查看
  • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

    难度     354人已做
    查看
  • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

    难度     318人已做
    查看
  • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

    难度     435人已做
    查看
  • 2024年上半年系统架构设计师考试综合知识真题

    难度     224人已做
    查看

相关文章

发现更多好内容

猜你喜欢

AI推送时光机
位置:首页-资讯-后端开发
咦!没有更多了?去看看其它编程学习网 内容吧
首页课程
资料下载
问答资讯