abstractqueuedsynchronizer 中文翻译叫做同步器,简称 aqs,是各种各样锁的基础,比如说 reentrantlock、countdownlatch 等等,这些我们经常用的锁底层实现都是 aqs,所以学好 aqs 对于后面理解锁的实现是非常重要的。
锁章节的内容是这么安排的:
1:aqs 源码非常多,我们会分成两个小节来说,先把底层原理弄清楚;
2:我们平时用不到 aqs,只会接触到 reentrantlock、countdownlatch 这些锁,我们以两个锁为例子,讲解下源码,因为 aqs 只要弄懂了,所有的锁你只要清楚锁的目的,就能够利用 aqs 去实现它;
3:总结一下锁的面试题;
4:总结一下锁在工作中有哪些使用场景,举几个实际的例子,看看锁使用时,有哪些注意事项;
5:最后我们自己来实现一个锁,看看如果我们自己来实现锁,有哪些步骤,需要注意哪些事项。
ps:本章内容需要大量队列基础知识,没有看过第四章节队列的同学,建议先阅读下队列章节。
首先我们来看一下 aqs 的整体架构图,如下:
这个图总结了 aqs 整体架构的组成,和部分场景的动态流向,图中两个点说明一下,方便大家观看。
aqs 中队列只有两个:同步队列 + 条件队列,底层数据结构两者都是链表礼物的含义;图中有四种颜色的线代表四种不同的场景,1、2、3 序号代表看的顺序。aqs 本身就是一套锁的框架,它定义了获得锁和释放锁的代码结构,所以如果要新建锁,只要继承 aqs,并实现相应方法即可。
接下来我们一起来看下这个图中各个细节点。
首先我们来看一下,从 aqs 类注释上,我们可以得到哪些信息:
提供了一种框架,自定义了先进先出的同步队列,让获取不到锁的线程能进入同步队列中排队;同步器有个状态字段,我们可以通过状态字段来判断能否得到锁,此时设计事业单位退休的关键在于依赖安全的 atomic value 来表示状态(虽然注释是这个意思,但实际上是通过把状态声明为 volatile,在锁里面修改状态值来保证线程安全的);子类可以通过给状态 cas 赋值来决定能否拿到锁,可以定义那些状态可以获得锁,哪些状态表示获取不到锁(比如定义状态值是 0 可以获得锁,状态值是 1 就获取不到锁);子类可以新建非 public 的内部类,用内部类来继承 aqs,从而实现锁的功能;aqs 提供了排它模式和共享模式两种锁模式。排它模式下:只有一个线程可以获得锁,共享模式可以让多个线程获得锁,子类 readwritelock 实现了两种模式;内部类 conditionobject 可以被用作 condition,我们通过 new conditionobject () 即可得到条件队列;aqs 实现了锁、排队、锁队列等框架,至于如何获得锁、释放锁的代码并没有实现,比如 tryacquire、tryrelea、tryacquireshared、tryreleashared、isheldexclusively 这些方法,aqs 中默认抛 unsupportedoperationexception 异常,都是需要子类去实现的;aqs 继承 abstractownablesynchronizer 是为了方便跟踪获得锁的线程,可以帮助监控和诊断工具识别是哪些线程持有了锁;aqs 同步队列和条件队列,获取不到锁的节点在入队时是先进先出,但被唤醒时,可能并不会按照先进先出的顺序执行。aqs 的注释还有很多很多,以上 9 点是挑选出来稍微比较重要的注释总结。
aqs 类定义代码如下:
可以看出两点:
aqs 是个抽象类,就是给各种锁子类继承用的,aqs 定义了很多如何获得锁,如何释放锁的抽象方法,目的就是为了让子类去实现;
继承了教室一角 abstractownablesynchronizer,abstractownablesynchronizer 的作用就是为了知道当前是那个线程获得了锁,方便监控用的,
代码如下:
aqs 的属性可简单分为四类:同步器简单属性、同步队列属性、条件队列属性、公用 node。
首先我们来看一下简单属性有哪些:
最重要的就是 state 属性,是 int 属性的,所有继承 aqs 的锁都是通过这个字段来判断能不能获得锁,能不能释放锁。
首先我们介绍以下同步队列:当多个线程都来请求锁时,某一时刻有且只有一个线程能够获得锁(排它锁),那么剩余获取不到锁的线程,都会到同步队列中去排队并阻塞自己,当有线程主动释放锁时,就会从同步队列头开始释放一个排队的线程,让线程重新去竞争锁。
所以同步队列的主要作用阻塞获取不到锁的线程,并在适当时机释放这些线程。
同步队列底层数据结构是个双向链表,我们从源码中可以看到链表的头尾,如下:
源码中的 node 是同步队列中的元素,但 node 被同步队列和条件队列公用,所以我们在说完条件队列之后再说 node。
首先我们介绍下条件队列:条件队列和同步队列的功能一样,管理获取不到锁的线程,底层数据结构也是链表队列,但条件队列不直接和锁打交道,但常常和锁配合使用,是一定的场景下,对锁功能的一种补充。
条件队列的属性如下:
conditionobject 我们就称为条件队列,我们需要使用时,直接 new conditionobject () 即可。
conditionobject 是实现 condition 接口的,condition 接口相当于 object 的各种监控方法,比如 object#wait ()、object#notify、object#notifyall 这些方法,我们可以先这么理解,后面会细说。
node 非常重要,即是同步队列的节点,又是条件队列的节点,在入队的时候,我们用 node 把线程包装一下,然后把 node 放入两个队列中,我们看下 node 的数据结构,如下:
从 node 的结构中,我们需要重点关注 wa关于月亮itstatus 字段,node 的很多操作都是围绕着 waitstatus 字段进行的。
node 的 pre、next 属性是同步队列中的链表前后指向字段,nextwaiter 是条件队列中下一个节点的指向字段,但在同步队列中,nextwaiter 只是一个标识符,表示当前节点是共享还是排它模式。
排它锁的意思是同一时刻,只能有一个线程可以获得锁,也只能有一个线程可以释放锁。
共享锁可以允许多个线程获得同一个锁,并且可以设置获取锁的线程数量。
刚才我们看条件队列 conditionobject 时,发现其是实现 condition 接口的,现在我们一起来看下 condition 接口,其类注释上是这么写的:
当 lock 代替 synchronized 来加锁时,condition 就可以用来代替 object 中相应的监控方法了,比如 object#wait ()、object#notify、object#notifyall 这些方法;提供了一种线程协作方式:一个线程被暂停执行,直到被其它线程唤醒;condition 实例是绑定在锁上的,通过 lock#newcondition 方法可以产生该实例;除了特殊说明外,任意空值作为方法的入参,都会抛出空指针;condition 提供了明确的语义和行为,这点和 object 监控方法不同。类注释上甚至还给我们举了一个例子:
假设我们有一个有界边界的队列,支持 put 和 take 方法,需要满足:
1:如果试图往空队列上执行 take,线程将会阻塞,直到队列中有可用的元素为止;
2:如果试图往满的队列上执行 put,线程将会阻塞,直到队列中有空闲的位置为止。
1、2 中线程阻塞都会到条件队列中去阻塞。
take 和 put 两种操作如果依靠一个条件队列,那么每次只能执行一种操作,所以我们可以新建两个条件队列,这样就可以分别执行操作了,看了这个需求,是不是觉得很像我们第三章学习的队列?实际上注释上给的 demo 就是我们学习过的队列,篇幅有限,感兴趣的可以看看 conditiondemo 这个测试类。
除了类注释,condition 还定义出一些方法,这些方法奠定了条件队列的基础,方法主要有:
这个方法的主要作用是:使当前线程一直等待,直到被 signalled 或被打断。
当以下四种情况发生时,条件队列中的线程将被唤醒
有线程使用了 signal 方法,正好唤醒了条件队列中的当前线程;有线程使用了 signalall 方法;其它线程打断了当前线程,并且当前线程支持被打断;被虚假唤醒 (即使没有满足以上 3 个条件,wait 也是可能被偶尔唤醒,虚假唤醒定义可以参考: https://en.wikipedia.org/wiki/spurious_wakeup)。被唤醒时,有一点需要注意的是:线程从条件队列中苏醒时,必须重新获得锁,才能真正被唤醒,这个我们在说源码的时候,也会强调这个。
await 方法还有带等待超时时间的,如下:
除了等待方法,还是唤醒线程的两个方法,如下:
至此,aqs 基本的属性就已经介绍完了,接着让我们来看一看 aqs 的重要方法。
在同步器中,我们有两个状态,一个叫做 state,一个叫做 waitstatus,两者是完全不同的概念:
state
是锁的状态,是 int 类型,子类继承 aqs 时,都是要根据 state 字段来判断有无得到锁,比如当前同步器状态是 0,表示可以获得锁,当前同步器状态是 1,表示锁已经被其他线程持有,当前线程无法获得锁;
waitstatus
是节点(node)的状态,种类很多,一共有初始化 (0)、cancelled (1)、signal (-1)、condition (-2)、propagate (-3人生的名言),各个状态的含义可以见上文。
这两个状态我们需要牢记,不要混淆了。
获取锁最直观的感受就是使用 lock.lock () 方法来获得锁,最终目的是想让线程获得对资源的访问权。
lock 一般是 aqs 的子类,lock 方法根据情况一般会选择调用 aqs 的 acquire 或 tryacquire 方法。
acquire 方法 aqs 已经实现了,tryacquire 方法是等待子类去实现,acquire 方法制定了获取锁的框架,先尝试使用 tryacquire 方法获取锁,获取不到时,再入同步队列中等待锁。tryacquire 方法 aqs 中直接抛出一个异常,表明需要子类去实现,子类可以根据同步器的 state 状态来决定是否能够获得锁,接下来我们详细看下 acquire 的源码解析。
acquire 也分两种,一种是排它锁,一种是共享锁,我们一一来看下:
以上代码的主要步骤是(流程见整体架构图中红色场景):
尝试执行一次 tryacquire,如果成功直接返回,失败走 2;线程尝试进入同步队列,首先调用 addwaiter 方法,把当前线程放到同步队列的队尾;接着调用 acquirequeued 方法,两个作用,1:阻塞当前节点,2:节点被唤醒时,使其能够获得锁;如果 2、3 失败了,打断线程。
代码很少,每个方法都是关键,接下来我们先来看下 addwaiter 的源码实现:
如果之前学习过队列的同学,对这个方法应该感觉毫不吃力,就是把新的节点追加到同步队列的队尾。
其中有一点值得我们学习的地方,是在 addwaiter 方法中,并没有进入方法后立马就自旋,而是先尝试一次追加到队尾,如果失败才自旋,因为大部分操作可能一次就会成功,这种思路在我们写自旋的时候可以借鉴。
下一步就是要阻塞当前线程了,是 acquirequeued 方法来实现的,我们来看下源码实现:
此方法的注释还是很清楚的,我们接着看下此方法的核心:shouldparkafterfailedacquire,这个方法的主要目的就是把前一个节点的状态置为 signal,只要前一个节点的状态是 signal,当前节点就可以阻塞了(parkandcheckinterrupt 就是使节点阻塞的方法),
源码如下:
acquire 整个过程非常长,代码也非常多,但注释很清楚,可以一行一行仔细看看代码。
总结一下,acquire 方法大致分为三步:
使用 tryacquire 方法尝试获得锁,获得锁直接返回,获取不到锁的走 2;把当前线程组装成节点(node),追加到同步队列的尾部(addwaiter);自旋,使同步队列中当前节点的前置节点状态为 signal 后,然后阻塞自己。整体的代码结构比较清晰,一些需要注意的点,都用注释表明了,强烈建议阅读下源码。
acquireshared 整体流程和 acquire 相同,代码也很相似,重复的源码就不贴了,我们就贴出来不一样的代码来,也方便大家进行比较:
第一步尝试获得锁的地方,有所不同,排它锁使用的是 tryacquire 方法,共享锁使用的是 tryacquireshared 方法,如下图:
第二步不同,在于节点获得排它锁时,仅仅把自己设置为同步队列的头节点即可(thead 方法),但如果是共享锁的话,还会去唤醒自己的后续节点,一起来获得该锁(theadandpropagate 方法),不同之处如下(左边排它锁,右边共享锁):
接下来我们一起来看下 theadandpropagate 方法的源码:
这个就是共享锁独特的地方,当一个线程获得锁后,它就会去唤醒排在它后面的其它节点,让其它节点也能够获得锁。
aqs 的内容实在太多了,这只是 aqs 的上篇,但内容长度已经超过了我们平时章节的三倍了,所以不得不分节,下一章仍然是 aqs,主要讲解锁的释放和条件队列两大部分。
以上就是java同步器aqs架构abstractqueuedsynchronizer原理解析的详细内容,更多关于java同步器abstractqueuedsynchronizer的资料请关注www.887551.com其它相关文章!
本文发布于:2023-04-06 03:04:14,感谢您对本站的认可!
本文链接:https://www.wtabcd.cn/fanwen/zuowen/efd1e2de15abcc801e8edb00b819cf8c.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文word下载地址:java同步器AQS架构AbstractQueuedSynchronizer原理解析.doc
本文 PDF 下载地址:java同步器AQS架构AbstractQueuedSynchronizer原理解析.pdf
留言与评论(共有 0 条评论) |