AQS(全称:AbstractQueuedSynchronizer),要探究AQS的原因是,几个常见的同步工具都是基于它实现的(栅栏:CountDownLatch,可重入锁:ReentrantLock,可重入读写锁:ReentrantReadWriteLock)。 AQS本身提供了一个等待队列CLH和一个资源变量(state),通过对资源变量的获取修改释放等操作实现各种同步功能,当多个线程尝试获取资源变量时,如果无法获取到,就会进入等待队列,AQS本身实现了队列和资源变量的维护,包括线程进入队列以及何时被唤醒。
state
state是AQS维护的一个属性,用volatile修饰,保证其可见性。该属性就是上面说的资源变量,AQS提供了对该变量的操作方法:
getState()
setState()
compareAndSetState()
这三个方法都是由final修饰,不可重构,并且都是原子操作。前两个get、set方法很好理解,第三个compareAndSet基于CAS的原子操作实现的,源码如下:
protected final boolean compareAndSetState(int expect, int update) {
// See below for intrinsics setup to support this
return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
AQS的使用
基于AQS实现一个同步器,是要继承AbstractQueuedSynchronizer类,并按照自己的需要重构以下几个方法:
isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。
tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。
tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。
tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
可以看到其实就是两种类型的获取资源和释放资源,AQS提供两种获取资源的方式,一种是独占方式:比如上面说的可重入锁,一个时刻他只允许一个线程获得共享资源state,另一种是共享方式:比如上面说的栅栏,他是允许多个线程去获取共享资源state的。
原理剖析
资源独占模式
资源独占模式下,我们要实现锁,都可以通过调用AQS的acquire(int arg)方法实现加锁,该方法表示以独占的方式尝试去获取资源,我们可以在可重入锁ReentrantLock、可重入读写锁ReentrantReadWriteLock、线程池构造器ThreadPoolExecutor中的lock()方法都是通过调用acquire(int arg)实现加锁的。acquire(int arg)源码如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到acquire中去调用了上面(AQS的使用)写到的这个方法tryAcquire(arg),如果tryAcquire获取到共享资源就直接结束了,否则就会调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg)进入等待队列,陷入阻塞。可以看到Node.EXCLUSIVE指明了该节点是一个独占节点。看一下addWaiter的源码:
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;
}
可以看到new了个Node节点,并把当前线程传了进去。然后将这个节点的前一个节点指向原本的尾结点,然后通过compareAndSetTail这个原子操作,替换掉AQS对象中的尾部节点,再把原本的尾部节点的下一个节点指向new出来的这个节点。如果说原本的尾结点为空,或者说compareAndSetTail这个操作失败了(即尾结点已经被其他的线程修改过了)则会走enq(node)这个方法。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中是一个自旋锁,如果等待队列为空则通过compareAndSetHead进行自旋(修改头结点),如果不为空则通过compareAndSetTail进行自旋(增加尾结点)。到此完成等待队列的线程节点增加,然后回到外层acquire(int arg)方法,往下继续执行的方法是acquireQueued(final Node node, int arg):
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;
}
//如果当前节点不是头结点,或者头结点获取资源失败
//判断是否需要park当前线程
//parkAndCheckInterrupt则是对当前线程进行park
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
我们可以进到shouldParkAfterFailedAcquire(p, node)去看一下:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
可看到每一个节点都一个状态变量waitStatus,变量定义:
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;
如果节点状态是SIGNAL,则表示节点需要park,如果是大于0即CANCELLED则会执行循环,节点不断往前移,并且当前节点的前一个节点也不断改变(其实就是把那些已经CANCELLED的节点从等待队列中去除掉)。如果既不是SIGNAL也不是CANCELLED,就会把节点的状态通过自旋锁(compareAndSetWaitStatus)改成SIGNAL,然后通过外层的循环进来再次判断节点为SIGNAL,然后park线程。parkAndCheckInterrupt()这个方法则是对当前线程进行park。总之只要队列不是全都为CANCELLED,那一定会park,否则就不断去掉CANCELLED节点,到了头结点,然后循环调tryAcquire()尝试获取共享资源,如果成功了,就会把当前节点设置为头结点,如果失败了就继续循环尝试获取共享资源。
上面是加锁,然后是释放锁,通过调用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;
}
release方法会调用需要我们重写的tryRelease(int arg) 方法来释放锁,如果释放成功,则会从等待队列的头结点开始,唤醒一个最近的线程。
资源独占模式小结
这种模式核心两个方法acquire和release,需要我们实现的是tryAcquire和tryRelease。整个流程:acquire尝试获取共享资源,如果获取不到就加到等待队列,如果等待队列只剩下head了,就循环去尝试获取共享资源,否则就睡眠。release进行释放共享资源,如果释放成功,就去等待队列中唤醒最靠前的一个节点让他继续去获取共享资源。所以,这么来看的话,AQS实现独占锁是一个公平锁,遵循先来先获取锁的准则。
资源共享模式
我们熟知的栅栏CountDownLatch是AQS的资源共享模式的典型案例。在CountDownLatch中,我们通过java.util.concurrent.CountDownLatch#await(long, java.util.concurrent.TimeUnit)方法阻塞等待线程,通过java.util.concurrent.CountDownLatch#countDown()来标记一个被等待线程执行结束,当countDown的次数到达阈值,等待线程就会继续往下执行。我们来看一下这两个方法的源码,首先是await方法:
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
sync则是继承自AQS的同步类,我们进入acquireSharedInterruptibly方法:
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
执行了需要我们重写的tryAcquireShared方法,如果共享模式获取资源失败,则会执行doAcquireSharedInterruptibly这个方法,我们看一下这个方法的源码:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
addWaiter方法创建了一个共享模式的线程节点放入到等待队列,然后进入一个循环,如果前一个节点不是头节点,就park,是头节点就tryAcquireShared尝试获取资源(这里和独占模式是一样的,只是调用的方法一个是tryAcquireShared一个是tryAcquire)。我们来看一下栅栏中tryAcquireShared怎么实现的:
protected int tryAcquireShared(int acquires) {
return (getState() == 0) ? 1 : -1;
}
如果共享变量等于0,表示获取资源成功,否则失败。而一开始state设置的是阈值,所以等待线程一直无法获取到资源的,所以一直阻塞在那里不断地去获取资源。
再来看一下countDown这个方法,这个方法直接调用了同步器中的releaseShared方法,源码如下:
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
这个方法调用了tryReleaseShared进行释放共享资源,栅栏中的资源释放代码如下:
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
可以看到和上面的await里的tryAcquireShared相呼应,获取资源是更具state为0进行获取,释放资源时对state进行减一操作,每一个被等待线程进行一次减一,最终等于0,原本的等待线程再去调用tryAcquireShared就可以获取到资源,然后停止等待,往下执行了。资源释放之后调用doReleaseShared方法,这个方法和独占中一样,也是去把阻塞队列中离头节点最近的一个唤醒,让他可以重新执行获取资源的操作。