关于java:AQS的简单描述

42次阅读

共计 5266 个字符,预计需要花费 14 分钟才能阅读完成。

对于锁的底层框架 AQS
谈到多线程就不得不说锁,锁是保障多线程并发中保障数据安全的重要工具,当然锁的底层实现也是略微有些简单的,对于这方面的常识我也是花了很久工夫才弄明确一点,写下这篇文章也算是记录一下心得体会。
平时应用锁时可能罕用的就是 synchronized 或者是 ReentrantLock,然而锁的底层 AQS 浑然不知,借助这个机会也一次性弄懂 AQS。AQS 全称为 AbstractQueuedSynchronizer(真长),这个类是 JAVA 官网提供的,想实现特定性能的锁只须要继承这个抽象类而后实现相干的办法就能够了,这个类自身并没有具体实现具体怎么锁住代码块的性能,实际上只是保护了一个共享资源变量 (volatile int state) 和一个 FIFO 线程期待队列(线程阻塞时会将线程寄存到这个期待队列中)。具体拜访 state 的形式有三种

  1. getState()
  2. setState()
  3. compareAndState()
    AQS 定义了两种资源共享形式,Exclusive(独占模式,每次只有一个线程可能拜访资源),Share(共享模式,多个线程都能够拜访资源)。
    不同的自定义同步器 (锁,前面将用同步器代替锁这种说法,更加书面化) 共享资源的形式也不同,自定义同步器在实现时只须要实现共享资源 state 的获取与开释即可,至于线程期待队列的保护,我在后面就说过了 AQS 曾经实现了这些工作。
    • isHeldExclusively():该线程是否正在独占资源。只有用到 condition 才须要去实现它。
    • tryAcquire(int):独占形式。尝试获取资源,胜利则返回 true,失败则返回 false。
    • tryRelease(int):独占形式。尝试开释资源,胜利则返回 true,失败则返回 false。
    • tryAcquireShared(int):共享形式。尝试获取资源。正数示意失败;0 示意胜利,但没有残余可用资源;负数示意胜利,且有残余资源。
    • tryReleaseShared(int):共享形式。尝试开释资源,如果开释后容许唤醒后续期待结点返回 true,否则返回 false。
    以 ReentrantLock 为例,state 初始化为 0,示意未锁定状态。A 线程 lock()时,会调用 tryAcquire()独占该锁并将 state+1。尔后,其余线程再 tryAcquire()时就会失败,直到 A 线程 unlock()到 state=0(即开释锁)为止,其它线程才有机会获取该锁。当然,开释锁之前,A 线程本人是能够反复获取此锁的(state 会累加),这就是可重入的概念。但要留神,获取多少次就要开释如许次,这样能力保障 state 是能回到零态的。
    再以 CountDownLatch 以例,工作分为 N 个子线程去执行,state 也初始化为 N(留神 N 要与线程个数统一)。这 N 个子线程是并行执行的,每个子线程执行完后 countDown()一次,state 会 CAS 减 1。等到所有子线程都执行完后 (即 state=0),会 unpark() 主调用线程,而后主调用线程就会从 await()函数返回,持续后余动作。
    一般来说,自定义同步器要么是独占办法,要么是共享形式,他们也只需实现 tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared 中的一种即可。但 AQS 也反对自定义同步器同时实现独占和共享两种形式,如 ReentrantReadWriteLock。

后面的基础知识铺垫完了,那么当初对源码进行剖析。
1)结点状态 waitStatus
这里咱们说下 Node。Node 结点是对每一个期待获取资源的线程的封装,其蕴含了须要同步的线程自身及其期待状态,如是否被阻塞、是否期待唤醒、是否曾经被勾销等。变量 waitStatus 则示意以后 Node 结点的期待状态,共有 5 种取值 CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。
• CANCELLED(1):示意以后结点已勾销调度。当 timeout 或被中断(响应中断的状况下),会触发变更为此状态,进入该状态后的结点将不会再变动。
• SIGNAL(-1):示意后继结点在期待以后结点唤醒。后继结点入队时,会将前继结点的状态更新为 SIGNAL。
• CONDITION(-2):示意结点期待在 Condition 上,当其余线程调用了 Condition 的 signal()办法后,CONDITION 状态的结点将从期待队列转移到同步队列中,期待获取同步锁。
• PROPAGATE(-3):共享模式下,前继结点不仅会唤醒其后继结点,同时也可能会唤醒后继的后继结点。
• 0:新结点入队时的默认状态。
留神,负值示意结点处于无效期待状态,而正值示意结点已被勾销。所以源码中很多中央用 >0、<0 来判断结点的状态是否失常。
2)acquire(int)办法
此办法是独占模式下获取共享资源的顶层入口,如果获取到资源线程就间接返回,否则线程进入期待队列,直到获取资源为止,整个过程中疏忽中断。上面是 acquire(int)的源代码

函数流程如下:

  1. tryAcquire()尝试间接去获取资源,如果胜利则间接返回(这里体现了非偏心锁,每个线程获取锁时会尝试间接抢占加塞一次,而 CLH 队列中可能还有别的线程在期待);
  2. addWaiter()将该线程退出期待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程阻塞在期待队列中获取资源,始终获取到资源后才返回。如果在整个期待过程中被中断过,则返回 true,否则返回 false。
  4. 如果线程在期待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt(),将中断补上。
    当初看这些办法还有点朦胧,然而不要紧,看完接下来的剖析你就会明确了。

2)办法的源码解读
1.tryAcquire(int) 办法,源代码如下:

竟然只抛出了一个异样,很显然不对劲,然而不要遗记,我在后面就始终说 AQS 是一个框架,具体的获取和开释资源是由具体的同步器去实现的,其实这里采纳的是模版办法设计模式(一种设计模式,有趣味的敌人能够具体百度理解一下,后续我也会出一些设计模式的记录文章)。当然,这个办法之所以没有设计成 abstract,是因为独占模式下只须要实现 tryAcquire-tryRelease,而共享模式下只用实现 tryAcquireShared-tryReleaseShared。如果都定义成 abstract,那么每个模式也要去实现另一模式下的接口。很显然,写底层代码的人还是为了咱们着想,让开发者尽量减少不必要的的工作。

  1. addWaiter(Node)
    这个办法用于将以后线程退出到期待队列的队尾,并且返回以后线程所在的节点,源码如下,有注解,应该是看得比较清楚的。

这里补充一下 Node 这个类,这是 AQS 中的一个动态外部类,实际上是一个先进先出的队列,用来保护期待线程。
Enq(Node)办法,不多说了,间接上源代码

懂行的人一眼就看进去这段代码的精髓,CAS 自旋 volatile 变量,很经典的用法,如果还不相熟用法,倡议百度一下。

  1. acquireQueued(Node,int)
    通过 tryAuquire 和 addWaiter()办法,该线程曾经获取资源失败,被放入期待队列尾部了,下一步要做的就是进入期待状态劳动,晓得其它线程开释资源后唤醒本人,本人再拿到资源,而后再做想做的事件,是不是和医院拿号有点类似,拿到号之后期待后面的病人问诊实现,轮到本人时再看病,acquireQueued(Node,int)干的就是这件事:在期待队列中排队拿号,直到拿到号了再返回。这个办法十分的要害,上源代码

先不焦急看 acquireQueued()的流程,先看看 shouldParkAfterFailedAcquire()和 parkAndCheckInterrupt()具体干些什么。

shouldParkAfterFailedAcquire(Node, Node)
这个办法用于查看状态,看看本人是不是真的可能劳动了。

整个流程查问前驱的状态是不是 SIGNAL,不是的话就不能安心劳动,而是找个安心的劳动点,同时试一下看看本人有没有机会拿到号。

parkAndCheckInterrupt()

如果线程找好平安劳动点后,那就能够安心去劳动了。此办法就是让线程去劳动,真正进入期待状态。

park 会让线程进入 wating 状态,这个状态有两个路径唤醒,1.unpark();2.interrupt()。
OK,看了 shouldParkAfterFailedAcquire()和 parkAndCheckInterrupt(),当初让咱们再回到 acquireQueued(),总结下该函数的具体流程:

  1. 结点进入队尾后,查看状态,找到平安劳动点;
  2. 调用 park()进入 waiting 状态,期待 unpark()或 interrupt()唤醒本人;
  3. 被唤醒后,看本人是不是有资格能拿到号。如果拿到,head 指向以后结点,并返回从入队到拿到号的整个过程中是否被中断过;如果没拿到,持续流程 1。
  4. 小结
    OK,acquireQueued() 剖析完之后,咱们接下来再回到 acquire()!再贴上它的源码吧:

再来总结下它的流程吧:

  1. 调用自定义同步器的 tryAcquire()尝试间接去获取资源,如果胜利则间接返回;
  2. 没胜利,则 addWaiter()将该线程退出期待队列的尾部,并标记为独占模式;
  3. acquireQueued()使线程在期待队列中劳动,有机会时(轮到本人,会被 unpark())会去尝试获取资源。获取到资源后才返回。如果在整个期待过程中被中断过,则返回 true,否则返回 false。
  4. 如果线程在期待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断 selfInterrupt(),将中断补上。

至此,acquire()的流程终于算是告一段落了。这也就是 ReentrantLock.lock()的流程,不信你去看其 lock()源码吧,整个函数就是一条 acquire(1)!!!

3)开释资源过程
上一大节曾经把 acquire()说完了,这一大节就来讲讲它的反操作 release()吧。此办法是独占模式下线程开释共享资源的顶层入口。它会开释指定量的资源,如果彻底开释了(即 state=0), 它会唤醒期待队列里的其余线程来获取资源。这也正是 unlock()的语义,当然不仅仅只限于 unlock()。上面是 release()的源码:

逻辑并不简单。它调用 tryRelease()来开释资源。有一点须要留神的是,它是依据 tryRelease()的返回值来判断该线程是否曾经实现开释掉资源了!所以自定义同步器在设计 tryRelease()的时候要明确这一点!!

1.tryRelease(int)
此办法尝试去开释指定量的资源。上面是 tryRelease()的源码:

跟 tryAcquire()一样,这个办法是须要独占模式的自定义同步器去实现的。失常来说,tryRelease()都会胜利的,因为这是独占模式,该线程来开释资源,那么它必定曾经拿到独占资源了,间接减掉相应量的资源即可 (state-=arg),也不须要思考线程平安的问题。但要留神它的返回值,下面曾经提到了,release() 是依据 tryRelease()的返回值来判断该线程是否曾经实现开释掉资源了!所以自义定同步器在实现时,如果曾经彻底开释资源(state=0),要返回 true,否则返回 false。

  1. unparkSuccessor(Node)
    此办法用于唤醒期待队列中下一个线程。上面是源码:

这个函数并不简单。一句话概括:用 unpark()唤醒期待队列中最前边的那个未放弃线程,这里咱们也用 s 来示意吧。此时,再和 acquireQueued()分割起来,s 被唤醒后,进入 if (p == head && tryAcquire(arg))的判断(即便 p!=head 也没关系,它会再进入 shouldParkAfterFailedAcquire()寻找一个平安点。这里既然 s 曾经是期待队列中最前边的那个未放弃线程了,那么通过 shouldParkAfterFailedAcquire()的调整,s 也必然会跑到 head 的 next 结点,下一次自旋 p ==head 就成立啦),而后 s 把本人设置成 head 标杆结点,示意本人曾经获取到资源了,acquire()也返回了!

最初的小结:
release()是独占模式下线程开释共享资源的顶层入口。它会开释指定量的资源,如果彻底开释了(即 state=0), 它会唤醒期待队列里的其余线程来获取资源。其实 AQS 做的就是这些,以后这只是一个简略的概述,还有共享锁没有写进去,篇幅切实是太长了,有趣味的敌人能够本人去搜寻源代码或者博客查看相干信息。这篇博客也参考了另一位博主的文章,链接如下:https://www.cnblogs.com/water…

正文完
 0