这篇文章首先对AQS进行详细解析,作为上文,还有下文,专门针对几个以AQS为低层进行实现的工具类进行讲解,通过这篇文章,能够对AQS有更好的理解。
AbstractQueuedSynchronizer(AQS)是java并发包中很多并发工具类的基础,比如java.util.concurrent.locks.ReentrantLock
重入锁、java.util.concurrent.locks.ReentrantReadWriteLock
读写锁、java.util.concurrent.Semaphore
信号量、java.util.concurrent.CountDownLatch
。它们都是在自己的类中又定义了内部类Sync类继承AbstractQueuedSynchronizer,然后重写其中的一些protected修饰的方法。AQS内部定义了一些模板方法,这些模板方法会调用前面说到的protected修饰的方法。通过模板方法设计模式,所有的这些高级的同步组件都不需要再重写关于线程排队、等待、唤醒等相关代码,这些代码被AQS抽离出来实现了重用。
下面我们从独占锁ReentrantLock入手,来看看AQS内部到底做了哪些事情。首先是一段很简单的代码,实现了三个线程同时调用被ReentrantLock加锁保护的方法sayHello。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class AQSDebug {
private Lock lock = new ReentrantLock();
private void sayHello() {
lock.lock();
System.out.println("hello");
lock.unlock();
}
public static void main(String[] args) {
AQSDebug aqsDebug = new AQSDebug();
new Thread(aqsDebug::sayHello, "first").start();
new Thread(aqsDebug::sayHello, "second").start();
new Thread(aqsDebug::sayHello, "third").start();
}
}
为了实现三个线程相互抢占的效果,我们使用多线程断点调试的方法(参考这篇文章),让线程first先获取到锁,然后切换到线程second或者third进行调试,这样就能够看到因为获取不到锁,而加入同步队列,等待,被唤醒的过程。
切换到线程second,我们来看一下,会走到java.util.concurrent.locks.ReentrantLock.NonfairSync#lock
方法,NonfairSync继承了Sync,Sync继承了AbstractQueuedSynchronizer。在lock方法中,首先尝试用CAS的方式,将AQS中用来表示同步状态的volatile变量state设置为1,这里用CAS的方式是保证原子性,因为可能有多个线程同时来争抢。
static final class NonfairSync extends Sync {
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
由于线程first已经获取了锁,所以这里会失败,然后走else,下面是代码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
到这个方法,首先会调用尝试获取,这个方法是一个protected修饰的方法,所以会在ReentrantLock中实现(不同的类在此方法上可以自定义实现,这里不是我们AQS的重点,我们想一下独占锁,first已经获取了,那这里一定会失败),下面是代码
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
ReentrantLock中实现tryAcquire方法的逻辑是,首先看看有没有线程已经获取了同步状态,如果有,看看已经获取的是不是当前线程,因为ReentrantLock是可重入的。这里second线程显然不是,所以返回false。
接着执行addWaiter(Node.EXCLUSIVE)
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
首先构造一个node,把当前线程封装进去。然后,由于刚开始同步队列还是空的,所以走enq方法,否则直接将构造的节点从尾部加入。这里需要注意依然是通过CAS的方式加入尾部,因为可能有多个线程一起加入同步队列。最后将节点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;
}
}
}
}
开始时队列是空的,或者并发插入尾部的时候插入失败,都会走到这里,在这里用死循环的方式尝试将node插入尾部。还有一个细节,就是当队列开始是空的的时候,会先放置一个空节点,这一点也是为了与其他情况兼容:“head指向的头结点都是当前获取锁的节点,当这个节点中的线程释放锁后会唤醒后继节点”,但是刚开始添加第一个节点时,这个节点没有获取锁,为了与其他情况兼容,在它之前插入一个空节点。当获取锁的线程执行完释放锁之后,依然会唤醒head指向节点的后继节点。
然后执行前面acquire中的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);
}
}
构造完节点,把节点加入队列中,然后需要判断当前节点的前一个节点是不是头结点(因为头结点是获取锁的线程节点,随时可能释放锁,只有后继节点有获取锁的可能,再往后面的节点不能),如果是,再次执行tryAcquire方法尝试获取锁,如果恰好获取成功,那么将自己设置为头结点,原来的头结点被GC。如果没有获取成功或者前继节点不是头结点,那么会走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;
}
看看前继节点的waitStatus是不是SIGNAL(SIGNAL表示当前继节点释放锁后会唤醒后继节点),大于0表示被取消的节点,我们的实际情况是second线程前面的节点是空节点,waitStatus=0,所以会为前继节点设置SIGNAL,来在释放锁的时候唤醒后继节点。返回false,然后由于acquireQueued方法是死循环,退出的途径就是获取到了锁。所以会继续走shouldParkAfterFailedAcquire,这一次,前继节点设置了SIGNAL,所以返回true。返回后走下面的parkAndCheckInterrupt方法,这个方法很简单,就是将当前线程阻塞,线程进入WAIT状态。
至此,线程second已经进入阻塞状态。切换到线程third开始调试,跟线程second很相似,只是不需要再在等待队列总加入空节点,直接排队到second节点的后面,然后前面节点也不是头结点,直接进入阻塞状态。
然后我们再切换到线程first,进入解锁流程。unlock会调用AQS中的release方法,代码如下
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
首先还是会调用tryRelease,这个方法是ReentrantLock中实现的protected修饰的方法,代码如下
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
ReentrantLock中实现的方法都是主要对同步状态state进行修改,解锁的时候将state减去1。返回成功后,调用unparkSuccessor,这个方法的主要作用就是唤醒后继节点。这时候second节点被唤醒成功,然后从前面讲到的parkAndCheckInterrupt方法返回,由于没在acquireQueued方法中,会再走一遍for循环,这一次能够获取锁成功,然后将前继节点从队列中移除,自己成为头结点,当自己也执行完成后,会唤醒third节点,然后third线程继续执行完成。
以上就是通过ReentrantLock独占锁来对AQS进行源码级别的剖析。