什么是AQS
AbstractQueuedSynchronizer,是Java并发包中,实现各种同步结构和部分其他组成单元的基础,如线程池中的Worker。
为什么需要AQS
Doug Lea曾经介绍过AQS的设计初衷。从原理上,一种同步结构往往是可以利用其他的结构实现的。但是,对某种同步结构的倾向,会导致复杂的实现逻辑,所以他选择将基础的同步相关操作抽象在AbstractQueuedSynchronizer中,利用AQS为我们构建同步结构提供范本。
AQS的内部数据和方法
-
一个volatile的整数成员表征状态,同时提供了setState和getState方法。
private volatile int state;
-
一个先入先出(FIFO)的等待线程队列,以实现多线程间竞争和等待,这是AQS机制的核心之一。
-
各种基于CAS的基础操作方法,以及各种期望具体同步结构去实现的acquire/release方法。
如何利用AQS实现同步结构
利用AQS实现一个同步结构,至少要实现两个基本类型的方法,分别是:
- acquire操作,获取资源的独占权.
- release操作,释放对某个资源的独占。
以ReentrantLock为例,它内部通过扩展AQS实现了Sync类型,以AQS的state来反映锁的持有情况。
private final Sync sync;
absract static class Sync extends AbsractQueuedSynchronizer { …}
下面是ReentrantLock对应acquire和release操作,如果是CountDownLatch则可以看作是await()/countDown(),具体实现也有区别。
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
下面是线程试图获取锁的过程,acquire()的实现逻辑是在AQS的内部,调用了tryAcquire和acquireQueued。
tryAcquire是按照特定场景需要开发者去实现的部分,而线程间竞争则是AQS通过Waiter队列与acquireQueued提供的。
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
在ReentrantLock中,tryAcquire的逻辑实现在NonfairSync和FairSync中,分别提供了进一步的非公平或公平性方法,而AQS内部tryAcquire仅仅 是个接近未实现的方法(直接抛异常)。
ReentrantLock默认是非公平的,下面是公平性在ReentrantLock构建时如何设定的。
public ReentrantLock() {
sync = new NonfairSync(); // 默认是非公平的
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
以非公平的tryAcquire为例,其内部实现了如何配合状态与CAS获取锁。对比公平版本的tryAcquire,非公平版本在锁无人占有时,并不检查是否有其他等待者,这里体现了非公平语义。
fnal boolean nonfairTryAcquire(int acquires) {
fnal Thread current = Thread.currentThread();
int c = getState();// 获取当前AQS内部状态量
if (c == 0) { // 0表示无人占有,则直接用CAS修改状态位,
if (compareAndSetState(0, acquires)) {// 不检查排队情况,直接争抢
setExclusiveOwnerThread(current); //并设置当前线程独占锁
return true;
}
} else if (current == getExclusiveOwnerThread()) { //即使状态不是0,也可能当前线程是锁持有者
//因为这是再入锁
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
接下来分析acquireQueued,如果前面的tryAcquire失败,代表着锁争抢失败,进入排队竞争阶段。
当前线程会被包装成为一个排他模式的节点(EXCLUSIVE),通过addWaiter方法添加到FIFO队列中。
如果当前节点的前面是头节点,则试图 获取锁,一切顺利则成为新的头节点;否则,有必要则等待。
final boolean acquireQueued(final Node node, int arg) {
boolean interrupted = false;
try {
for (;;) {// 循环
final Node p = node.predecessor();// 获取前一个节点
if (p == head && tryAcquire(arg)) { // 如果前一个节点是头结点,表示当前节点合适去tryAcquire
setHead(node); // acquire成功,则设置新的头节点
p.next = null; // 将前面节点对当前节点的引用清空
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node)) // 检查是否失败后需要park
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);// 出现异常,取消
if (interrupted)
selfInterrupt();
throw t;
}
}
总结。以上是对ReentrantLock的acquire方法的分析,release方法与之相似。
参考
《Java并发编程实战》
《Java核心技术36讲》杨晓峰