首页 资讯时装美容情感健康百科

propagate 面试官问我AQS中的PROPAGATE有什么用

2022-01-11 10:38

“自己写,告诉别人。你好,这是think123的第81篇原创文章。”

我之前分析过AQS的源代码,但是只分析了排他锁的原理。

我们可以使用信号量来分析共享锁。

如何使用信号量

公共类SemaphoreDemo {

公共静态void main {

//应用的共享锁的数量

信号量sp =新信号量;

for-> { 0

尝试{

//获取共享锁

sp.acquire

string thread name = thread . currentthread . getname;

//访问API

system . out . println);

时间单位。睡眠;

//释放共享锁

sp.release

system . out . println);

}捕获{

e.printStackTrace

}

},“thread-+)。开始;

}

}

}

锁是在Java SDK中提供的,那么为什么要提供信号量呢?实际上,实现互斥锁只是信号量功能的一部分。信号量的另一个功能是锁不容易实现,也就是说,信号量可以允许多个线程访问一个关键区域。

常见的需求是共享资源,如连接池、对象池和线程池。其中,您可能最熟悉数据库连接池。同时,必须允许多个线程同时使用连接池。当然,每个连接在被释放之前都不允许被其他线程使用。

例如,上面的代码演示了只允许三个线程同时访问API。

AQS如何实现信号量

抽象静态类Sync扩展了AbstractQueuedSynchronizer {

同步{

setState

}

//打开锁

final int nonairtryacquire shared {

对于{ 0

int available = getState

int剩余=可用-获取;

if)

返回剩余;

}

}

//松开锁

受保护的最终布尔型tryReleaseShared {

对于{ 0

int current = getState

int next =当前+发布;

如果;

if)

返回真;

}

}

}

信号量的获取/释放仍然使用AQS,它将状态值作为共享资源的数量。获取锁时,状态值减1,释放锁时,状态值加1。

获取锁

public void acquire引发中断异常{

sync.acquireSharedInterruptibly

}

// AQS

公共最终无效acquireSharedInterruptibly

引发中断异常{

if)

引发新的中断异常;

如果

doAcquireSharedInterruptibly

}

信号量也有两种实现:公平锁和不公平锁,但是这两种都是由AQS实现的。这里的默认实现是不公平锁,所以最终将调用nonfairTryAcquireShared方法。

开锁

公开作废发布{

sync.releaseShared

}

// AQS

public final boolean release shared {

if){ 0

doReleaseShared

返回真;

}

返回false

}

锁释放成功后,会调用doReleaseShared,后面会分析这个方法。

未能锁定

当锁获取失败时,新线程将被添加到队列中

公共最终无效acquireSharedInterruptibly

引发中断异常{

if)

引发新的中断异常;

//实际调用是nonairtry acquire shared in nonairsync

如果

doAcquireSharedInterruptibly

}

当锁的数量小于0时,您需要加入队列。

由共享锁调用的方法doAcquireSharedInterruptibly和由排他锁调用的方法acquireQueued之间只有一些细微的区别。

差异

exclusive锁构造的节点首先是模式是EXCLUSIVE的,而shared锁构造模式是SHARED的,这是通过AQS的next服务员变量来区分的。

其次,在准备加入队列时,如果获取共享锁的尝试成功,将调用setHeadAndPropagate方法重置头节点,并决定是否唤醒后续节点

private void setHeadAndPropagate {

//旧的头节点

节点h =头;

//将获得锁的节点设置为头节点

setHead

//如果还有很多锁,返回值)

//或者旧的头节点是空,或者头节点的ws小于0

//或者新头节点为空,或者新头节点的ws小于0,唤醒后续节点

if == null || h.waitStatus

节点s = node.next

if)

doReleaseShared

}

}

私有void setHead {

head =节点;

node.thread = null

node.prev = null

}

私人作废文件共享

对于{ 0

节点h =头;

//确保同步队列中至少有两个节点

如果{

int ws = h.waitStatus

//需要唤醒后续节点

如果{

if)

继续;//循环检查案例

取消标记后继者;

}

//更新要传播的节点状态

否则如果)

继续;//失败CAS上的循环

}

if //循环if头已更改

打破;

}

}

其实这里有些逻辑我是看不懂的,比如setHeadAndPropagate方法中的这个逻辑

if == null || h.waitStatus

这样写不好吗?

如果

这个判断和我的认知非常吻合。但是会造成什么样的问题呢?根据bug的描述,我们来纸上谈兵一下没有PROPAGATE状态会发生什么。

首先,信号量将状态值初始化为0,然后四个线程分别运行四个任务。线程T1和T2同时获取锁,另外两个线程T3和T3同时释放锁

公共类TestSemaphore {

//这里信号量设置为0

私有静态信号量sem =新信号量;

私有静态类Thread1扩展了Thread {

@覆盖

公共无效运行{

//打开锁

SEM . acquire不间断;

}

}

私有静态类Thread2扩展了Thread {

@覆盖

公共无效运行{

//松开锁

sem.release

}

}

公共静态void main引发中断异常{

对于;

线程t2 =新线程1;

线程t3 =新线程2;

线程t4 =新线程2;

t1.start

t2.start

t3.start

t4.start

t1.join

t2.join

t3 .加入;

t4.join

system . out . println;

}

}

}

根据上面的代码,我们将信号量设置为0,因此T1和T2将无法获取锁。

假设队列中某个周期的情况如下

head - > t1 - > t2

锁首先在t3释放,然后在t4释放

在时间1:线程t3调用releaseShared,然后唤醒队列中的节点。此时,磁头的状态从-1变为0

在时间2:线程t1被t3唤醒,因为线程t3释放了锁,然后通过nonfairTryAcquireShared获得传播值0

再去拿锁

在时间3:线程t4调用releaseShared,读取时waitStatue为0,不满足条件,所以没有唤醒后续节点

差速器

在时间4:线程t1成功获取锁,并调用setHeadAndPropagate,因为propagate > 0不满足,后续节点不会唤醒。

如果没有PROPAGATE状态,上述情况将导致线程t2不被唤醒。

引入propagate后,这个变量会发生什么变化?

在时间1:线程t3调用doReleaseShared,然后唤醒队列中的节点,此时head的状态从-1变为0

在时间2:线程t1被t3唤醒,因为t3释放了信号量,然后通过nonfairTryAcquireShared获得传播值0

在时间3:线程t4调用releaseShared,当它被读取时,waitStatue为0,节点状态设置为PROPAGATE

否则如果)

继续;//失败CAS上的循环

在时间4:线程t1成功获取了锁,并调用了setHeadAndPropagate。虽然不满足传播> 0,但不满足等待状态。

至此,我们知道了PROPAGATE的作用,就是避免线程无法唤醒的窘境。由于共享锁时会有很多线程获取或释放锁,所以有些方法是并发执行的,会产生很多中间状态,而PROPAGATE就是让这些中间状态不影响程序的正常运行。

小方法和大智慧

无论是释放锁还是申请锁,都将调用doReleaseShared方法。这个方法看似简单,但里面的逻辑还是很微妙的。

私人作废文件共享

对于{ 0

节点h =头;

//确保同步队列中至少有两个节点

如果{

int ws = h.waitStatus

//需要唤醒后续节点

如果{

//可能还有其他线程在调用doreleaseshared,解标记操作只需要一次调用。

if)

继续;//循环检查案例

取消标记后继者;

}

//将节点状态设置为“传播”

否则如果)

继续;//失败CAS上的循环

}

if //循环if头已更改

打破;

}

}

有一个判断条件

ws == 0 &&!比较网络状态

这个if条件也是巧妙的

首先,队列中至少有两个节点。为了简化分析,我们认为它只有两个节点,head - > node

执行到else if意味着跳过前面的if条件,这意味着头节点刚刚成为头节点,其waitStatus为0,尾节点添加在其后。出现这种情况是因为上一个节点的ws值没有在shouldParkAfterFailedAcquire中更改为SIGNAL。

CAS失败表示此时头节点的ws不是0,这意味着shouldParkAfterFailedAcquire已将前一个节点的waitStatus值更改为SIGNAL

更新以前的节点状态

而整个循环的退出条件是h==head,这是为什么呢?

由于我们的头节点是一个虚拟节点,因此假设同步队列中节点的顺序如下:

头部->甲->乙->丙

现在假设A获得了共享锁,那么它将成为新的虚拟节点。

头部- > B - > C

此时,线程a将调用doReleaseShared方法唤醒后继节点b,后者将很快获得锁并成为新的头节点

头部- > C

此时,线程b也会调用这个方法并唤醒它的后继节点c,但是当线程b调用时,线程a可能还没有运行完,也正在执行这个方法。当它执行到h==head时,它发现head已经改变,所以for循环不会退出,它将继续执行for循环并唤醒后续节点。

至此,我们已经完成了对共享锁的分析。事实上,只要我们理解AQS的逻辑,依靠AQS实现的Semaphore是非常简单的。

在查看共享锁源代码的过程中,特别需要注意的是,该方法会被多个线程并发执行,所以很多判断只会出现在多线程竞争的情况下。同时需要注意的是,共享锁不能保证线程安全,程序员还是需要保证共享资源的操作是安全的。

相关阅读
千万不要用线雕精华 雅诗兰黛线雕精华和小棕瓶哪个好?一起使用效果佳
雅诗兰黛这一时期最吸引眼球的就是这款新推出的线雕精华,之前只在日本上市,现在终于可以在中国买到了。提升肌肤,让肌肤拥有线雕般的紧致度,和烫手的小棕瓶精华哪个更适合你?雅诗兰黛线雕精华和小棕瓶哪个好在这种情况下,两者的效果是不同的。如果想达到瘦脸紧致的效果,可以选择雅诗兰黛线雕精华,适合皮肤松弛浮肿的人群。如果你想获得祛皱、抗衰老、补水的多重功效,那么小棕瓶是个不错的选择。小棕瓶的综合性更高,线雕精0在看 04-23
burberry男士香水 burberry情缘男士香水女士能用吗?男女皆宜的木质香调
巴宝莉是一个极具英国传统风格的奢侈品牌,其多层次的产品系列满足了不同年龄和性别消费者的需求。五号网下面的小系列,带你去看看巴宝莉爱心男士香水是否可以让女性使用。burberry情缘男士香水女士能用吗作为“女汉子”,她通常不喜欢太甜太香的香水,所以自然会排斥很多女性香水。经过不断的尝试,发现男士香水也可以有很大的成就。巴宝莉的爱情男士淡香水作为一种木质香水,具有所有令人愉悦的元素。在选择之初,它也被0在看 04-23
收缩毛孔的爽肤水 爽肤水可以收缩毛孔吗?推荐这几款爽肤水
许多调色剂不仅用于水合产品,还用于功能性调色剂。比如有的爽肤水可以美白,有的爽肤水可以去皱。那么,爽肤水能收缩毛孔吗?酒精类爽肤水可以收缩毛孔爽肤水可以收缩毛孔,但必须含有醇类的爽肤水才能收缩毛孔。但是含酒精的爽肤水会让皮肤产生依赖感,不适合长期使用。收敛水可以收缩毛孔对于那些名字叫收敛剂水或爽肤水的功能,明确说明这种爽肤水可以收敛毛孔,一般里面都有一些收敛成分,比如柑橘、薄荷、酒精等。比如叶澄博0在看 04-23
相宜本草去痘痘 油性皮肤用相宜本草的哪个系列好?油性皮肤可以用相宜本草吗?
适本草在上海发展壮大了,虽然不如韩国的大品牌。然而,作为我们当地的化妆品,合适的本草仍然受到许多女性的喜爱。适用本草系列很明确,适合我们中国姑娘。适合的《本草纲目》有哪些系列特点?油性皮肤用相宜本草的哪个好?仙人掌系列适合缺水油性肌肤,此系列为清爽无油配方,也适合夏季干性肌肤。添加了仙人掌和芦荟的成分,非常保湿清爽,能改善干燥缺水造成的干纹和细纹,有防辐射作用。红石榴系列适合暗沉肌肤,添加红石榴和0在看 04-22
画眼线的技巧 怎样减少画眼线的伤害?新手画眼线的技巧图解
在我们的日常生活中,很多人经常化妆,眼线是我们化妆时经常采取的一个步骤。很多人担心化妆品会对眼睛造成伤害,那么如何减少眼线的伤害呢?新手画眼线技巧图解。怎样减少画眼线的伤害定期更换眼线未开封的眼线保质期一般为2-3年,最好使用不超过3个月的未开封眼线,这样可以减少眼线上细菌感染眼睛的几率。不要交互使用眼线眼线要个人使用,不能同时使用,以免浪费很多人,容易造成交叉感染。彻底化妆眼睛周围的皮肤比较特殊0在看 04-22

热文排行