1.AQS
1.1AQS是什么?
1.1.1概述
AQS是构建锁、其他同步组件的基础框架。
以上这些锁或者是同步组件都是基于AQS构建的。AQS就是各种锁、各种同步组件实现的公共基础部分的抽象实现。
1.1.2AQS能干什么?
- 同步队列的管理和维护
- 同步状态的管理
- 线程的阻塞、唤醒线程
1.1.3独占锁和共享锁
1)独占锁
也叫作互斥锁、排它锁。这个锁在同一时间只能有一个线程持有,其他线程想要持有这把锁,必须要等待当前线程释放锁。
2)共享锁
同一时间内可以有多个线程获取到同一把锁。如读写锁里面的“读锁”。
1.2基本设计思路
①把来竞争的线程以及其等待状态封装成一个Node对象
②把这些Node,放到一个同步队列中去,这个同步队列是一个FIFO(先进先出)的一个双向队列,是基于CLH队列来实现的。
③使用一个int类型的volatile state 变量来表示同步状态。
- 是否有线程获得锁
- 锁的重入次数
- 具体的含义有具体的子类自己定义
④线程的唤醒和阻塞:伴随着同步队列的维护。使用LockSupport来实现对线程的唤醒和阻塞。
1.2.1如何把AQS的基础功能提供出去?
AQS使用模板方法模式。AQS只是提供模板,子类继承AQS,在AQS基础上重写AQS的一些方法,来实现自己想要的锁或者同步器。
1)那些方法是我们可以重写的?
这些方法在AQS中都没有实现,只是抛出了一个异常,具体细节需要子类来实现。
1.4 AQS的源码分析
1.4.1初步认知AQS类以及其父类
1)父类
AQS类的父类只有一个属性exclusiveOwnerThread,保存的是当前独占锁的线程。还有这个属性的get和set方法。
1.4.2主要的属性
1)head
双向链表的头结点。
2)tail
双向链表的尾结点。
3)state
表示锁状态(资源状态的)state值。这个属性还有get和set方法。还有一个CAS修改state值的CAS方法。
这三个操作都是原子操作,这个CAS方法调用的是Unsafe类中的方法。
(1)还有一些找到AQS和Node中属性的偏移量的方法:
1.4.3CAS方法
1)有修改AQS和Node中属性的CAS方法:
1.4.4 AQS同步队列的数据结构:Node
1)Node中的属性
2)方法
1.4.5 实现非阻塞的获取和释放独占锁的源码分析
获取独占锁的大致流程:
1)同步化对列的构建与获取锁(假设加入的线程都获取不到锁,锁被占用了)
①首先,线程调用acqiure方法准备获取独占式锁,如果获取锁成功(tryAcqiure(arg)返回为true),这个方法执行结束,线程继续执行。
②如果tryAquire(arg)返回为false,尝试获取锁失败。那么调用addWaiter()方法将这个线程加入到等待队列中。在addWaiter()中,先创建一个Node结点,将这个线程封装在Node结点的thread属性中。然后再把这个结点加到双向链表的末尾。addWait()有一个参数,表示的是这个线程要获取的锁是什么类型的,独占锁/共享锁。如果等待队列还没有创建或者第一次加入失败,进入到enq方法中创建或者通过自旋的CAS操作将结点加入到链表中。
③如果没有创建链表,创建一个初始结点,让头尾结点都指向它,并且这个结点里面没有封装线程。如果链表队列已经存在,把结点加入到链表末尾,采用了CAS的自旋方式,直到加入队列成功才会退出循环。
④在addWaiter()方法结束后,返回刚刚插入的结点给acquireQueued()方法作为参数,另一个参数arg是改变后state的值,一般是需要获取的资源的个数。
⑤进入到acquireQueued()方法,通过自旋的方式获取锁,先得到这个结点的前驱结点,再判断这个前驱结点是不是头结点,如果是的话,第二次尝试获取锁资源(tryAcquire)。如果还是获取不到锁资源,执行后面的shouldParkAfterFailedAcquire()方法。
⑥进入到shouldParkAfterFailedAcquire()判断当前线程是否需要在获取资源失败后挂起。在这个方法里面获取到前驱结点的waitStatus值。再判断waitStatus的值,根据不同的值进行不同的操作:
- 如果前驱结点的
waitStatus==SIGNAL,那么直接返回true。 - 如果前驱结点的
waitStatus>0,表示这个前驱结点封装的线程已经被取消了,把这个前驱结点在链表中删除,并且循环这个操作,直到找到一个waitStatus<=0的结点就停止,最后返回false。 - 如果是其他情况,将前驱结点的waitStatus设置为SIGNAL,最后返回false。
结点进入队列时,都会使用CAS它的前一个结点的waitStatus设置为SIGNAL,SIGNAL的值是-1,用final修饰的,无法修改。最后一个结点没有后继节点,所以它的waitStatus值为0
⑦如果返回了false,因为还是在通过自旋获取锁,所以还会执行一次循环,第三次调用trAcquire()方法尝试获取锁,如果还是没有获取到,继续执行shouldParkAfterFailedAcquire()方法,因为上次循环已经将waitStatus的值设置为了SIGNAL,所以会直接返回true,调用parkAndCheckInterrupt()方法将当前线程挂起。所以,线程在刚刚开始来的时候尝试获取一次锁,后面又通过自旋尝试获取两次锁,如果还没有获取到锁,就将当前线程挂起。所以,当线程进入到同步队列中时,是进入到挂起状态的,除了头结点所持有的线程。(不持有线程的头结点除外)
2)同步队列的释放锁
①如果有占用锁的线程释放锁,那么就会调用release()方法。先获取到头结点,如果头结点不为null并且它的等待状态waitStatus != 0,表示这还不是链表的最后一个结点,只有链表的最后一个结点waitStatus == 0,其余结点waitStatus==SINGAL。那么就需要唤醒下一个结点,使他去争夺cpu资源。
②在进入到unparkSuccessor()方法后,如果waitStatus<0,就通过CAS操作把这个值变为0。表示自己将要唤醒自己的下一个结点的线程。通过头结点得到下一个结点
- 如果发现下一个结点为null或者下一个结点封装的线程已经被取消,那么就从尾结点开始找,找到头结点后面第一个
waitStatus!=CANCLLED的结点,并且唤醒它。 - 如果下一个结点不为null,那么就用
LockSupport.unpark()方法唤醒它。
③一个线程被唤醒,从他被挂起的那个地方重新开始运行。
④会有两次机会尝试获取锁tryAcquire(),第一次没有获得锁把这个结点的前驱结点的waitStatus赋值为SIGNAL,第二次直接挂起。
- 如果获取到了锁,会把当前的这个结点设置为头结点,原来的头结点出队
⑤这个结点也不是一定会获取锁的,如果此时正好来了一个线程,这个线程先尝试获取锁,获取到了,就不会进入到同步队列。那么这个刚刚被唤醒的线程又会阻塞。
3)运行期间产生异常或者终止的情况
(1)如果发生了异常或其他原因导致自旋终止
①如果抛出异常或者因为某种原因退出自旋,就会取消获取锁。在finally语句块中,调用了cancel方法。
②如果当前结点不为null,将结点里面的thread属性置为null。判断前面结点是不是已经线程取消的节点,如果是,将他们出队,直到找到一个没有取消线程的结点为止,通过双向链表的删除操作完成。将当前结点
waitStatus=CANCELLED,最后将当前结点也出队。
③如果它的前驱结点是head结点,(head结点一般是正在持有锁的结点。刚刚构建队列时的head结点除外,这个head结点没有封装线程。)那么要重新唤醒结点,尝试获取锁。unparkSuccessor(node)。
(2)如果一个线程被中断
①如果一个线程被唤醒后,检测自己的中断状态
- 如果为true,并且重新获取了锁,将就自己中断,具体的中断由程序员来实现,AQS只是提供一个模板。
1.4.6可中断式获取释放锁的源码分析
1)获取锁
- 流程与普通的获取独占锁的流程基本相似,不过在开始获取时会检查线程的中断状态,如果发生了中断,就抛出异常。
- 以及在线程挂起被唤醒后,检查线程的中断标志,如果为true,也需要抛出异常,最后执行finally语句块的内容,把线程从队列中删除。而普通的获取锁仅仅是记录线程的中断状态。
2)释放锁
释放锁的流程和普通释放锁的流程一模一样,调用一样的方法。所有独占锁调用同一个释放方法。
1.4.7超时获取和释放独占锁
- 超时获取独占锁也包括了可中断的获取独占锁。
- 先根据允许执行的时间+当前时间获取一个截止日期,每次在获取锁失败后都会判断剩下的时间是否大于0,如果小于0,直接返回false,获取锁失败。
- 如果时间还大于0,判断是否挂起以及剩余时间是否还大于自旋最大允许的时间(spinForTimeoutThreshold)。如果大于,将线程挂起,不会一直让线程自旋,占用cpu资源。否则,再尝试获取锁。
释放锁一样。
1.4.8获取和释放共享锁的源码分析
1)释放锁的源分析
①如果有一个线程释放共享锁,那么会调用tryReleaseShare(),这个方法是需要被子类重写覆盖的方法。
- 如果这个方法返回true,表示允许唤醒下一个线程。
- 否则返回false,表示释放线程失败。
②进入doReleaseShared()方法。
-
获取到头结点,如果不为null并且不等于尾结点。
- 并且
waitStatus == SIGNAL,就将head结点的waitStatus赋值为0,表示将后继节点唤醒(只要一个结点的后继节点被唤醒,这个结点的waitStatus就被设置为0,作为一个标识。 )。这个操作是一个自旋的CAS操作,直到成功才会退出。然后调用unparkSuccessor()方法唤醒后面的线程。 - 如果头结点的
waitStatus==0,那么将waitStatus=PROPAGATE。这个操作是一个自旋的CAS操作,直到成功呢才会退出。PROPAGATE表示还可以有线程可以获取共享锁,这个线程应该唤醒后面的线程。
- 并且
②unparkSuccessor():唤醒线程的方法,独占锁也是使用这个方法唤醒线程。
2)获取共享锁以及维护
①tryAquireShared是由子类来实现的。返回值表示还可以获取锁的线程个数。如果这个值大于0,表示尝试获取锁成功,并且还有线程可以获得锁;如果等于0,也获取成功,当时没有线程可以获取锁了;如果小于0,获取锁失败。
②获取失败会将线程加入到等待队列中。如果前驱结点是head结点,尝试获取锁。如果获取成功(r >=0),就设置这个结点为头结点。(在独占锁中,头结点持有锁,一直到释放锁后有线程抢到锁,抢到锁的线程的结点成为新的头结点,原来的头结点出队;在共享锁中,头结点即使出队了可能还是会持有锁;它们相同的地方是:新获取锁的结点都变成的头结点,原来的头结点出队。即在独占式的AQS中,一个时间点只能有一个线程持有锁;而共享式的AQS,同一时间点可以有多个线程持有锁。)
③如果剩余的资源数目大于0,即还可以有线程获得锁,并且是共享模式,那么就唤醒下一个线程。(doReleaseShared(),释放锁如果允许唤醒线程,调用的也是这个方法)
在这里面s==null可能是一个线程维护过程中的中间状态,并不是真正后面就没有结点。
④线程唤醒后又会尝试获取锁,如果获取到锁,又会将当前线程结点设置为头结点并且如果tryAcquireShared()方法返回值大于0,又会唤醒一个线程,再次尝试获取锁,直到方法返回值不再大于0,即没有锁可以获取为止。共享模式下就通过这种传播的方式唤醒线程。
1.4.9获取和释放可中断共享锁的源码分析
1)获取锁
和独占锁类似,就是获取线程的中断状态,如果是true,就抛出异常,然后执行finally语句块。不是中断的锁通常不处理。
1.4.10获取和释放超时共享锁的源码分析
超时获取锁包括可中断的获取锁。而且和独占锁的超时锁差不多。
1.4.11总结
①正常情况下,独占锁只有持有的线程运行结束了,才释放锁,独占锁的结点才会出队;共享锁是唤醒了下一个结点,如果下一个结点拿到锁,那么下一个结点成为新的头结点,当前结点出队,不需要释放锁。
②独占锁只有在释放锁的时候,才会去看要不要唤醒下一个结点;共享锁在释放锁的时候唤醒下一个结点。也在获得锁后尝试唤醒下一个结点setHeadAndPropagate()方法。