propagate 面试官问我AQS中的PROPAGATE有什么用
“自己写,告诉别人。你好,这是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是非常简单的。
在查看共享锁源代码的过程中,特别需要注意的是,该方法会被多个线程并发执行,所以很多判断只会出现在多线程竞争的情况下。同时需要注意的是,共享锁不能保证线程安全,程序员还是需要保证共享资源的操作是安全的。