AQS源码分析

151 阅读6分钟

1.AQS

1.1AQS是什么?

1.1.1概述

AQS是构建锁、其他同步组件的基础框架。

image.png

以上这些锁或者是同步组件都是基于AQS构建的。AQS就是各种锁、各种同步组件实现的公共基础部分的抽象实现。

1.1.2AQS能干什么?

  1. 同步队列的管理和维护
  2. 同步状态的管理
  3. 线程的阻塞、唤醒线程

1.1.3独占锁和共享锁

1)独占锁

也叫作互斥锁、排它锁。这个锁在同一时间只能有一个线程持有,其他线程想要持有这把锁,必须要等待当前线程释放锁。

2)共享锁

同一时间内可以有多个线程获取到同一把锁。如读写锁里面的“读锁”。

1.2基本设计思路

①把来竞争的线程以及其等待状态封装成一个Node对象

②把这些Node,放到一个同步队列中去,这个同步队列是一个FIFO(先进先出)的一个双向队列,是基于CLH队列来实现的。

image.png

③使用一个int类型的volatile state 变量来表示同步状态。

image.png

  • 是否有线程获得锁
  • 锁的重入次数
  • 具体的含义有具体的子类自己定义

④线程的唤醒和阻塞:伴随着同步队列的维护。使用LockSupport来实现对线程的唤醒和阻塞。

1.2.1如何把AQS的基础功能提供出去?

AQS使用模板方法模式。AQS只是提供模板,子类继承AQS,在AQS基础上重写AQS的一些方法,来实现自己想要的锁或者同步器。

1)那些方法是我们可以重写的?

image.png

image.png

这些方法在AQS中都没有实现,只是抛出了一个异常,具体细节需要子类来实现。

image.png

1.4 AQS的源码分析

1.4.1初步认知AQS类以及其父类

1)父类

image.png

AQS类的父类只有一个属性exclusiveOwnerThread保存的是当前独占锁的线程。还有这个属性的get和set方法。

1.4.2主要的属性

1)head

image.png 双向链表的头结点。

2)tail

image.png 双向链表的尾结点。

3)state

image.png

表示锁状态(资源状态的)state值。这个属性还有get和set方法。还有一个CAS修改state值的CAS方法。

这三个操作都是原子操作,这个CAS方法调用的是Unsafe类中的方法。

(1)还有一些找到AQS和Node中属性的偏移量的方法:

image.png

1.4.3CAS方法

1)有修改AQS和Node中属性的CAS方法:

image.png

1.4.4 AQS同步队列的数据结构:Node

1)Node中的属性

image.png

2)方法

image.png

1.4.5 实现非阻塞的获取和释放独占锁的源码分析

获取独占锁的大致流程:

image.png

1)同步化对列的构建与获取锁(假设加入的线程都获取不到锁,锁被占用了)

①首先,线程调用acqiure方法准备获取独占式锁,如果获取锁成功(tryAcqiure(arg)返回为true),这个方法执行结束,线程继续执行。

image.png

②如果tryAquire(arg)返回为false,尝试获取锁失败。那么调用addWaiter()方法将这个线程加入到等待队列中。在addWaiter()中,先创建一个Node结点,将这个线程封装在Node结点的thread属性中。然后再把这个结点加到双向链表的末尾。addWait()有一个参数,表示的是这个线程要获取的锁是什么类型的,独占锁/共享锁。如果等待队列还没有创建或者第一次加入失败,进入到enq方法中创建或者通过自旋的CAS操作将结点加入到链表中。

image.png

③如果没有创建链表,创建一个初始结点,让头尾结点都指向它,并且这个结点里面没有封装线程。如果链表队列已经存在,把结点加入到链表末尾,采用了CAS的自旋方式,直到加入队列成功才会退出循环。

image.png

④在addWaiter()方法结束后,返回刚刚插入的结点给acquireQueued()方法作为参数,另一个参数arg是改变后state的值,一般是需要获取的资源的个数。

image.png

⑤进入到acquireQueued()方法,通过自旋的方式获取锁,先得到这个结点的前驱结点,再判断这个前驱结点是不是头结点,如果是的话,第二次尝试获取锁资源(tryAcquire)。如果还是获取不到锁资源,执行后面的shouldParkAfterFailedAcquire()方法。

image.png

⑥进入到shouldParkAfterFailedAcquire()判断当前线程是否需要在获取资源失败后挂起。在这个方法里面获取到前驱结点的waitStatus值。再判断waitStatus的值,根据不同的值进行不同的操作:

  1. 如果前驱结点的waitStatus==SIGNAL,那么直接返回true。
  2. 如果前驱结点的waitStatus>0,表示这个前驱结点封装的线程已经被取消了,把这个前驱结点在链表中删除,并且循环这个操作,直到找到一个waitStatus<=0的结点就停止,最后返回false。
  3. 如果是其他情况,将前驱结点的waitStatus设置为SIGNAL,最后返回false。

结点进入队列时,都会使用CAS它的前一个结点的waitStatus设置为SIGNAL,SIGNAL的值是-1,用final修饰的,无法修改。最后一个结点没有后继节点,所以它的waitStatus值为0

image.png

⑦如果返回了false,因为还是在通过自旋获取锁,所以还会执行一次循环,第三次调用trAcquire()方法尝试获取锁,如果还是没有获取到,继续执行shouldParkAfterFailedAcquire()方法,因为上次循环已经将waitStatus的值设置为了SIGNAL,所以会直接返回true,调用parkAndCheckInterrupt()方法将当前线程挂起。所以,线程在刚刚开始来的时候尝试获取一次锁,后面又通过自旋尝试获取两次锁,如果还没有获取到锁,就将当前线程挂起。所以,当线程进入到同步队列中时,是进入到挂起状态的,除了头结点所持有的线程。(不持有线程的头结点除外)

2)同步队列的释放锁

①如果有占用锁的线程释放锁,那么就会调用release()方法。先获取到头结点,如果头结点不为null并且它的等待状态waitStatus != 0,表示这还不是链表的最后一个结点,只有链表的最后一个结点waitStatus == 0,其余结点waitStatus==SINGAL。那么就需要唤醒下一个结点,使他去争夺cpu资源。

image.png

②在进入到unparkSuccessor()方法后,如果waitStatus<0,就通过CAS操作把这个值变为0。表示自己将要唤醒自己的下一个结点的线程。通过头结点得到下一个结点

  1. 如果发现下一个结点为null或者下一个结点封装的线程已经被取消,那么就从尾结点开始找,找到头结点后面第一个waitStatus!=CANCLLED的结点,并且唤醒它。
  2. 如果下一个结点不为null,那么就用LockSupport.unpark()方法唤醒它。

image.png

③一个线程被唤醒,从他被挂起的那个地方重新开始运行。

image.png

④会有两次机会尝试获取锁tryAcquire(),第一次没有获得锁把这个结点的前驱结点的waitStatus赋值为SIGNAL,第二次直接挂起。

  • 如果获取到了锁,会把当前的这个结点设置为头结点,原来的头结点出队

image.png

这个结点也不是一定会获取锁的,如果此时正好来了一个线程,这个线程先尝试获取锁,获取到了,就不会进入到同步队列。那么这个刚刚被唤醒的线程又会阻塞。

3)运行期间产生异常或者终止的情况

(1)如果发生了异常或其他原因导致自旋终止

①如果抛出异常或者因为某种原因退出自旋,就会取消获取锁。在finally语句块中,调用了cancel方法。

image.png ②如果当前结点不为null,将结点里面的thread属性置为null。判断前面结点是不是已经线程取消的节点如果是,将他们出队,直到找到一个没有取消线程的结点为止,通过双向链表的删除操作完成。将当前结点waitStatus=CANCELLED,最后将当前结点也出队。

③如果它的前驱结点是head结点,(head结点一般是正在持有锁的结点。刚刚构建队列时的head结点除外,这个head结点没有封装线程。)那么要重新唤醒结点,尝试获取锁。unparkSuccessor(node)

image-20220318143208408.png

(2)如果一个线程被中断

①如果一个线程被唤醒后,检测自己的中断状态

  1. 如果为true,并且重新获取了锁,将就自己中断,具体的中断由程序员来实现,AQS只是提供一个模板。

image.png

image.png

1.4.6可中断式获取释放锁的源码分析

1)获取锁

  • 流程与普通的获取独占锁的流程基本相似,不过在开始获取时会检查线程的中断状态,如果发生了中断,就抛出异常。

image.png

image.png

  • 以及在线程挂起被唤醒后,检查线程的中断标志,如果为true,也需要抛出异常,最后执行finally语句块的内容,把线程从队列中删除。而普通的获取锁仅仅是记录线程的中断状态

2)释放锁

释放锁的流程和普通释放锁的流程一模一样,调用一样的方法。所有独占锁调用同一个释放方法。

1.4.7超时获取和释放独占锁

  • 超时获取独占锁也包括了可中断的获取独占锁。

image.png

image.png

  • 先根据允许执行的时间+当前时间获取一个截止日期,每次在获取锁失败后都会判断剩下的时间是否大于0,如果小于0,直接返回false,获取锁失败。
  • 如果时间还大于0,判断是否挂起以及剩余时间是否还大于自旋最大允许的时间(spinForTimeoutThreshold)。如果大于,将线程挂起,不会一直让线程自旋,占用cpu资源。否则,再尝试获取锁。

释放锁一样

1.4.8获取和释放共享锁的源码分析

1)释放锁的源分析

①如果有一个线程释放共享锁,那么会调用tryReleaseShare(),这个方法是需要被子类重写覆盖的方法。

  • 如果这个方法返回true,表示允许唤醒下一个线程。
  • 否则返回false,表示释放线程失败。

image.png

②进入doReleaseShared()方法。

  1. 获取到头结点,如果不为null并且不等于尾结点。

    • 并且waitStatus == SIGNAL,就将head结点的waitStatus赋值为0,表示将后继节点唤醒只要一个结点的后继节点被唤醒,这个结点的waitStatus就被设置为0,作为一个标识。 )。这个操作是一个自旋的CAS操作,直到成功才会退出。然后调用unparkSuccessor()方法唤醒后面的线程。
    • 如果头结点的waitStatus==0,那么将waitStatus=PROPAGATE。这个操作是一个自旋的CAS操作,直到成功呢才会退出。PROPAGATE表示还可以有线程可以获取共享锁,这个线程应该唤醒后面的线程。

image.png

②unparkSuccessor():唤醒线程的方法,独占锁也是使用这个方法唤醒线程。

2)获取共享锁以及维护

tryAquireShared是由子类来实现的。返回值表示还可以获取锁的线程个数。如果这个值大于0,表示尝试获取锁成功,并且还有线程可以获得锁;如果等于0,也获取成功,当时没有线程可以获取锁了;如果小于0,获取锁失败。

image.png

②获取失败会将线程加入到等待队列中。如果前驱结点是head结点,尝试获取锁。如果获取成功(r >=0),就设置这个结点为头结点。(在独占锁中,头结点持有锁,一直到释放锁后有线程抢到锁,抢到锁的线程的结点成为新的头结点,原来的头结点出队;在共享锁中,头结点即使出队了可能还是会持有锁;它们相同的地方是:新获取锁的结点都变成的头结点,原来的头结点出队。即在独占式的AQS中,一个时间点只能有一个线程持有锁;而共享式的AQS,同一时间点可以有多个线程持有锁。)

image.png

③如果剩余的资源数目大于0,即还可以有线程获得锁,并且是共享模式,那么就唤醒下一个线程。(doReleaseShared(),释放锁如果允许唤醒线程,调用的也是这个方法)

在这里面s==null可能是一个线程维护过程中的中间状态,并不是真正后面就没有结点。

image.png

④线程唤醒后又会尝试获取锁,如果获取到锁,又会将当前线程结点设置为头结点并且如果tryAcquireShared()方法返回值大于0,又会唤醒一个线程,再次尝试获取锁,直到方法返回值不再大于0,即没有锁可以获取为止。共享模式下就通过这种传播的方式唤醒线程。

1.4.9获取和释放可中断共享锁的源码分析

1)获取锁

和独占锁类似,就是获取线程的中断状态,如果是true,就抛出异常,然后执行finally语句块。不是中断的锁通常不处理。

1.4.10获取和释放超时共享锁的源码分析

超时获取锁包括可中断的获取锁。而且和独占锁的超时锁差不多。

1.4.11总结

①正常情况下,独占锁只有持有的线程运行结束了,才释放锁,独占锁的结点才会出队;共享锁是唤醒了下一个结点,如果下一个结点拿到锁,那么下一个结点成为新的头结点,当前结点出队,不需要释放锁。

②独占锁只有在释放锁的时候,才会去看要不要唤醒下一个结点;共享锁在释放锁的时候唤醒下一个结点。也在获得锁后尝试唤醒下一个结点setHeadAndPropagate()方法。