- Java锁的逻辑(结合对象头和ObjectMonitor)
- 还在用饼状图?来瞧瞧这些炫酷的百分比可视化新图形(附代码实现)⛵
- 自动注册实体类到EntityFrameworkCore上下文,并适配ABP及ABPVNext
- 基于Sklearn机器学习代码实战
在上一篇文章 多线程并发(一 )中我们通过acquire()详细地分析了AQS中的独占锁的获取流程,提到独占锁,自然少不了共享锁,所以我们这边文章就以AQS中的acquireShared()方法为例,来分析下并发编程中共享锁的获取与释放吧,获取共享锁的大体流程和获取独占锁一样,但是因为共享锁可以被多个线程同时持有,所以共享锁比起独占锁来可能更复杂,文章有点长,静下心来,慢慢读,读完之后可能会使你收获颇多.
通过上篇文章的分析,我们发现AQS中主要做三件事:1、同步状态的state的获取和释放,即同步状态的管理;2、同步队列的维护;3、线程的阻塞和唤醒,即线程间的协作;AQS中定义了大量的同步状态管理的模板方法,比如acquireShared()就是一个线程获取共享锁的入口方法,我们就从这个方法开始我们的共享锁之旅吧! 。
1、acquireShared(int arg)方法:
1
public
final
void
acquireShared(
int
arg) {
2
//
获取共享资源成功直接返回
3
if
(tryAcquireShared(arg) < 0
)
4
//获取资源不成功执行此方法阻塞
5
doAcquireShared(arg);
6
}
这个方法是AQS中定义的一个模版方法,也是获取共享锁的入口,调用tryAcquireShared()尝试获取共享锁,如果获取共享锁成功,则此方法直接返回;获取共享锁不成功,则执行doAcquireShared()方法,将当前节点包装成Node节点,加入到同步队列中,进行阻塞,直到被其它线程唤醒了,成功获取到了共享锁再返回。是不是和获取独占锁的流程很类似呢?是的,大致流程基本一致,但是两者最大的区别共享锁在同一时刻只能被一个线程持有,而共享锁在同一时刻可能会被多个线程同时持有,所以共享锁比独占锁更复杂。其中这个tryAcquireShared()方法我们这个模版类中只提供了定义,并没有提供实现,具体实现还需要自定义的同步器去实现。我们接着往下看doAcquireShared()方法:
2、doAcquireShared方法:
1
private
void
doAcquireShared(
int
arg) {
2
//
将当前线程加入到同步队列中,并标记为共享模式
3
final
Node node =
addWaiter(Node.SHARED);
4
//
线程阻塞等待获取共享资源的过程中是否发生了异常
5
boolean
failed =
true
;
6
try
{
7
//
现在在阻塞等待获取资源的过程中,其它线程对此线程是否发生了中断请求
8
boolean
interrupted =
false
;
9
10
/*
自旋,找到合适点的点将当前线程挂起,再寻找合适点的过程中也不断尝试重新获取共享锁,因为可能再这个尝试的过程中,其它线程释放了共享锁
*/
11
for
(;;) {
12
//
找到当前节点的前继节点
13
final
Node p =
node.predecessor();
14
15
//
当前节点的前继节点是同步队列的头节点
16
if
(p ==
head) {
17
//
尝试获取指定量的共享资源
18
int
r =
tryAcquireShared(arg);
19
//
当前Node节点的线程成功获取到了共享资源
20
if
(r >= 0
) {
21
//
将当前线程Node节点设置为head头节点,并尝试唤醒后面的阻塞节点
22
setHeadAndPropagate(node, r);
23
p.next =
null
;
//
help GC
24
//
等待获取资源的过程中发生了线程中断的请求是不响应线程中断的,所以这里要将线程中断补上。
25
if
(interrupted)
26
//
获取独占锁是放在acquire()方法中处理的,不过作用都一样。
27
selfInterrupt();
28
//
表示获取阻塞获取资源的过程中没有发生异常,就不用执行finally中的取消方法了。
29
failed =
false
;
30
return
;
31
}
32
}
33
34
//
找到当前被阻塞线程节点的前继有效节点,将它的状态设置为Node.SIGNAL
35
if
(shouldParkAfterFailedAcquire(p, node) &&
36
/**
37
* 找到了有效前继节点并它的状态设置为Node.SIGNAL,那么我们就可以将当前节点park(),
38
* 等待前继节点释放资源后唤醒它,唤醒之后在进行一次线程中断检测,进入下次"自旋"。
39
*/
40
parkAndCheckInterrupt())
41
interrupted =
true
;
42
}
43
}
finally
{
44
//
阻塞等待获取共享资源的时候发生了异常,需要将当前Node节点出队,上一篇文章中讲过,这里就不再赘述了。
45
if
(failed)
46
cancelAcquire(node);
47
}
48
}
上面已经说过这个方法主要干两件事情:1、将阻塞的线程包装成Node节点,加入到同步队列中;2、通过一定次数的“自旋”操作,当前线程找到合适的点,将自己挂起,等待其它线程唤醒.
在寻找这个“合适点”( 这个合适点的选择,上篇文章 多线程并发(一):以AQS中acquire()方法为例来分析多线程间的同步与协作 中提到过,有不清楚的可以在这里找到答案 )的过程中,有可能其它线程释放了共享锁,那么当前线程应该检查下有没有资格获取,有资格获取,并且获取成功,那么就将它自己设置为头节点,然后唤醒后继节点之后再返回,至此获取锁的整个流程就完了.
细心的读者可能发现,这个“自旋”中,将尝试获取锁放在前面,将阻塞判定放在后面执行,现在想想这是不是一个类型do{}while()模型呢,要是第一次直接获取锁成功了,是不是线程就少了一次阻塞----》唤醒的状态转化呢?
上面方法中也提到过,“如果当前线程节点的前继节点是队列head节点时,我们就可以尝试获取一次共享资源”。为什么当前节点的前继节点不是head节点的时候,就不能尝试获取共享锁呢?这是因为当前这个LCH同步队列是严格按照FIFO出队的,当前节点前继节点不是head, 说明在当前节点之前还有线程被阻塞等待获取共享锁,所以当前线程节点就应该老老实实地等待,等待它的前继节点获取成功共享锁或者释放了共享锁之后,再唤醒它去尝试获取共享锁吧.
在独占锁模式中,因为锁只能被一个线程持有,所以当同步队列中的一个线程获取了独占锁之后,只需要将它自身设置为头节点,让原来的头节点“出队”就可以了。但是,在共享锁模式下,因为共享锁可以被多个线程同时持有,当前线程获取共享锁成功,并将自身设置为头节点之后,还需判断同步队列中是否有满足唤醒条件的后继节点,如果有则继续唤醒后继节点去竞争共享锁,这个是通过 setHeadAndPropagate()来实现的.
3、setHeadAndPropagate()方法分析:
private
void
setHeadAndPropagate(Node node,
int
propagate) {
//
后继节点成功获取了共享锁,队列的"旧head"还没有改变,将其保存下来,锁定到方法的局部变量做后序的判断使用;
Node h = head;
//
Record old head for check below
/**
* 将这个获取共享锁成功的后继节点设置为同步队列的“新head”,此时同步队列的head发生变化, 此线程还未唤起任何线程。
*/
setHead(node);
/**
* 1、h == null这个条件什么时候成立呢?仔细翻了下AQS中的源码发现: * 这个setHeadAndPropagate()方法只在共享锁模式下,同步队列head的后继节点成功获取了共享锁才会调用。 * 获取到共享锁的当前线程是同步队列的头结点的后继节点,"旧head"有后继节点,说明同步队列不为空,那么"旧head"也必定不为空, * 此方法中第一行通过h == head,在执行setHead(node)方法之前将"旧head"保存了下来,所以h == null必定不会成立, * 至于为什么这么写呢? 查阅了下资料网上说"发现这个是防止空指针异常发生的标准写法(既如果要取一个对象的某个属性进行判断的时候,首先对这个对象进行null判断)。" * 这说的过去吧? * * 2、(h = head) == null这个条件什么时候成立呢? * 这个条件也是不可能成立的,下面这种情况应该是最常见的: * (1)、例如有个Semaphore实例s初始化了2个许可,线程A首先调用s.acquire(2)申请了两个许可,成功申请到了许可; * (2)、线程B调用了s.acquire()方法申请一个许可,申请失败,加入到同步队列; * (3)、线程C调用了s.acquire()方法申请一个许可,申请失败,加入到同步队列; * (4)、线程A调用了s.releaseShared(2)方法释放了两个许可,再调用doReleaseShared()方法,进行同步队列唤醒; * (6)、首先唤醒了同步队列中的线程B,B线程获取到共享锁: * a)、如果此时线程B还未setHead(Node)方法,还未改变同步队列的head头结点,那么线程A的唤醒工作就结束,也仅仅只是唤醒了同步队列中的线程B, * 则必定有(h = head) == Node(C) != null成立,线程C的唤醒工作仍然需要线程B去执行; * b)、如果此时线程B执行了SetHead(Node)方法,改变了同步队列的head头结点,那么线程A同时也会唤醒线程C,相当于线程A同时唤醒了线程B和线程C: * 1)、如果线程C中的setHeadAndPropagate()在线程B前调用完毕(即线程C执行了setHead()方法改变了同步队列的head),那么 (h = head) == Node(C); * 2)、如果线程C中的setHeadAndPropagate()在线程B之后才调用(即线程C此时还未执行setHead()方法,未改变同步队列的head),那么 (h = head) == Node(B) * 所以综上所述,只要执行过addWaiter()方法,向同步队列中添加过线程,那么(h = head)== null必定不成立。只能理解为“防止空指针的标准写法”。
*/
if
(propagate > 0 || h ==
null
|| h.waitStatus < 0 ||
(h
= head) ==
null
|| h.waitStatus < 0
) { Node s
=
node.next;
/**
* s == null这种情况是可能存在的,如果当前唤醒的这个node节点是同步队列的尾节点就可能出现node.next == null; * s.isShared()指定是共享锁模式,当前线程获取共享锁之后,是需要尝试唤醒同步队列中的其它线程的。
*/
if
(s ==
null
||
s.isShared()) doReleaseShared(); } }
上面提到的这个方法其实就做了两件事情:1、将当前获取共享锁的线程设置为同步队列的头节点;2、根据同步队列头节点head的状态,来决定是否需要唤醒后续节点,符合条件就调用doReleaseShared()方法执行唤醒后续节点的操作。存在的各种情况,也在上面的代码中上面代码中分析过了,下面我们接着往下走来分析doReleaseShared()方法吧! 。
4、doReleaseShared()方法:
1
private
void
doReleaseShared() {
2
for
(;;) {
3
Node h =
head;
4
/**
5
* h != null保证了队列不为空,h != tail保证了队列中有需要唤醒的节点,
6
* 如果这不能同时满足说明队列中没有需要唤醒的节点,此时h == head这个条件是成立的,
7
* 直接跳转到h == head判断中break,此方法结束执行。
8
*/
9
if
(h !=
null
&& h !=
tail) {
10
int
ws =
h.waitStatus;
11
//
如果头节点的状态是Node.SIGNAL说明后续有节点是需要唤醒的,
12
if
(ws ==
Node.SIGNAL) {
13
/**
14
* 考虑到共享锁可以被多线程并发持有,可以采用CAS操作,将设置头节点的状态为的0的compareAndSetWaitStatus(h,Node.SIGNAL,0)的操作
15
* 和unparkSuccessor(h)唤醒后节点的操作绑定在一起,这个CAS操作成功,说明头节点之前肯定是Node.SIGNAL状态,那么后继结点肯定能被唤醒。
16
*/
17
if
(!compareAndSetWaitStatus(h, Node.SIGNAL, 0
))
18
continue
;
//
loop to recheck cases
19
unparkSuccessor(h);
20
}
21
/**
22
* ws == 0说明头节点的后继节点已经被唤醒或者即将被唤醒。
23
*/
24
else
if
(ws == 0 &&
25
!compareAndSetWaitStatus(h, 0
, Node.PROPAGATE))
26
continue
;
//
loop on failed CAS
27
}
28
//同步队列的头节点未发生变化,跳出唤醒的动作
29
if
(h == head)
//
loop if head changed
30
break
;
31
}
32
}
这个方法可能是共享锁中最难理解的一个方法了,粗略地读完上面的注释,你是否会有以下几个问题呢?
Q1、什么时候会调用doReleaseShared()方法呢?
翻了下AQS源码我们发现有两个地方调用:
1、在获取共享锁acquireShared()方法中,满足一定的条件下(共享锁还可以被同步队列中的其他线程获取情况下),可以调用; 。
2、在释放共享锁releaseShared()方法中,释放成功一定会被调用; 。
Q2、谁会调用调用doReleaseShared()方法呢?
在通过 上一篇 文章我们了解到,独占锁中,只有获取了锁的线程才能调用release释放锁,因此调用unparkSuccessor(h)唤醒后继节点的必然是持有锁的线程,该线程可看做是当前的头节点(虽然在setHead方法中已经将头节点的thread属性设为了null,但是这个头节点曾经代表的就是这个线程); 。
而共享锁中,持有锁的线程可以有多个,这些线程都可以调用releaseShared()方法释放锁;假如这些线程都是从同步队列出队获取共享锁的,那么他们必然曾经成为过head或者现在就是head,如果是reReaseShared()中的方法调用doReleaseShared()方法,那么可能现在调用此方法的线程,已经不是同步队列头节点所代表的线程了,头节点可能被易主好多次了.
Q3、调用该doReleaseShared()方法的目的是什么呢,何时结束这个唤醒操作呢?
无论是在acquireShared()调用,还是在releaseShared()方法中调用,其目的就是在共享锁是可用的状态,唤醒头节点的后继有效节点,竞争共享锁。但是共享锁和独占锁的一个重要区别是:共享锁在头节点发生变化时(说明后继节点已经成功获取了共享锁,并执行了setHead()方法,将其设置为head),会再执行一次自旋唤醒新的头节点的后继节点,去竞争共享锁.
上面的话是什么意思呢?换句话说:就是当前线程完成后继节点唤醒任务,需要退出的时候,检查了一下头节点,唤醒的这个节点已经是新的头节点了(这个唤醒的节点也成功获取到了共享锁),那么它的后继节点是有资格竞争共享锁的,所以需要继续唤醒它的后续节点,周而复始,直到h == head不再执行后续节点唤醒.
Q4、什么时候才会发生满足h == head这个条件呢?
经过分析大概有以下这么几种情况(欢迎大家补充,有不对的地方还请大家指出):
1、阻塞队列为空,即阻塞队列中没有需要唤醒的节点,满足h == head这个条件.
2、线程A唤醒了后继线程B,但是线程B并没有获取到共享资源(线程B当然也就不会执行setHead()方法改变同步队列的head了),又发生了线程阻塞,不需要再唤醒后续的线程了,也满足h == head这个条件,那么B的后续线程的唤醒工作应该交给线程B获取资源时候在负责去唤醒吧.
3、线程A唤醒了线程B,线程B成功获取了资源,还是还未执行到setHead()这个方法;当前线程A,此时判断h == head也成立了,其调用的doReleaseShared()方法结束了,那么将唤醒线程B后续节点的工作,就应该交给刚刚被唤醒的线程B去执行了.
Q5、ws == 0这个状态怎么理解?什么情况下才会出现ws == 0这个状态呢?
其实我们仔细分析之前的代码我们可以得出以下结论: head的后继节点已经被唤醒或者即将被唤醒,分以下几种情况:
1、 有线程A刚释放了锁,刚执行了unparkSuccessor里的if (ws < 0) compareAndSetWaitStatus(node, ws, 0);把head的状态设置为了0,然后尝试唤醒head后继线程B,这里也分3种情况:
(1)、执行了if (ws < 0) compareAndSetWaitStatus(node, ws, 0),还没有执行LockSupport(this)方法( 后继节点中的线程即将被唤醒 ); 。
(2)、head后继线程B获取锁成功,直到head后继线程将自己设置为AQS的新head的这段时间里,head的状态为0( 后继节点中的线程已经被唤醒 ),
(3)、head后继线程B获取锁失败,直到将head重置为Node.SIGNAL这段时间里,这个head的状态也是为0的( 后继节点中的线程已经被唤醒 ); 。
2、同步队列中只有一个head == tail 的dummy node节点,它的状态为0; 。
3、在第2中情况上更进一步,同步队列中只有一个head == tail 的dummy node节点,它的状态为0,此时有个线程A获取共享锁失败了,但是只进行了入队操作,还未执行shouldParkAfterFailedAcquire()方法,未将head节点设置未Node.SIGNAL状态,这段时间head的状态也0( 后继节点中的线程即将被唤醒 ); 。
综上所述,我们不难看出,其实head.waitStatus == 0 这个状态是一个中间状态,可能会很快改变。后继节点获取共享锁失败了,head节点不会发生变化,只不过很快会将head.waitStatus 设置未Node.SIGNAL;后继结点获取共享锁成功,后继节点会被设置为新的head,假如后继节点不是尾节点,那么必定新的head.waitSatus == Node.SIGNAL,如果后继节点是尾节点,那么必定head.waitStatus == 0,因为没有后续入队节点将它的状态置为Node.SIGNAL.
Q6、什么时候会出现 ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)为true的情形呢?
读过上面的Q5中的分析,我们可以发现这个条件成立可能有两种状况:
1)、第一种可能的情况是:有个线程A执行到doReleaseShared()方法,线程B获取共享锁的时候在同步队列中阻塞的,此时有个线程C也执行了doReleaseShared()方法,doReleaseShared()方法调用unparkSuccessor()方法,设置head的ws == 0,此时线程A正好执行到了这个ws == 0的位置,而此时线程B获取共享锁失败,执行shouldParkAfterFailedAcquire()方法,又设置head的ws = Node.SIGNAL,恰好线程A执行到compareAndSetWaitStatus(h,0,Node.PROPAGETE)为false,
2)、第二种可能的情况是:同步队列中head头节点是刚刚成为头节点的,它的waitStatus值还为0,尾节点是在这之后刚刚加进来的。这种怎么理解呢?同步队列中的“旧的尾节点”状态是0,用线程A表示它,此时线程A刚刚获取到共享锁,将自己设置为头结点head节点,此时有个线程B获取共享锁失败,将自己加入到同步队列中,此时线程B还未执行shouldParkAfterFailedAcquire()方法,改变同步队列头结点head的状态;此时线程A执行了doReleaseShared()中的方法,发现ws == 0,但是恰好就在此时,线程B执行了 。
shouldParkAfterFailedAcquire()方法,设置head的waitStatus == Node.SIGNAL,紧接着线程A执行执行到compareAndSetWaitStatus(h,0,Node.PROPAGATE)失败了,继续continue进入下次“自旋”.
由此可见,doReleaseShared()方法中else if 这个分支的 && 连接了两个不一致的状态,分别对应了 shouldParkAfterFailedAcquire 的 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 执行成功前和执行成功后,因为 doReleaseShared 和 shouldParkAfterFailedAcquire 是可以并发执行的,所以这一条件是有可能满足的,只是满足的条件非常严苛,可能只是一瞬间的事.
至于共享锁的释放逻辑,相信看完上面的分析,再去看也不是什么难事,这里就不再赘述了.
总结:
鉴于水平有限就只能分析到如此了,如有说的不对的地方,还请大家批评指正,共同交流,共同进步.
参考文章地址:
1、 https://www.cnblogs.com/waterystone/p/4920797.html 。
2、 https://blog.csdn.net/anlian523/article/details/106319294 。
3、 https://segmentfault.com/a/1190000016447307 。
4、 https://www.cnblogs.com/micrari/p/6937995.html 。
最后此篇关于(原创)多线程并发:AQS源码分析(2)——共享锁的实现原理的文章就讲到这里了,如果你想了解更多关于(原创)多线程并发:AQS源码分析(2)——共享锁的实现原理的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我正在尝试在多线程环境中实现某种累积逻辑;我想知道没有 lock 和 synchronized 关键字是否有更好/更快的方法来做到这一点?以下是我当前的代码: public class Concurr
我需要帮助构建一个实现信号量的监视器,简单的 C 示例就可以。 这是为了证明可以在任何可以使用信号量的地方使用监视器。 最佳答案 如果您说允许使用互斥锁/condvars,请检查: #include
我已经构建了一些返回部分产品目录的 ajax,并且我正在尝试将 xml 输出到文档中,到目前为止,这是我所拥有的: $("#catalog").append("Item NamePriceDe
很抱歉,如果我的问题之前已经被问过,或者它太明显了,但我真的需要澄清这一点。感谢您的帮助。 在多用户界面中,如果来自不同用户的相同事务同时到达服务器,会发生什么? 我有下一张表: create tab
这可能是一个愚蠢的问题,但是这个程序的输出(它的方式)可以为零吗? public class Test2{ int a = 0; AtomicInteger b = new Atomi
假设我本地主机上的一个网站处理每个请求大约需要 3 秒。这很好,正如预期的那样(因为它在幕后进行了一些奇特的网络)。 但是,如果我在选项卡(在 firefox 中)中打开相同的 url,然后同时重新加
我对 MongoDB 的读锁定有点困惑。单个集合可以支持多少个并发读取操作? 最佳答案 如 tk 给出的链接中所写:http://www.mongodb.org/pages/viewpage.acti
如果有四个并发的 CUDA 应用程序在一个 GPU 中竞争资源会发生什么这样他们就可以将工作卸载到图形卡上了? Cuda Programming Guide 3.1 提到那里 某些方法是异步的: 内核
👊上次的百度面试遇到了关于spark的并发数的问题,今天我们就来将这些问题都一并解决一下,图画的的有点丑,还行大家见谅,百度实习的问题我放在了下面的链接👇: 链接: 2022百度大数据开发工程师实
我对 Groovy 线程有疑问。 我的任务是以某种方式翻译给定目录中的每个文件 并将生成的输出放在其他目录中的文件中。 我编写了以下代码,该代码有效: static def translateDir(
Java中的同步和锁定有什么区别? 最佳答案 synchronized是语言关键字;锁是对象。 当一个方法或代码块被标记为同步时,您是说该方法或代码块必须先获得某个锁对象(可以在同步的语法中指定)才能
我需要创建一个能够同时处理来自客户端的多个请求的并发 RPC 服务器。 使用 rpcgen linux编译器(基于sun RPC),不支持-A为并发服务器创建 stub 的选项。 (-A 选项在 so
System.out.println("Enter the number of what you would like to do"); System.out.println("1 = Manuall
我正在将我的应用程序移植到 iOS 8.0 并注意到 UIAlertView 已被弃用。 所以我改变了使用 UIAlertController 的方法。这在大多数情况下都有效。 除了,当我的应用程序打
我正在逐行同时读取两个文本文件。 我特别想做的是当lineCount在每个线程上都是相同的我想看看扫描仪当前正在读取的字符串。 我环顾四周寻找可以实现的某些模式,例如 Compare and Swap
我正在阅读 Java Concurrency in Practice .在章节中断政策部分 取消和关闭 它提到 A task should not assume anything about the
我正在尝试学习线程,互斥等的基础知识。遵循here的文档和示例。在下面的代码中,我得到预期的输出。问题: 想确认我是否有任何陷阱?我们如何改善下面的代码? 我的线程在哪一行尝试获取互斥锁或正在等待互斥
并发是指两个任务在不同的线程上并行运行。但是,异步方法并行运行,但在同一个线程上。这是如何实现的?另外,并行性怎么样? 这三个概念有什么区别? 最佳答案 并发和并行实际上与您正确推测的原理相同,两者都
以此ConcurrentDouble类定义为例: public class ConcurrentDouble { public double num = 0; public void subt
在得知并发确实增加了许多人的吞吐量后,我一直计划在项目中使用并发。现在我在多线程或并发方面还没有做太多工作,因此决定在实际项目中使用它之前学习并进行简单的概念验证。 以下是我尝试过的两个示例: 1.
我是一名优秀的程序员,十分优秀!