AQS及其实现
一、AQS介绍
1、前置问题
1)同步队列,等待队列和条件队列的区别?
2)AQS和monitor区别?队列的对比?
3)类继承结构?
2、AQS的类注释
Doug Lea开发juc包的核心组件是AQS,是实现Semaphore,ReentrantLock,ReadWriteLock,CountDownLatch等的基础。
AQS提供了一个框架,可用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等)。对于大多数依赖单个原子整数值来表示状态的同步器来说,这个类是一个有用的基础。子类必须定义用来更改此状态的受保护方法,以及定义此状态对于获取或释放此对象意味着什么。考虑到这些,这个类中的其他方法执行所有排队和阻塞机制。子类可以维护其他状态,但是必须使用方法getState,setState,compareAndSetState来原子性的更新其值,来维护同步。
子类应该被定义为一个非公共的内部类,来实现外部类的同步属性。AbstractQueuedSynchronizer没有实现任何同步接口。相反,它定义了诸如acquiredinterruptibly之类的方法,这些方法可以由具体锁和相关同步器酌情调用,以实现它们的公共方法。
这个类支持默认的独占模式和共享模式。以独占模式获取时,其他线程尝试的获取无法成功。共享模式下多线程获取可能(非必须)成功。这个类只有通过“当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取”的机制才能区别两者。在不同模式下等待的线程共享同一个FIFO队列。通常,实现子类只支持其中一种模式,但这两种模式也可以都支持,例如在ReadWriteLock。仅支持独占模式或共享模式的子类不需要定义支持未使用模式的方法。
本类定义了ConditionObject内部类,它可以被支持独占模式的子类(AQS子类)用作条件(Condition)实现,对于这种模式,isheldexclusly报告是否以独占方式保持与当前线程的同步,使用当前getState值调用的方法release完全释放这个对象,并且acquire,给定这个保存的状态值,最终将这个对象恢复到它以前的获取状态。(本段待优化)
AQS类内没有方法会去创建条件(condition),如果无法满足此约束,请不要使用他。ConditionObject的行为当然取决于其同步器实现的语义。
这个类提供内部队列的检查、检测和监视方法,以及针对条件对象的类似方法。可以把AbstractQueuedSynchronizer导入需要的类来实现同步机制。
此类的序列化仅存储底层原子整数维护状态,因此反序列化对象具有空线程队列。需要序列化的典型子类将定义 readObject方法,该方法在反序列化时将其还原为已知的初始状态。
3、AQS$Node类注释
同步队列是CLH队列的变种。CLH常用于实现自旋锁。我们用它来实现阻塞的同步器,但是使用相同的策略来保持前驱节点内线程的一些控制信息。每个结点里的“status”字段用于跟踪线程是否应该阻塞。
当前驱结点release后,当前结点被唤醒。否则,队列里的每个结点都被作为特定通知类型的监视器,其内部的线程是唯一的等待线程。“status”字段不控制线程是否被授予锁等资源。当线程在队首时可以尝试去占有。但是在队首并不保证占有成功,只是由权利去竞争。所以当前被唤醒的竞争线程有可能因竞争失败而重新wait,等待下次唤醒。
要在CLH里入队一个结点,你需要原子性的把它插入队列并设为tail。出队时只需要设置head字段。
1 | +------+ prev +-----+ +-----+ |
由于插入CLH队列只需要“tail”保证原子性操作,所以在未入队到入队间有个简单的原子点界限。同样出队时只需要更新“head”。但是这期间需要多做一点工作来确定后继结点是谁,部分是为了处理由超时或中断导致的取消操作。
“prev”字段(原始的CLH中不会使用)主要用于取消场景。如果一个结点被取消,则他的后继结点会重新通过该字段连接至非取消状态的前驱结点。
我们使用“next”字段实现阻塞机制。每个结点的线程ID都被自己持有,所以前驱结点通过该字段来发送signal信号给next结点,以此来唤醒其持有的线程。决定next结点时必须避免与新入队的结点设置前驱结点的“next”字段的场景产生竞态条件。 This is solved when necessary by checking backwards from the atomically updated “tail” when a node’s successor appears to be null。
取消在基本算法中引入了保守性。因为我们必须轮询其他节点的取消,所以我们可能会忽略已取消的节点是在我们前面还是后面。常用处理的方法是一旦取消当前结点,则unpark其后继结点,使他们能找到新的前驱结点,除非我们能找到一个未取消的前驱结点来承担这一职责。
CLH队列需要一个dummy头结点。但是我们并没有一开始就创建他,因为如果不存在竞争时此举会产生多余的开销。反之,我们在第一次竞争时再创建头结点,并把head和tail指向他。
条件队列也使用此类,但是使用了额外的连接。条件只需要以简单的队列连接结点即可,因为他们仅在独占时访问。一旦await,结点便被插入条件队列。一旦signal,结点便被转移到主队列。“status”字段有个特殊的值来标识结点处于哪个队列。
4、AQS和monitor关系
AQS的同步队列相当于Object Monitor的EntrySet;AQS的条件队列和等待队列是一个东西,相当于Object Monitor的WaitSet。
参考:https://www.cnblogs.com/549294286/p/3688829.html
线程特征 | 队列特征 | 线程来源 | |
---|---|---|---|
同步队列 | 在同步队列中等待unpark信号去竞争锁 | 双向链表 | 1)条件队列;2)新的线程获取锁失败; |
条件队列 | 在条件队列中等待signal信号,然后转移至同步队列参与竞争 | 单向链表 | 获取锁的线程在该线程上下文对某个条件对象执行await方法 |
5、AQS的获取方法
6、AQS的acquire和acquireShared流程图
shouldParkAfterFailedAcquire方法流程:
检查当前结点的前驱结点的waiteStatus
1)如果是是SIGNAL,则返回true
2)如果是已取消,则在队列中剔除当前结点前连续的取消结点;返回false;
3)否则,设置前驱结点的waiteStatus为SIGNAL。返回false。
Note:
1)通过addWaiter增加的结点waitStatus=0; 会再第二次进入本方法时才返回true;
2)确保前驱结点为SIGNAL才阻塞当前线程。因为只有前驱结点的waiteStatus为SIGNAL时才会在前驱结点release或cancel时唤醒其后继结点,才不至于使线程一直下去 。
7、AQS的release方法流程图
8、Node类关键属性
1 | static final class Node { |
9、acquire源码
1 | /** |
10、AQS的acquireShared源码
1 | /** |
11、AQS的release方法源码
1 | /** |
二、使用AQS的实现
以下流程图是通过阅读JDK8源码得出。
1、ReentrantLock
ReentrantLock中state表示锁定次数,为0时表示未被锁定。默认使用非公平锁。
2、ReentrantReadWriteLock
state为int类型,高16位用于读锁,低16位用于写锁。默认非公平锁。
3、Semaphore
state表示许可数量。默认非公平锁。
4、CountDownLatch
state是初始化时传入的参数。