每个 Java 工程师都应该或多或少地了解 AQS,我已经反复研究了很长时间,忘记了一遍又一遍地看它.每次我都有不同的经历.这一次,我打算重新拿出系统的源代码,并将其总结成一系列文章,以供将来查看.
一般来说,AQS规范是很难理解的,本次准备分五篇文章用来分析AQS框架:
大师给的解释是,虽然大多数应用程序应最大程度地提高总吞吐量,最大程度地容忍缺乏饥饿的概率。但是,在诸如资源控制之类的应用程序中,保持跨线程访问的公平性,容忍较差的聚合吞吐量更为重要,没有任何框架能够代表用户在这些相互冲突的目标之间做出决定;相反,必须适应不同的公平政策。所以AQS框架提供了两种模式
共享模式:允许多个线程同时持有资源;
本篇文章为系列文章的第四篇,本篇文章介绍AQS共享模式的代码实现,首先,我们从总体过程入手,了解AQS的执行逻辑,然后逐步深入分析了源代码。
获取锁的过程:
基于上面提到的获取和释放排他锁的一般过程,让我们来看看源代码实现逻辑.首先,让我们看看获取锁的acquireShared()方法。
public final void acquireShared(int arg) {
//试图获取共享锁。返回值小于0表示获取失败
if (tryAcquireShared(arg) < 0)
//获取锁失败后执行方法
doAcquireShared(arg);
}
这里,tryacquisharered()方法留给用户来实现特定获取锁的逻辑.关于这个方法的实现有两点
根据上面的分析,让我们来看看doAcquireShared方法的实现
private void doAcquireShared(int arg) {
//添加等待节点(与独占锁的唯一区别是节点类型变为共享类型)
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取节点的前节点
final Node p = node.predecessor();
//p == head 表示上一个节点已经获取了锁,当前节点将尝试获取它
if (p == head) {
int r = tryAcquireShared(arg);
//注意,等于0表示不需要唤醒后续节点,大于0需要唤醒
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);
}
}
在独占模式中,排他锁模式设置头节点成功后,会返回到中断状态结束进程。在共享锁定模式获取锁成功之后,setHeadAndPropagate方法将被调用。从方法名中,您可以看到除了设置新的头节点之外还有一个传播的操作.让我们看看下面的代码:
//有两个输入参数,一个是成功获取共享锁的节点,另一个是tryacquisharered方法的返回值。注意,它可能大于或等于0
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // 记录当前的头节点
//设置一个新的头节点,即将获得锁的节点设置为头节点
//注意:这里是获取锁后的操作,不需要并发控制
setHead(node);
//有两种情况需要执行唤醒操作
//1.Propagate>0 表示调用方指示需要唤醒后续节点
//2.头节点后面的节点需要被唤醒(waitstatus < 0),无论它是旧的头节点还是新的头节点
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
//如果当前节点的后继节点是共享类型的或者没有后继节点,它将被唤醒
//可以理解,除非明确表示不需要唤醒(后续等待节点是独占的),否则都需要唤醒
//s.isShared() 在第二篇中有介绍
if (s == null || s.isShared())
//我稍后再详细说
doReleaseShared();
}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
最后的唤醒操作也很复杂,所以我特地把它拿出来分析.
注意:唤醒操作在releasshare()方法中也被调用。
private void doReleaseShared() {
for (;;) {
//唤醒操作从头节点开始.注意,这里的头节点已经是上面新设置的头节点
//实际上,它是唤醒新获得共享锁节点的后继节点
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
//后继节点需要唤醒
if (ws == Node.SIGNAL) {
//这里需要并发控制,因为这里有setHeadAndPropagate和Release两个操作,避免了两次unpark(接触阻塞)
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
//执行唤醒操作
unparkSuccessor(h);
}
//如果后续节点不需要临时唤醒,则当前节点状态被设置为PROPAGATE,以确保它在将来可以被传播
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//如果头部节点没有变化,则表示设置完成,循环退出
//如果head节点改变了,例如,其他线程得到了锁,为了使唤醒动作可以被传递,他必须再次尝试
if (h == head) // loop if head changed
break;
}
}
接下来,让我们看看释放共享锁的过程
public final boolean releaseShared(int arg) {
//试图释放共享锁
if (tryReleaseShared(arg)) {
//唤醒过程,详见上述分析
doReleaseShared();
return true;
}
return false;
}
注意:上面的setHeadAndPropagate()方法表明等待队列中的线程成功地获得了共享锁。此时,它需要唤醒它后面的共享节点(如果有的话)。但是,当共享锁通过releasshared()方法释放时,可以唤醒等待排他锁和共享锁的线程来尝试获取它。
与排他锁相比,共享锁的主要特点是当等待队列中的共享节点成功获得锁(即获得共享锁)时,由于它是共享的,所以必须依次唤醒所有可以与其共享当前锁资源的节点.毫无疑问,这些节点也一定在等待共享锁(这是前提,如果您在等待共享锁),我们可以以读写锁为例.当读锁定被释放时,读锁定和写锁定都可以争用资源。