队列同步器(AQS)简介:
AbstractQueueSynchronizer,用来构建锁和其他同步组件的基础框架,使用一个int型变量来表示同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。
我们可以这么理解,锁是面向使用者的,即我们可以用锁来完成多线程处理的一些问题,而隐藏了实现的细节,而同步器面向锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理,线程派对,等待与唤醒等底层的操作。相当于使用者使用锁,而AQS来实现锁。
使用场景:比如可重入锁,CountDownLatch等锁和工具类都用到了AQS
使用方式:
子类继承并实现他的抽象方法来管理同步状态,对同步状态进行修改时,使用同步器提供的3个方法(getState()、setState()、compareAndSetState())来操作,他们能保证状态的改变是安全的。子类推荐为自定义同步组件的静态内部类。
刚才提到的三个方法getState()、setState()、compareAndSetState()都是final方法,我们并不需要去重写,需要重写的方法是下面的这几个方法:
当我们自定义同步组件时,将会调用同步器提供的模板方法,这些模板方法如下:
这些模板方法同样是final的,我们在调用他们时,也会调用到我们之前重写的方法,在后面我们会介绍到这些模板方法。
下面给出一个例子,大概了解一下怎么使用(静态内部类继承AQS)
class Mutex implements Lock {
// 静态内部类, 自定义同步器
private static class Sync extends AbstractQueuedSynchronizer {
// 是否处于占用状态
protected boolean isHeldExclusively() {
return getState() == 1;
}
// 当状态为0的时候获取锁
public boolean tryAcquire(int acquires) {
if (compareAndSetState(0, 1) ) {
setExclusiveOwnerThread(Thread. currentThread() ) ;
return true;
}
return false;
}
// 释放锁, 将状态设置为0
protected boolean tryRelease(int releases) {
if (getState() == 0) throw new
IllegalMonitorStateException() ;
setExclusiveOwnerThread(null) ;
setState(0) ;
return true;
}
// 返回一个Condition, 每个condition都包含了一个condition队列
Condition newCondition() { return new ConditionObject() ; }
}
// 仅需要将操作代理到Sync上即可
private final Sync sync = new Sync() ;
public void lock() { sync. acquire(1) ; }
public boolean tryLock() { return sync.tryAcquire(1) ; }
public void unlock() { sync. release(1) ; }
public Condition newCondition() { return sync.newCondition() ; }
public boolean isLocked() { return sync. isHeldExclusively() ; }
public boolean hasQueuedThreads() { return sync.hasQueuedThreads() ; }
public void lockInterruptibly() throws InterruptedException {
sync. acquireInterruptibly(1) ;
}
public boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException {
return sync. tryAcquireNanos(1, unit.toNanos(timeout) ) ;
}
}
同步队列:
AQS中很重要的一个数据结构就是同步队列了,他是一个FIFO双向的同步队列。
当前的线程在获取同步状态失败时,同步器会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点中的线程唤醒,使其再次尝试获取同步状态。
Node节点是AQS中的一个内部类,成员如下
static final class Node {
/** Marker to indicate a node is waiting in shared mode */
static final Node SHARED = new Node();
/** Marker to indicate a node is waiting in exclusive mode */
static final Node EXCLUSIVE = null;
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
/**
* waitStatus value to indicate the next acquireShared should
* unconditionally propagate
*/
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
/**
* Returns true if node is waiting in shared mode.
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition
this.waitStatus = waitStatus;
this.thread = thread;
}
}
而每个同步器中都会持有同步队列的首节点和尾节点
private transient volatile Node head;
private transient volatile Node tail;
所以基本结构如下:
刚才说了获取同步状态失败时,就会把线程信息加入一个新构建的节点,然后接入队列的尾部,所以这个加入队列的过程也必须要保证线程的安全,所以同步器有一个基于CAS的设置尾节点的方法:
compareAndSetTail(Node except,Node update)
首节点是获取同步状态成功的节点,首节点的线程在释放同步状态时,就会唤醒后续的节点,而后续的节点将会在获取同步状态成功时将自己设置为首节点。设置首节点是由获取同步状态成功的线程来完成的,由于只有一个线程能够成功的获取到同步状态,因此就不需要CAS来保证线程安全了(就只有一个线程)。
同步状态的获取与释放:
在了解了同步队列的结构后,我们就可以来看看AQS到底是怎样来进行同步状态的获取和释放的。
获取与释放分为独占式的和共享式的。
独占式:顾名思义就是同一时刻只能有一个线程获取到锁,其他获取锁线程只能处于同步队列中等待,只有获取锁的线程释放了锁,后继的线程才能够获取到锁。
共享式:同一时刻能够有多个线程获取到同步状态。
(1)独占式同步状态的获取:
通过acquire(int arg)方法获取同步状态,该方法对于中断不敏感,也就是由于线程获取同步状态失败后进入同步队列中, 后续对线程进行中断操作时, 线程不会从同步队列中移出。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这个方法中调用了重写之后tryAcquire(int arg)方法,还调用了addWaiter方法和acquireQueued方法
下面我们一个一个来分析一下这些方法
首先是addWaiter(Node node)方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure——尝试快速加入尾节点
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
这个方法会使用compareAndSetTail()方法来吧当前线程加入尾节点,如果没有加入成功,就会去调用enq()方法,那我们再看看enq(final Node node)方法
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
可以看出,enq方法就是一个死循环,而它所完成的工作和addWaiter(Node node)是一样的,都是吧当前的线程加入到同步队列的尾节点处,所以enq方法和addWaiter可以看做是同一个方法来对待,通过CAS设置尾节点的方式,将并发添加节点的请求变得串行化了,也就是保证了尾节点添加是线程安全的。在执行完这两个方法之后,就会去调用acquireQueued方法,我们再看看这个方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
同理,这还是一个死循环的方法,它的逻辑是判断前驱节点是不是首结点(这个时候通过之前说的方法当前线程已经成功的加入了同步队列),假如是首节点(只有他的前驱是首节点他才有机会获取同步状态)那就尝试获取同步状态,获取成功的话就把自己设置为首节点,否则就继续循环直到他获取成功为止。
仔细看看这个方法是个无限循环,感觉如果p == head && tryAcquire(arg)条件不满足循环将永远无法结束,当然不会出现死循环,奥秘在于后面的parkAndCheckInterrupt会把当前线程挂起,从而阻塞住线程的调用栈。当然也不是马上把请求不到锁的线程进行阻塞,还要检查该线程的状态,比如如果该线程处于Cancel状态则没有必要,具体的检查在shouldParkAfterFailedAcquire中:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
检查原则在于:
- 规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞
- 规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞
- 规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同
那么我们现在总结一下,介绍了刚才那么多的方法,大多数的方法里面都有一个死循环,这时回到最初的acquire(int arg)方法,我们就大概能够明白这个方法的逻辑了,当获取失败时,那么后面的方法就会把当前线程加入同步队列,并且让该节点一直自旋(相当于被阻塞了),直到他获取到了同步状态成功(别的线程释放了)为止。
那么现在还有一个疑问,假如第一个获取的线程在tryAcquire方法中就获取同步状态成功,直接返回ture,那么线程就不会被包装成节点加入到同步队列中,那队列何来的首节点和尾节点?
其实我们回去看addWaiter和enq方法就会发现这些方法都会去判断是否有尾节点,当发现没有尾节点时,就会在enq方法里就会创建首节点和尾节点(首节点就是尾节点)
(代码片段)
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
}
说了这么多,对于锁这种并发组件来说的话,从acquire(int arg)这个方法返回就代表当前线程获取了锁,以上就是这个方法的含义。
获取的逻辑图:
(2)独占式同步状态的释放
刚才讲的是获取,现在讲的是如何释放同步状态。通过调用AQS的release(int arg)方法即可,他会唤醒首节点的后续节点。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ps:唤醒的方法unparkSuccessor(Node node)使用了LockSupport工具类
(3)共享式同步状态的获取与释放:
以文件的读写为例,写操作要求对资源的独占式访问,而读操作可以是共享式访问。
一个线程在读时,其他线程也可以读,但是一个线程在写时,其他线程均不能读写。
调用方法acquireShare(int arg)可以共享式获取同步状态,
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
当tryAcquireShared返回值大于等于0时,表示可以获取到同步状态,否则进入doAcquireShared(int arg)方法中自旋
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
同样是一个死循环,不断的进行tryAcquireShare(int arg)方法,直到获取成功,其实它和独占式的差别不大,差别主要在setHeadAndPropagate方法,顾名思义,即在设置head之后多执行了一步propagate操作
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
这意味着独占锁某个节点被唤醒之后,它只需要将这个节点设置成head就完事了,而共享锁不一样,某个节点被设置为head之后,如果它的后继节点是SHARED状态的,那么将继续通过doReleaseShared方法尝试往后唤醒节点,实现了共享状态的向后传播。
释放同步状态使用releaseShare(int arg)方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
该方法在释放同步状态之后,将会唤醒后续处于等待状态的节点。对于能够支持多个线程同时访问的并发组件(比如Semaphore),它和独占式主要区别在于tryReleaseShared(int arg)方法必须确保同步状态(或者资源数)线程安全释放,一般是通过循环和CAS来保证的, 因为释放同步状态的操作会同时来自多个线程。
(4)独占式超时获取同步状态
先介绍一下响应中断的同步状态获取过程。在Java 5之前,当一个线程获取不到锁而被阻塞在synchronized之外时,对该线程进行中断操作,此时该线程的中断标志位会被修改, 但线程依旧会阻塞在synchronized上,等待着获取锁。在Java 5中,同步器提供了acquireInterruptibly(int arg)方法, 这个方法在等待获取同步状态时, 如果当前线程被中断, 会立刻返回, 并抛出InterruptedException。超时获取同步状态过程可以被视作响应中断获取同步状态过程的“增强版”,doAcquireNanos(int arg,long nanosTimeout)方法在支持响应中断的基础上, 增加了超时获取的特性。 针对超时获取, 主要需要计算出需要睡眠的时间间隔nanosTimeout, 为了防止过早通知,nanosTimeout计算公式为: nanosTimeout-=now-lastTime, 其中now为当前唤醒时间, lastTime为上次唤醒时间, 如果nanosTimeout大于0则表示超时时间未到, 需要继续睡眠nanosTimeout纳秒,反之, 表示已经超时
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
final long deadline = System.nanoTime() + nanosTimeout;
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
该方法在自旋过程中,当节点的前驱节点为头节点时尝试获取同步状态,如果获取成功则从该方法返回,这个过程和独占式同步获取的过程类似,但是在同步状态获取失败的处理上有所不同。如果当前线程获取同步状态失败,则判断是否超时(nanosTimeout小于等于0表示已经超时),如果没有超时,重新计算超时间隔nanosTimeout,然后使当前线程等待nanosTimeout纳秒(当已到设置的超时时间,该线程会从LockSupport.parkNanos(Object blocker,long nanos)方法返回)。如果nanosTimeout小于等于spinForTimeoutThreshold(1000纳秒)时,将不会使该线程进行超时等待,而是进入快速的自 旋过程。 原因在于,非常短的超时等待无法做到十分精确, 如果这时再进行超时等待, 相反会让nanosTimeout的超时从整体上表现得反而不精确。因此,在超时非常短的场景下, 同步器会进入无条件的快速自旋。
使用的例子
最后给出一个使用的例子——TwinsLock,这个工具类允许在同一时刻,之多两个线程同时访问。
import java.util.concurrent.locks.AbstractQueuedSynchronizer;
import java.util.concurrent.locks.Lock;
public class TwinsLock implements Lock {
private final Sync sync = new Sync(2) ;
private static final class Sync extends AbstractQueuedSynchronizer {
Sync(int count) {
if (count <= 0) {
throw new IllegalArgumentException("count must large than zero.");
}
setState(count) ;
}
public int tryAcquireShared(int reduceCount) {
for (; ; ) {
int current = getState() ;
int newCount = current - reduceCount;
if (newCount < 0 || compareAndSetState(current,
newCount) ) {
return newCount;
}
}
}
public boolean tryReleaseShared(int returnCount) {
for (; ; ) {
int current = getState() ;
int newCount = current + returnCount;
if (compareAndSetState(current, newCount) ) {
return true;
}
}
}
}
public void lock() {
sync. acquireShared(1) ;
}
public void unlock() {
sync. releaseShared(1) ;
}
// 其他接口方法略
}