AQS框架
首先对于syncronized
,锁对象的对象头指向了一个管程,由管程负责维护EntryList
和WaitSet
的队列。
同样的,对于我们自定义的锁,除了线程安全地获取和释放锁之外,也需要做维护这些等待队列的工作。而AQS提供的就是一个这样的维护等待队列的机制,用户只需要关注于自己的锁的释放和获取的逻辑就可以了。这就是AQS的基本功能。
AQS内部维护了一个volatile
修饰的state
表示锁的占有状态,同时维护了一个线程等待的CLH队列。
AQS将资源分为独占和共享的,因此自定义同步器只需要实现对应的tryAcquire-tryRelease和tryAcquireShared-tryReleaseShared即可
源码
结点
节点设置了头尾结点指针、结点对应线程、结点状态等信息,结点状态分成以下几个
- CANCLED(1):结点取消调度,timeout或中断的话会出发变更成此状态,进入此状态后结点不在变化
- (0):新结点进入队的初始状态
- SIGNAL(1):后续有结点在等待本结点的唤醒
- CONDITION(2):该结点等待在CONDITION上,有其他线程唤醒时该线程将从等待队列转移到同步队列获取同步锁
- PROPAGATE(3):共享模式下不仅会唤醒后继结点,还会唤醒后继结点的后继结点
独占模式
acquire
代码
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
首先执行tryAcquire
来尝试获取同步锁,这个方法是需要调用者自行定义的,AQS中只抛出了一个异常,之所以没用抽象方法是因为除了独占模式还有共享模式,使用抽象方法的话无论哪种模式都要实现另一种模式下的抽象方法,所以为了方便这里没有定义成一个抽象的方法
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
其次将Node添加到等待队列当中
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;
}
}
}
}
在线程进入队列之后就需要排队获取同步锁了,这就是上面的acquireQueued()
方法,注意在等待的过程中如果超时或者被中断了会执行cancelAcquire
方法放弃等待。该方法的一次循环是执行尝试获得锁或者暂停本线程
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);
}
}
进入等待的shouldParkAfterFailedAcquire(p, node)
方法主要做的就是找到前面第一个没有放弃获取锁、也就是状态小于0的结点,将这个状态小于零的结点置为-1,使其释放锁之后通知当前结点
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;
}
再告知前面的结点通知自己之后,该线程就可以暂停了
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
总结
上锁经历了这样一个过程
- 尝试获取锁
- 建立本线程的结点
- 如果该结点是哑元后的第一个结点,尝试获得同步锁,失败的话结点进入队尾
- 通知前面的结点在释放锁之后通知本结点
- 本线程
park
暂停
release
唤醒过程的代码如下,首先尝试释放同步锁,如果同步锁被彻底释放,会唤醒等待队列中的线程使其尝试获取锁(见acquire
的代码)
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
逻辑由调用者编写,返回资源释放是否成功的布尔值
/**
* Wakes up node's successor, if one exists.
*
* @param node the node
*/
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
对于结点的唤醒是从后往前找的,因为在AQS的代码中入队和取消排队的prev炼是用CAS操作保证强一致性的,而next链并非强一致的
共享模式
acquireShared
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
tryAcquireShared
方法同样是用户自定义的逻辑,尝试获取资源并返回一个整型值,非负数表示剩余资源数量,负数表示失败,doAcquireShared
也是类似acquire
的入队流程
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);
}
}
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();
}
}
值得注意的是,尝试获取到资源之后,除了唤醒自己之外,还会唤醒自己的下一个结点尝试获取资源,这是与独占不同的地方,独占是释放资源才会唤醒后继的线程
releaseShared
releaseShared
也跟release
相似,每一个线程释放资源后都会唤醒队列中的线程
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
应用
不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:
isHeldExclusively()
:该线程是否正在独占资源。只有用到condition才需要去实现它。tryAcquire(int)
:独占方式。尝试获取资源,成功则返回true,失败则返回false。tryRelease(int)
:独占方式。尝试释放资源,成功则返回true,失败则返回false。tryAcquireShared(int)
:共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。tryReleaseShared(int)
:共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。
CountDownLatch
、ReentranLock
等的实现都基于AQS
ReentrantLock
ReentrantLock
有公平的和非公平的两种基于AQS的同步器的实现,不同在于是否在竞争锁之前检查等待队列的状态ReentrnatLock
可重入是基于判断锁的Owner是否是当前线程,如果是的话计数增加,同理释放的时候计数减少,计数为0的时候就证明锁完全释放了ReentrantLock
的可打断:AQS的实现中也有一个打断标记,不过只是将这个打断标记返回,而可打断逻辑当中是在打断的时候抛出了异常ReentrantLock
还维护了ConditionObject
对象,内部也维护了一个队列,当唤醒该Condition的时候会将队列当中的结点添加到AQS维护的等待队列当中