AQS简介
AQS,AbstractQueuedSynchronizer
,即队列同步器。它是构建锁或者其他同步组件的基础框架(如ReentrantLock
、ReentrantReadWriteLock
、Semaphore
等),JUC 并发包的作者( Doug Lea )期望它能够成为实现大部分同步需求的基础。它是 JUC 并发包中的核心基础组件。
AQS 解决了在实现同步器时涉及到的大量细节问题,例如获取同步状态、 FIFO 同步队列。基于 AQS 来构建同步器可以带来很多好处。它不仅能够极大地减少实现工作,而且也不必处理在多个位置上发生的竞争问题。
在基于 AQS 构建的同步器中,只能在一个时刻发生阻塞,从而降低上下文切换的开销,提高了吞吐量。同时在设计 AQS 时充分考虑了可伸缩性,因此 J.U.C 中所有基于 AQS 构建的同步器均可以获得这个优势。
AQS 的主要使用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态。
AQS 使用一个 int 类型的成员变量 state 来表示同步状态,当 state > 0 时表示已经获取了锁,当 state = 0 时表示释放了锁。它提供了三个方法( getState() 、setState(int newState) 、compareAndSetState(int expect,int update))来对同步状态 state 进行操作,当然 AQS 可以确保对 state 的操作是安全的。
AQS 通过内置的 FIFO 同步队列来完成资源获取线程的排队工作,如果当前线程获取同步状态(锁)失败时,AQS 则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,则会把节点中的线程唤醒,使其再次尝试获取同步状态。
AQS方法
getState()
:返回同步状态的当前值;setState(int newState)
:设置当前同步状态;compareAndSetState(int expect, int update)
:使用CAS设置当前状态,该方法能够保证状态设置的原子性;tryAcquire(int arg)
:独占式获取同步状态,获取同步状态成功后,其他线程需要等待该线程释放同步状态才能获取同步状态;tryRelease(int arg)
:独占式释放同步状态;tryAcquireShared(int arg)
:共享式获取同步状态,返回值大于等于0则表示获取成功,否则获取失败;tryReleaseShared(int arg)
:共享式释放同步状态;isHeldExclusively()
:当前同步器是否在独占式模式下被线程占用,一般该方法表示是否被当前线程所独占;acquire(int arg)
:独占式获取同步状态,如果当前线程获取同步状态成功,则由该方法返回,否则,将会进入同步队列等待,该方法将会调用可重写的tryAcquire(int arg)方法;acquireInterruptibly(int arg)
:与acquire(int arg)相同,但是该方法响应中断,当前线程为获取到同步状态而进入到同步队列中,如果当前线程被中断,则该方法会抛出InterruptedException异常并返回;tryAcquireNanos(int arg,long nanos)
:超时获取同步状态,如果当前线程在nanos时间内没有获取到同步状态,那么将会返回false,已经获取则返回true;acquireShared(int arg)
:共享式获取同步状态,如果当前线程未获取到同步状态,将会进入同步队列等待,与独占式的主要区别是在同一时刻可以有多个线程获取到同步状态;acquireSharedInterruptibly(int arg)
:共享式获取同步状态,响应中断;tryAcquireSharedNanos(int arg, long nanosTimeout)
:共享式获取同步状态,增加超时限制;release(int arg)
:独占式释放同步状态,该方法会在释放同步状态之后,将同步队列中第一个节点包含的线程唤醒;releaseShared(int arg)
:共享式释放同步状态;
CLH同步队列
CLH 同步队列是一个 FIFO 双向队列,AQS 依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS 则会将当前线程已经等待状态等信息构造成一个节点(Node)并将其加入到 CLH 同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒(公平锁),使其再次尝试获取同步状态。
在 CLH 同步队列中,一个节点表示一个线程,它保存着线程的引用(thread)、状态(waitStatus)、前驱节点(prev)、后继节点(next),其定义如下:
1 | static final class Node { |
CLH同步队列结构图如下:
入列
学了数据结构的我们,CLH 队列入列是再简单不过了,无非就是tail指向新节点、新节点的prev指向当前最后的节点,当前最后一个节点的next指向当前节点。代码我们可以看看addWaiter(Node node)方法:
1 | private Node addWaiter(Node mode) { |
addWaiter(Node node) 先通过快速尝试设置尾节点,如果失败,则调用 enq(Node node) 方法设置尾节点
1 | private Node enq(final Node node) { |
在上面代码中,两个方法都是通过一个 CAS 方法 compareAndSetTail(Node expect, Node update)
来设置尾节点,该方法可以确保节点是线程安全添加的。在 enq(Node node)
方法中,AQS 通过“死循环”的方式来保证节点可以正确添加,只有成功添加后,当前线程才会从该方法返回,否则会一直执行下去。
过程图如下:
出列
CLH 同步队列遵循 FIFO ,首节点的线程释放同步状态后,将会唤醒它的后继节点(next),而后继节点将会在获取同步状态成功时将自己设置为首节点,这个过程非常简单,head 执行该节点并断开原首节点的 next 和当前节点的 prev 即可,注意在这个过程是不需要使用 CAS 来保证的,因为只有一个线程能够成功获取到同步状态。过程图如下: