前言
上一篇文章中,我们分析了AbstractQueuedSynchronizer独占锁的源码。
没看的建议先移步上一篇,共用的代码这篇文章中不会再进行详细的解释。接下来,会继续把学习AbstractQueuedSynchronizer共享锁的过程记录下来,希望对你们有所帮助。如果文章中有错误的地方,也请在评论区不吝指正,本人也将第一时间修改错误,来帮助更多的人。
共享同步器的原理
共享锁的实现和同步锁稍有不同,共享锁是对一组资源的控制,打个通俗的比方。学校器材室有10个篮球,每个班级都有权利来借取篮球去使用,使用完毕后归还。有一天大家上体育课,一班的同学跑得快,先到器材室跟大爷借了6个篮球,二班赶到之后,二班需要7个篮球,发现不够,于是在器材室门口排队等候一班归还篮球。这时候三班四班也来借篮球,三班需要2个,四班需要3个。这个时候大家可能会想,现在还剩下4个篮球。可以给三班或者给四班先使用,想法很好,但是老大爷可不允许。老大爷规定,先来后到,不够就等着,于是二三四班都在门口等着,过了一会,一班把篮球归还,还完之后告诉二班我还球了,二班发现需要的7个够了,于是二班借走了七个球,借走之后发现还有余量,于是二班告诉了三班,三班也去借球,借到了两个,发现还有余量,于是告诉4班,4班去接球,发现只有1个了。不够自己使用,于是继续等待。这就是共享锁的基本机制, 细心的可以发现,等待队列严格遵循FIFO, 这其实是保证了公平性, 但是降低了并发量。因为一班一直不还球的话,三班四班明明可以先借到球去玩,但是因为二班需要的球不够,也跟着一起等待。
共享同步器的简单实现
共享同步器,只需要实现tryAcquireShared(int arg)和tryReleaseShared(int arg)就可以。这里借鉴了一下Semaphore的实现
public class MyShardLock {
private final MySync mySync;
public MyShardLock(int permits) {
this.mySync = new MySync(permits);
}
public void acquire(int permits) {
if (permits < 0) {
throw new IllegalArgumentException();
}
mySync.acquireShared(permits);
}
public void release(int permits) {
mySync.releaseShared(permits);
}
final class MySync extends AbstractQueuedSynchronizer {
MySync(int permits) {
setState(permits);
}
@Override
protected int tryAcquireShared(int arg) {
for (; ; ) {
int available = getState();
int remaining = available - arg;
if (remaining < 0 ||
compareAndSetState(available, remaining)) {
return remaining;
}
}
}
@Override
protected boolean tryReleaseShared(int arg) {
for (; ; ) {
int current = getState();
int next = current + arg;
if (next < current) {
throw new Error("Maximum permit count exceeded");
}
if (compareAndSetState(current, next)) {
return true;
}
}
}
}
}
这里需要注意的一点是,tryAcquireShared(int arg)不再像独占锁返回的Boolean值,而是返回的int值:
- 返回负数, 代表资源不够,无法获得资源。
- 返回零, 代表资源分配后刚好够,资源已分配。
- 返回正数, 代表资源分配后还有余量, 资源同样已分配。
源码分析
还是从加锁开始, 加锁首先调用的是acquireShared(int arg)
acquireShared(int arg)
public final void acquireShared(int arg) {
// 首先去获取资源,获取成功直接返回,不成功才会进入doAcquireShared(arg)
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
首先调用tryAcquireShared(arg),这个同样需要自己去实现
protected int tryAcquireShared(int arg) {
throw new UnsupportedOperationException();
}
@Override
protected int tryAcquireShared(int arg) {
for (; ; ) {
int available = getState();
int remaining = available - arg;
if (remaining < 0 ||
compareAndSetState(available, remaining)) {
return remaining;
}
}
}
我们的实现里,计算资源数量,不够直接返回,加自旋是为了保证cas一定能成功。
如果资源不够,则去调用doAcquireShared(arg)
doAcquireShared(arg);
// 和独占锁的获取排队方法很类似
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
// 补充自我中断逻辑,独占锁的是放在acquire()里,其实都一样
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 这里的逻辑上一篇已经细讲过,这里不再赘述,需要的去看上一篇
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
// 这个上一篇里也有。
cancelAcquire(node);
}
}
需要注意的两点
- 补充中断的时机,独占锁里补充中断是放在acquire()里,这里是放在了自旋逻辑里。我认为都是一样的。如果有我没注意到的细节,欢迎留言告诉我。
- finally的执行,和独占锁一样,也是自定义实现里出异常才会执行。
setHeadAndPropagate(Node node, int propagate)
这是申请资源成功后调用的方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
// 这个判断最后会分析,想看的也可以直接跳过去
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}
这里我们看下setHeadAndPropagate(Node node, int propagate)方法,参数propagate是tryAcquireShared(arg)返回的值。逻辑只有一个,满足条件触发doReleaseShared()。注意,前面两个判断的是旧head,后两个判断是新head。这个判断最后会讲,因为涉及到锁的释放。
这里先讲一下锁的释放,因为锁释放也调用了doReleaseShared()
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
tryReleaseShared(arg)方法没什么好说的,看一下上面的实现就懂了。释放成功就会调用doReleaseShared()。这里和独占锁不同的一点是,独占锁直接就可以释放资源,因为有且只有一个线程能获取到资源, 而共享锁不一样 ,共享锁可以同时存在好几个线程拿到资源,所以共享锁释放线程要使用乐观锁。
@Override
protected boolean tryReleaseShared(int arg) {
for (; ; ) {
int current = getState();
int next = current + arg;
if (next < current) {
throw new Error("Maximum permit count exceeded");
}
if (compareAndSetState(current, next)) {
return true;
}
}
}
doReleaseShared()
private void doReleaseShared() {
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
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;
}
}
这里释放资源的时候,检测头结点的状态,如果是SIGNAL,则去唤醒下一个线程。SIGNAL状态很好理解,因为大多数等待的Node都是这个状态,unparkSuccessor(h)方法不再赘述,这里说一下else if里的逻辑。ws什么时候会是0呢?
-
新增的节点,新增的节点是0,但是新增的节点必然是尾节点,上面第一个if判断里,有一个h != tail的判断,所以这个地方存在冲突。但是一定冲突吗?还真不一定。我们做一个这样的假设, 假设线程A获取资源,并且直接获取成功,而队列还没有初始化。此时,线程2来获取资源, 没有获取到,于是走到addWaiter(Node.SHARED)方法里的enq()方法去初始化队列,当走到if (compareAndSetHead(new Node()))和tail = head之间的时候。这个时候头结点是个dummy node,符合head != null, 而这个时候尾节点还没有初始化,是null,同时满足h != tail, 所以如果此时线程A去释放资源,就有可能走到ws == 0这个判断里。
-
唤醒下一个节点的时候,会先把自身的状态设置为0。假设线程A获取资源成功,并且还有余量,就会进入到doReleaseShared()方法继续唤醒下一个Node, 唤醒下一个节点之前,把ws从SIGNAL变更为0, 下一个节点线程B去获取资源,此时有两种情况,
情况1:线程B还没有尝试获取资源,线程A去释放资源,再次走到doReleaseShared()里,此时head节点即线程A状态便是0,把状态变更为PROPAGATE,线程B去获取资源成功,线程A以PROPAGATE状态结束
情况2:线程B尝试获取资源但是失败,还没有走到if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())这个判断,此时线程A去释放资源,再次走到doReleaseShared()里,此时head节点即线程A状态便是0,把状态变更为PROPAGATE,线程B走到shouldParkAfterFailedAcquire()方法里把线程A状态变更为SIGNAL,然后再次自旋tryAcquireShared(arg)成功,因为线程A已经释放了资源, 线程A以SIGNAL状态结束
如果还有其他情况,欢迎留言补充!
我们再回顾一下这段代码
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();
}
}
这五个条件我们逐一分析,首先明确一点,这五个条件是或链接,如果走到后面的判断,那么前面的判断必为false。
- propagate > 0, 这个条件一目了然,就是申请资源后,还有支援的剩余。
- h == null, 这个条件大家可能就有点迷糊了。什么情况下,h==null呢。翻阅代码我发现,h == null不可能成立, 因为Node h = head在执行setHead()之前,这个时候head肯定不为null,即使队列之前为空走了enq()方法,也会有一个dummy node,我网上翻阅资料后,发现这个是防止空指针异常发生的标准写法。
- h.waitStatus < 0,这个h是旧head, 这里有两种情况:
情况一:我们假设一个线程B在doAcquireShared(int arg)方法里刚执行完图下的时间片1,另一个线程A是尾节点,且去释放资源,走到doReleaseShared()里,这个时候ws就是0,状态被设置为Node.PROPAGATE。然后线程B获取资源,由于资源已经释放,肯定可以获取到,资源获取到之后走到setHeadAndPropagate()方法,这个条件就会成立了。但是这是一次无效的唤醒。因为走到这个判断,propagate必然等于0。这时,Node.PROPAGATE只是一个中间状态,方便被检测到。
情况二:还一种情况是一个线程A在doAcquireShared(int arg)方法里走到了图下时间片2。另一个线程B释放资源,状态设置为Node.PROPAGATE, 线程A走到shouldParkAfterFailedAcquire()方法里,又把头结点状态ws更改为SIGNAL, 之后在第二次执行自旋的tryAcquireShared(arg)获取到了资源,走到setHeadAndPropagate()方法, 此时这个条件仍然是成立的,这仍然是一次无效的唤醒。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
// 时间片1
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;
}
}
// 时间片2
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- (h = head) == null, 这个也不会成立的。同二
- h.waitStatus < 0 ,这个是经常存在的,因为只要有后继节点,后继节点就会把前驱节点的状态设置为SIGNAL, 一个线程拿到资源时,如果有后继节点,就会走到这个判断。这种情况也会造成不必要的唤醒
关于不必要的唤醒
作者在注释里提到了,不必要的唤醒是有必要的。这段代码经过这么久的测试,也确实证明了是很健壮的。
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
至此,AbstractQueuedSynchronizer的部分已经讲完了,我们更多的应该去思考。很多时候,思考的可能都是错误的,但是这种思考的过程很重要,如果能对思考的结果做一个求证,那会更加的完美。
关于学习AbstractQueuedSynchronizer引申的几个思考
看到这里,我觉得大家有必要问自己几个问题。
- AbstractQueuedSynchronizer是公平锁还是非公平锁?
- 学习AbstractQueuedSynchronizer有什么用?或者可以说学习了AbstractQueuedSynchronizer让我们对jdk哪些常用的工具类一目了然了。
- AbstractQueuedSynchronizer源码中有什么值得我们学习的地方并且可以应用在自己的编码里的。
- 自己能不能写一篇博客来给大家讲一讲AbstractQueuedSynchronizer。
关于这几个问题,相信大家都有不同的答案,还是那句话,思考很重要。我来给大家说一说第四个问题的,源码我看过不少,相信大家也都或多或少的看过,但是看过之后马上就忘掉的居多,看着看着瞌睡的时候也不少。我在看ThreadPoolExecutor的时候,一个封装任务的Worker类继承了aqs,我当时突然想到一个问题,aqs的源码之前我看过一次,现在如果让我跟别人讲一讲,我能不能讲出来。答案是否定的。然后我就问自己,为什么我会遗忘的这么快。我想起了我小学语文老师说过的一句话,看一遍不如写一遍,写一遍不如给别人讲一遍。于是我开始了写博客之旅,记录学习,分享知识。