AQS,全名AbstractQueuedSynchronizer(抽象队列同步器),它是CLH(不明白的可以先了解一下CLH)的变种。它与CLH不同之处在于:
CLH是一种公平锁,它是通过自旋同步队列中节点的前驱结点状态,判断同步队列中的节点是否能够进入临界区;AQS的同步队列中的节点不会以自旋的方式来进入临界区,而是先以公平或者不公平的方式尝试进入临界区,如果不能,则进行阻塞,等待被唤醒,再去尝试是否能够进入临界区。
AQS属于队列,那么就是一个一个节点连接而成,在AQS中节点的数据结构如下:
static final class Node {
static final Node SHARED = new Node();
/** 独占锁模式 */
static final Node EXCLUSIVE = null;
/**
* AQS中判断节点是否为取消状态,有时候是判断状态值是否大于零
*/
static final int CANCELLED = 1;
/**
* 如果节点是这个状态,
* 那么该节点就需要唤醒它的后继节点
*/
static final int SIGNAL = -1;
/**
* 不明白,但是不影响对acquire方法的理解
*/
static final int CONDITION = -2;
/**
* 不明白,但是不影响对acquire方法的理解
*/
static final int PROPAGATE = -3;
/**
* 值为:-3 , -2 , -1 , 0 , 1
*/
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
final boolean isShared() {
return nextWaiter == SHARED;
}
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() {
}
Node(Thread thread, Node mode) {
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) {
this.waitStatus = waitStatus;
this.thread = thread;
}
}
可以看出来AQS同步队列是一个双向链表结构。需要说明的是除了头节点是进入到临界区的节点,其他大部分节点(有一部分节点可能被取消了,waitStatus=1)都是希望进入临界区的节点。
acquire方法解析
AQS中的acquire方法,是获取独占锁方法,它的代码很简单,代码主体只有一个if判断语句:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
可以看到acquire方法实现,主要是下面这三个方法
- tryAcquire(arg)
- addWaiter(Node.EXCLUSIVE)
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
这三个方法的作用
- tryAcquire(arg)
顾名思义,它就是尝试获取锁,AQS在这里没有对其进行功能的实现,只有一个抛出异常的语句,用户可以对其重写实现公平锁、不公平锁、可重入锁、不可重入锁
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
- 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;
}
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;
}
}
}
}
由于可能有多个线程并发加入队尾产生竞争,因此,采用compareAndSetTail无锁方法来保证同步
- acquireQueued(addWaiter(Node.EXCLUSIVE), 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; // 帮助GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) /** 尝试获取锁失败是否阻塞线程获取锁 */ &&
parkAndCheckInterrupt() /** 阻塞 */)
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
前面说过,当一个节点的前驱节点的waitStatus=SIGNAL,当其前驱结点释放锁的时候需要对其后继节点进行唤醒。shouldParkAfterFailedAcquire方法的功能就是判断该节点的前驱结点的waitStatus==SIGNAL,如果相等则该节点可以阻塞,否则将该节点的前驱结点waitStatus状态修改为SIGNAL。由此可以知道,该节点如果没有获取到锁,AQS就会尽最大努力(为什么说最大努力,而不是一定会将节点阻塞呢?可以思考一下,为什么,shouldParkAfterFailedAcquire里面能找到答案)将该节点阻塞,之后等待前驱结点唤醒,再尝试获取锁。