共计 3276 个字符,预计需要花费 9 分钟才能阅读完成。
搞清楚 AQS 独占锁的实现原理之后,再看共享锁的实现原理就会轻松很多。两种锁模式之间很多通用的中央本文只会简略阐明一下,就不在赘述了
一、执行过程概述
获取锁的过程:
- 当线程调用 acquireShared()申请获取锁资源时,如果胜利,则进入临界区。
- 当获取锁失败时,则创立一个共享类型的节点并进入一个 FIFO 期待队列,而后被挂起期待唤醒。
- 当队列中的期待线程被唤醒当前就从新尝试获取锁资源,如果胜利则 唤醒前面还在期待的共享节点并把该唤醒事件传递上来,即会顺次唤醒在该节点前面的所有共享节点,而后进入临界区,否则持续挂起期待。
开释锁过程:
- 当线程调用 releaseShared()进行锁资源开释时,如果开释胜利,则唤醒队列中期待的节点,如果有的话。
二、源码深入分析
基于下面所说的共享锁执行流程,咱们接下来看下源码实现逻辑:
首先来看下获取锁的办法 acquireShared(),如下
public final void acquireShared(int arg) {
// 尝试获取共享锁,返回值小于 0 示意获取失败
if (tryAcquireShared(arg) < 0)
// 执行获取锁失败当前的办法
doAcquireShared(arg);
}
这里 tryAcquireShared()办法是留给用户去实现具体的获取锁逻辑的。对于该办法的实现有两点须要特地阐明:
一、该办法必须本人查看以后上下文是否反对获取共享锁,如果反对再进行获取。
二、该办法返回值是个重点。其一、由下面的源码片段能够看出返回值小于 0 示意获取锁失败,须要进入期待队列。其二、如果返回值等于 0 示意以后线程获取共享锁胜利,但它后续的线程是无奈持续获取的,也就是不须要把它前面期待的节点唤醒。最初、如果返回值大于 0,示意以后线程获取共享锁胜利且它后续期待的节点也有可能持续获取共享锁胜利,也就是说此时须要把后续节点唤醒让它们去尝试获取共享锁。
有了下面的约定,咱们再来看下 doAcquireShared 办法的实现:
// 参数不多说,就是传给 acquireShared()的参数
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);
// 留神下面说的,等于 0 示意不必唤醒后继节点,大于 0 须要
if (r >= 0) {
// 这里是重点,获取到锁当前的唤醒操作,前面具体说
setHeadAndPropagate(node, r);
p.next = null;
// 如果是因为中断醒来则设置中断标记位
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 挂起逻辑跟独占锁一样,不再赘述
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
// 获取失败的勾销逻辑跟独占锁一样,不再赘述
if (failed)
cancelAcquire(node);
}
}
独占锁模式获取胜利当前设置头结点而后返回中断状态,完结流程。而共享锁模式获取胜利当前,调用了 setHeadAndPropagate 办法,从办法名就能够看出除了设置新的头结点以外还有一个传递动作,一起看下代码:
// 两个入参,一个是以后胜利获取共享锁的节点,一个就是 tryAcquireShared 办法的返回值,留神下面说的,它可能大于 0 也可能等于 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;
// 如果以后节点的后继节点是共享类型或者没有后继节点,则进行唤醒
// 这里能够了解为除非明确指明不须要唤醒(后继期待节点是独占类型),否则都要唤醒
if (s == null || s.isShared())
// 前面具体说
doReleaseShared();}
}
private void setHead(Node node) {
head = node;
node.thread = null;
node.prev = null;
}
最终的唤醒操作也很简单,专门拿进去剖析一下:
注:这个唤醒操作在 releaseShare()办法里也会调用。
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;
// 执行唤醒操作
unparkSuccessor(h);
}
// 如果后继节点临时不须要唤醒,则把以后节点状态设置为 PROPAGATE 确保当前能够传递上来
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
// 如果头结点没有发生变化,示意设置实现,退出循环
// 如果头结点发生变化,比如说其余线程获取到了锁,为了使本人的唤醒动作能够传递,必须进行重试
if (h == head)
break;
}
}
接下来看下开释共享锁的过程:
public final boolean releaseShared(int arg) {
// 尝试开释共享锁
if (tryReleaseShared(arg)) {
// 唤醒过程,详情见下面剖析
doReleaseShared();
return true;
}
return false;
}
注:下面的 setHeadAndPropagate()办法示意期待队列中的线程胜利获取到共享锁,这时候它须要唤醒它前面的共享节点(如果有),然而当通过 releaseShared()办法去开释一个共享锁的时候,接下来期待独占锁跟共享锁的线程都能够被唤醒进行尝试获取。
三、总结
跟独占锁相比,共享锁的次要特色在于当一个在期待队列中的共享节点胜利获取到锁当前(它获取到的是共享锁),既然是共享,那它必须要顺次唤醒前面所有能够跟它一起共享以后锁资源的节点,毫无疑问,这些节点必须也是在期待共享锁(这是大前提,如果期待的是独占锁,那后面曾经有一个共享节点获取锁了,它必定是获取不到的)。当共享锁被开释的时候,能够用读写锁为例进行思考,当一个读锁被开释,此时不论是读锁还是写锁都是能够竞争资源的。
欢送关注公众号【码农开花】一起学习成长
我会始终分享 Java 干货,也会分享收费的学习材料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦