AQS源码解读(六)——从PROPAGATE和setHeadAndPropagate()分析共享锁的传播性

共享锁是多个线程可以共享一把锁,如ReentrantReadWriteLockReadLock是共享锁,Semaphore是共享锁,CountDownLatch是共享锁,且这三个都是基于AQS实现的。与之相对的就是独占锁,ReentrantLockReentrantReadWriteLockWriteLock都是独占锁,独占锁也称为互斥锁,表示一把锁只能有一个线程持有。所谓,读读共享,读写互斥,写写互斥。

AQS中分别用Node SHARED = new Node()表示共享模式,Node EXCLUSIVE = null表示独占模式。

static final class Node {

/** Marker to indicate a node is waiting in shared mode */

//共享一个节点对象

static final Node SHARED = new Node();

/** Marker to indicate a node is waiting in exclusive mode */

//独占节点

static final Node EXCLUSIVE = null;

//condition中记录下一个节点,Lock中记录当前的node是独占node还是共享node

Node nextWaiter;

/**

  • Returns true if node is waiting in shared mode.

*/

final boolean isShared() {

return nextWaiter == SHARED;

}

}

三、acquireShared获取共享锁


共享锁除了可以多个线程共享外,在共享节点间还具有传播性。何为传播性,先看共享锁获取锁的代码:

//java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireShared

public final void acquireShared(int arg) {

//tryAcquireShared 返回-1获取锁失败,返回值大于1或者0获取锁成功

if (tryAcquireShared(arg) < 0)

//获取锁失败,进入队列操作

doAcquireShared(arg);

}

tryAcquireSharedReentrantReadWriteLock中的实现,返回值只有两种,1表示获取锁成功,-1表示获取锁失败。在SemaphoretryAcquireShared的返回值代表资源剩余量,返回值大于等于0表示获取锁成功,小于0表示获取锁失败。

四、doAcquireShared进入同步队列操作


tryAcquireShared获取锁失败后,进入AQS同步队列操作doAcquireShared。创建共享节点node,并CAS排到队列尾部,接下来判断是应该阻塞还是继续获取锁。当node的前驱节点是head时,尝试获取锁tryAcquireShared,如果获取锁成功返回值r >= 0,则执行函数setHeadAndPropagate,这个函数就是共享锁的传播性。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#doAcquireShared

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) {

//如果前继节点是head,则尝试获取锁

int r = tryAcquireShared(arg);

if (r >= 0) {

//获取锁成功,设置新head和共享传播(唤醒后继共享节点)

setHeadAndPropagate(node, r);

p.next = null; // help GC

if (interrupted)

selfInterrupt();

failed = false;

return;

}

}

/**

  • p不是头结点 or 获取锁失败,判断是否应该被阻塞

  • 前继节点的ws = SIGNAL 时应该被阻塞

*/

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

五、setHeadAndPropagate共享锁的传播性


node获取锁成功出队,设置新head,并将共享性传播给后继节点,即唤醒后继共享节点。为什么当一个节点的线程获取共享锁后,要唤醒后继共享节点?共享锁是可以多个线程共有的,当一个节点的线程获取共享锁后,必然要通知后继共享节点的线程,也可以获取锁了,这样就不会让其他等待的线程等很久,而传播性的目的也是尽快通知其他等待的线程尽快获取锁。

private void setHeadAndPropagate(Node node, int propagate) {

Node h = head; // Record old head for check below

//设置node为新head

setHead(node);

/*

  • 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.

*/

// propagate > 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();

}

}

setHeadAndPropagate中调用doReleaseShared前需要一连串的条件判断,大概可以分为三部分:

1. propagate > 0

ReentrantReadWriteLock中走到setHeadAndPropagate,只可能是propagate > 0,所以后面判断旧、新head的逻辑就被短路了。

而在Semaphore中走到setHeadAndPropagatepropagate是可以等于0的,表示没有剩余资源了,故propagate > 0不满足,往后判断。

2. h == null || h.waitStatus < 0

首先判断旧head是否为null,一般情况下是不可能是等于null,除非旧head刚好被gc了。h == null不满足,继续判断h.waitStatus < 0h.waitStatus可能等于0,可能等于-3。

  • h.waitStatus=0的情况,某个线程释放了锁(release or releaseShared)或者前一个节点获取共享锁传播setHeadAndPropagate,唤醒后继节点的时候将h.waitStatus=-1设置为0。

  • h.waitStatus=-3doReleaseShared唤醒head后继节点后h.waitStatus从-1到0,还没来得及更新head,即被唤醒的共享节点还没有setHeadAndPropagate,又有其他线程doReleaseShared唤醒head后继节点h.waitStatus从0到-3。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#unparkSuccessor

private void unparkSuccessor(Node node) {

int ws = node.waitStatus;

if (ws < 0)

//cas设置h.waitStatus -1 --> 0

compareAndSetWaitStatus(node, ws, 0);

//唤醒后继节点的线程,若为空or取消了,从tail往后遍历找到一个正常的节点

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

//uppark线程

LockSupport.unpark(s.thread);

}

当释放共享锁or共享锁传播后会调用doReleaseShared唤醒同步队列中head的后继节点。

首先明确几个判断:

  • h.waitStatus = Node.SIGNALcompareAndSetWaitStatus(h, Node.SIGNAL, 0))unparkSuccessor

  • h.waitStatus = 0compareAndSetWaitStatus(h, 0, Node.PROPAGATE)设置head为传播模式。

  • h == head,head没有变,break中断循环;也可能被唤醒的节点立刻获取了锁出队列,导致head变了,所以继续循环唤醒head后继节点。

//java.util.concurrent.locks.AbstractQueuedSynchronizer#doReleaseShared

private void doReleaseShared() {

for (;😉 {

Node h = head;

if (h != null && h != tail) {

int ws = h.waitStatus;

if (ws == Node.SIGNAL) {

//SIGNAL --> 0

if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))

continue; // loop to recheck cases

//唤醒后继节点的线程

unparkSuccessor(h);

}

else if (ws == 0 &&

//0 --> PROPAGATE

!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

continue; // loop on failed CAS

}

/**

  • we must loop in case a new node is added

  • while we are doing this

*/

if (h == head) // loop if head changed

//head没有变则break

break;

}

}

3. (h = head) == null || h.waitStatus < 0

首先判断新head是否为空,一般情况下新head不为空,(h = head) == null不满足,判断h.waitStatus < 0h.waitStatus可能等于0,可能小于0(-3 or -1)。

  • h.waitStatus可能等于0的情况,后继节点刚好入队列,还没有走到shouldParkAfterFailedAcquire()中的修改前继节点waitStatus的代码。

  • h.waitStatus=-3,上一个共享节点被唤醒后,成为新head,后继节点刚入队列,又有其他线程释放锁调用doReleaseSharedh.waitStatus从0改为-3。

  • h.waitStatus=-1,已经调用了shouldParkAfterFailedAcquire()h.waitStatus从0 or -3 改为-1,可能阻塞,可能未阻塞。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

大型分布式系统犹如一个生命,系统中各个服务犹如骨骼,其中的数据犹如血液,而Kafka犹如经络,串联整个系统。这份Kafka源码笔记通过大量的设计图展示、代码分析、示例分享,把Kafka的实现脉络展示在读者面前,帮助读者更好地研读Kafka代码。

麻烦帮忙转发一下这篇文章+关注我

就这一次!拼多多内部架构师培训Kafka源码笔记(现已绝版)

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
**

img

总结

大型分布式系统犹如一个生命,系统中各个服务犹如骨骼,其中的数据犹如血液,而Kafka犹如经络,串联整个系统。这份Kafka源码笔记通过大量的设计图展示、代码分析、示例分享,把Kafka的实现脉络展示在读者面前,帮助读者更好地研读Kafka代码。

麻烦帮忙转发一下这篇文章+关注我

[外链图片转存中…(img-qMUH2ZKP-1712092757889)]

《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 28
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值