AbstractQueuedSynchronizer
2019-06-22 本文已影响0人
那谁319
AQS简介
什么是AQS(抽象队列同步器)
- 详细的可以看源码中的类注释
1、同步器是用来构建锁和其他同步组件的基础框架,它的实现主要依赖一个int
成员变量来表示同步状态以及通过一个FIFO队列构成等待队列。它的子类必须重
写AQS的几个protected修饰的用来改变同步状态的方法,其他方法主要是实现了
排队和阻塞机制。状态的更新使用getState,setState以及compareAndSetState这
三个方法。
2、子类被推荐定义为自定义同步组件的静态内部类,同步器自身没有实现任何
同步接口,它仅仅是定义了若干同步状态的获取和释放方法来供自定义同步组件
的使用,同步器既支持独占式获取同步状态,也可以支持共享式获取同步状态,
这样就可以方便的实现不同类型的同步组件。
acquire方法执行逻辑(独占锁的获取)

- 执行tryAcquire方法,如果tryAcquire方法返回结果为true,即尝试获取锁成功,则直接返回;
- 若失败,则先执行addWaiter方法,然后执行acquireQueued方法,最后执行selfInterrupt方法中断线程。
addWaiter方法执行逻辑(添加等待线程)

- 创建Node节点对象node,指定了当前线程和下一个等待节点(其实为null);
- 如果尾节点tail不为null(有具体的节点指向),把当前新添加对象的前驱节点(node.prev = pred)指向当前尾节点,cas操作更新尾节点由老的尾节点替换为新添加的节点,将原尾节点指向的节点的后继节点设置为指向当前新添加的节点。成功后返回。
- 如果尾节点tail为null(说明当前节点为第一个加入到等待列表中的线程节点)或者尾节点不为null时替换尾节点的cas操作(compareAndSetTail(pred, node))失败,则执行enq方法
enq方法执行逻辑

- 死循环执行,直到将尾节点的指向更新为当前节点。
- 获取老的尾节点,如果老的尾节点为null(说明当前节点为第一个加入到等待列表中的线程节点),首先通过cas操作将头节点指向创建的一个空节点对象,并且此时头节点和尾节点都指向在这个空对象。
- 如果老的尾节点tail不为null,把当前新添加对象的前驱节点(node.prev = pred)指向当前尾节点,cas操作更新尾节点由老的尾节点替换为新添加的节点,将原尾节点指向的节点的后继节点设置为指向当前新添加的节点。成功后返回。
以上完成了等待线程构建成Node节点对象,添加到等待链式列表的逻辑,但是这个等待线程怎样才能让自己获取独占锁呢?
acquireQueued方法执行逻辑

- 执行死循环,
- 获取当前节点的前驱节点(Node p = node.predecessor());
- 如果当前节点前驱节点是头节点,则当前节点是等待队列中第一个优先尝试获取锁的节点。
- 如果获取锁成功,重新设置头节点,将头节点指向设置为当前节点同时将当前节点对应的线程设置为null、将当前节点的前驱节点也设置为null;(当前节点在获取锁之后,转换成了头节点对象)
- 当前节点前驱节点的后继节点设置为null(即不指向当前节点,前驱节点实际是头节点,其前驱节点为null,此时其后继节点也为null,后续可以进行垃圾回收);
- 返回interrupted 中断状态。
- 如果当前节点前驱节点不是头节点,或者尝试获取锁失败,调用shouldParkAfterFailedAcquire方法,之后调用parkAndCheckInterrupt方法;
shouldParkAfterFailedAcquire方法执行逻辑

- 使用cas操作将节点的前驱节点的状态由初始值(0)设置成SIGNAL(-1),表示pred的后继节点即当前节点线程需要阻塞等待。
- shouldParkAfterFailedAcquire方法主要是将当前节点的前驱节点的等待状态设置为SIGNAL,才返回true(可以理解为当前节点想要获得锁,需要根据等待状态为SIGNAL的前驱节点的通知,来决定当前节点线程需要阻塞等待)
parkAndCheckInterrupt方法执行逻辑

- 该方法的关键是会调用LookSupport.park()方法用来阻塞当前线程。
- parkAndCheckInterrupt方法在shouldParkAfterFailedAcquire方法返回true即给出需要阻塞当前线程的判断后执行阻塞逻辑,方法执行到这整个线程阻塞。等待锁资源释放时唤醒下一个节点线程。从而让阻塞结束,程序继续往下执行。
所以,acquireQueued方法在自旋过程中主要完成了两件事情:
1、如果当前节点的前驱节点是头节点,并且当前线程能够获得锁,则该方法执行结束退出;
2、获取锁失败的话,先将当前节点的前驱节点状态设置成SIGNAL,然后调用LookSupport.park方法使得当前线程阻塞。
独占式锁的获取过程也就是acquire()方法的执行流程借用网上的一张图如下图所示:

release()方法执行逻辑(独占锁的释放)

- 执行tryRelease方法释放锁成功后,当头节点不为null,并且头节点的等待状态不等于0(实际就是小于0的那几个值)则执行unparkSuccessor方法。
unparkSuccessor方法执行逻辑

- 如果当前释放的节点的等待状态小于0,则需要通过cas操作设置成0;
- 获取当前释放节点的后继节点,如果后继节点不为null,并且后继节点的等待状态大于0(可以理解为该后继节点是被取消的线程,无需等待获取锁),则从尾节点开始遍历前驱节点,判断节点不为null,并且节点不是当前节点,当遍历的节点的等待状态小于等于0时,将在当前节点之后的第一个等待状态小于等于0的节点作为当前节点有效后继节点,并通过执行LockSupport.unpark(s.thread)唤醒有效的后继节点。
可中断式获取锁(acquireInterruptibly方法)

- 如果当前线程中断了,通过Thread.interrupted()返回中断状态,并重置中断状态。然后抛出异常。
- 尝试获取锁如果成功,直接返回;如果失败执行doAcquireInterruptibly方法。
doAcquireInterruptibly方法执行逻辑

- 与acquire方法逻辑几乎一致,唯一的区别是当parkAndCheckInterrupt返回true时(即线程阻塞被唤醒时,如果该线程被设置了中断状态),代码抛出被中断异常。
超时等待式获取锁(tryAcquireNanos()方法)

- 如果当前线程中断了,通过Thread.interrupted()返回中断状态,并重置中断状态。然后抛出异常。
- 尝试获取锁如果成功,直接返回;如果失败执行doAcquireNanos方法。
doAcquireNanos方法执行逻辑

- 和acquire方法区别在于如果过当前节点的前驱节点不是头节点,或者是头节点,但是当前线程获取锁失败,则计算超时等待的时间差,如果小于0说明超时了,直接返回false,获取锁超时失败;如果判断当前节点需要等待,则判断剩余超时时间是否大于阀值(1秒),如果大于则阻塞剩余超时时长,否则不阻塞当前线程,死循环消耗掉剩余时长。
共享锁的获取(acquireShared()方法)

- 调用tryAcquireShared方法,tryAcquireShared返回值是一个int类型,当返回值为大于等于0的时候方法结束说明获得成功获取锁,否则,表明获取同步状态失败即所引用的线程获取锁失败,会执行doAcquireShared方法。
doAcquireShared方法执行逻辑
