AQS源码解读(五)——从acquireShared探索共享锁实现原理,何为共享?如何共享(1)

本次面试答案,以及收集到的大厂必问面试题分享:

字节跳动超高难度三面java程序员面经,大厂的面试都这么变态吗?

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

更多JUC源码解读系列文章请持续关注JUC源码解读文章目录JDK8


文章目录

    • 一、前言
  • 二、共享模式获取锁

    • 1、doAcquireShared入队列,自旋,阻塞
    • (1)setHeadAndPropagate传播唤醒后继
  • (2)判断是否应该阻塞和阻塞线程

  • 三、共享模式可中断获取锁

    • doAcquireSharedInterruptibly入队列,自旋,阻塞,响应中断
  • 四、共享模式可超时获取锁

    • doAcquireSharedNanos入队列,自旋,阻塞,自动唤醒,响应中断
  • 五、共享模式释放锁

    • doReleaseShared唤醒后继
  • 六、总结

一、前言


何为共享锁?共享锁就是多个线程可以共享一把锁,如ReentrantReadWriteLockReadLock是共享锁,Semaphore是共享锁,CountDownLatch是共享锁,且这三个都是基于AQS实现的。

在AQS中共享锁和独占锁一样,也实现了一套通用的模板,子类只需要实现如何获取锁(tryAcquireShared),如何释放锁(tryReleaseShared)的逻辑。

二、共享模式获取锁


共享模式获取锁的过程和独占锁非常类似,都是先获取锁,失败之后进入同步队列操作。tryAcquireShared在AQS中没有给出具体实现,在子类ReentrantReadWriteLock.ReadLockSemaphoreCountDownLatch中相应实现了获取共享锁的逻辑。

tryAcquireShared返回值小于0,表示获取共享锁失败,从而进入同步队列操作doAcquireShared

public final void acquireShared(int arg) {

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

if (tryAcquireShared(arg) < 0)

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

doAcquireShared(arg);

}

1、doAcquireShared入队列,自旋,阻塞

doAcquireShared的逻辑和独占锁acquireQueued非常类似,入队列、自旋、阻塞等。基本流程如下:

  1. 新建共享节点node,并入队列(addWaiter(Node.SHARED))。

  2. 自旋判断新节点node前驱是否是head,是则获取锁(tryAcquireShared),获取锁成功并传播唤醒后继(setHeadAndPropagate)。

  3. 新节点node前驱不是head,则判断是否应该阻塞shouldParkAfterFailedAcquire

  4. 判断应该阻塞,则阻塞当前线程parkAndCheckInterrupt

  5. 被其他线程唤醒,或中断导致唤醒,则自旋重复2、3、4、5。

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

}

}

(1)setHeadAndPropagate传播唤醒后继

自旋判断新节点node前驱是head,且尝试获取共享锁成功,则需要传播唤醒共享后继节点。基本流程如下:

  1. node出队列,并成为新的head。

  2. 判断是否满足传播条件。

  3. 如若node还有后继且后继是共享节点,则执行唤醒操作doReleaseShared

(共享锁的传播模式比较复杂,如需深入了解,请阅读拙作《AQS源码解读(七)——从PROPAGATE和setHeadAndPropagate()分析共享锁的传播性》

private void setHeadAndPropagate(Node node, int propagate) {

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

//设置node为新head

setHead(node);

//一连串判断满足后,唤醒共享后继

if (propagate > 0 || h == null || h.waitStatus < 0 ||

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

//唤醒后继共享节点

Node s = node.next;

if (s == null || s.isShared())

doReleaseShared();

}

}

(2)判断是否应该阻塞和阻塞线程

共享锁判断判断是否应该阻塞和阻塞线程的代码和独占锁的完全一样,不过多赘述。shouldParkAfterFailedAcquire主要做了如下几步:

  1. 判断node前驱状态是否为SIGNAL,是则直接返回true。

  2. node前驱状态不是SIGNAL,有可能是ws>0,说明前驱取消了,自旋跳过取消的节点,并寻找链接一个正常的前驱。

  3. node前驱状态不是SIGNAL,有可能是0(初始化状态)或PROPAGATE(传播状态),修改node前驱状态为SIGNAL

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;

if (ws == Node.SIGNAL)

/*

  • This node has already set status asking a release

  • to signal it, so it can safely park.

  • node拿锁失败,前继节点的状态是SIGNAL,node节点可以放心的阻塞,

  • 因为下次会被唤醒

*/

return true;

if (ws > 0) {

/*

  • Predecessor was cancelled. Skip over predecessors and

  • indicate retry.

  • pred节点被取消了,跳过pred

*/

do {

//pred = pred.prev;

//node.pred = pred;

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

//跳过取消节点,给node找一个正常的前驱,然后再循环一次

pred.next = node;

} else {

/* 0 -3

  • waitStatus must be 0 or PROPAGATE. Indicate that we

  • need a signal, but don’t park yet. Caller will need to

  • retry to make sure it cannot acquire before parking.

*/

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

shouldParkAfterFailedAcquire判断返回true,则调用parkAndCheckInterrupt阻塞当前线程。

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this);

return Thread.interrupted();

}

三、共享模式可中断获取锁


acquireSharedInterruptibly在代码结构上与doAcquireShared类似,不同之处在于doAcquireShared不响应中断,acquireSharedInterruptibly响应中断。

public final void acquireSharedInterruptibly(int arg)

throws InterruptedException {

if (Thread.interrupted())

//被打断 抛出异常

throw new InterruptedException();

if (tryAcquireShared(arg) < 0)

//获取锁失败

doAcquireSharedInterruptibly(arg);

}

doAcquireSharedInterruptibly入队列,自旋,阻塞,响应中断

doAcquireSharedInterruptibly逻辑上也和doAcquireShared类似,但是doAcquireSharedInterruptibly在线程阻塞期间,被中断导致唤醒,将抛出异常InterruptedException,响应中断,此时failed=truefinally块中会执行取消节点的操作,取消过程和独占锁一样。

private void doAcquireSharedInterruptibly(int arg)

throws InterruptedException {

//新建节点入队列

final Node node = addWaiter(Node.SHARED);

boolean failed = true;

try {

for (;😉 {

//node节点的前驱节点是head,获取锁

final Node p = node.predecessor();

if (p == head) {

int r = tryAcquireShared(arg);

if (r >= 0) {

setHeadAndPropagate(node, r);

p.next = null; // help GC

failed = false;

return;

}

}

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

//在阻塞期间因为中断而唤醒,抛异常

throw new InterruptedException();

}

} finally {

if (failed)

cancelAcquire(node);

}

}

四、共享模式可超时获取锁


独占锁的可超时获取锁类似,共享模式超时获取锁,不仅可以响应中断,还可以将线程阻塞一段时间,自动唤醒。tryAcquireSharedNanos可传入一个纳秒单位的时间nanosTimeout,可超时的逻辑在doAcquireSharedNanos中。

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)

throws InterruptedException {

if (Thread.interrupted())

throw new InterruptedException();

return tryAcquireShared(arg) >= 0 ||

doAcquireSharedNanos(arg, nanosTimeout);

}

doAcquireSharedNanos入队列,自旋,阻塞,自动唤醒,响应中断

doAcquireSharedNanosdoAcquireShared逻辑类似,但是不仅可以响应中断,同时还可以让线程阻塞一段时间自动唤醒,如果超时了还没获取锁则返回false。

doAcquireSharedNanos还有一个非常不同之处,就是即使shouldParkAfterFailedAcquire判断应该阻塞了,也有可能不阻塞,还会再自旋一段时间,这个自旋的时长有一个阈值spinForTimeoutThreshold = 1000L,1000纳秒,自旋了1000纳秒后还没有获取锁,且此时也判断应该阻塞了,就让线程休眠一段时间。

线程唤醒,有可能是自动唤醒,有可能是被其他释放锁的线程唤醒,还有可能是线程中断导致唤醒,如果是线程中断唤醒,则需要响应中断,抛出异常InterruptedException;如果没有中断,则继续循环刚才的流程(判断前驱是否是head,判断是否超时,判断是否需要阻塞等)。

private boolean doAcquireSharedNanos(int arg, long nanosTimeout)

Kafka进阶篇知识点

image

Kafka高级篇知识点

image

44个Kafka知识点(基础+进阶+高级)解析如下

image

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

te boolean doAcquireSharedNanos(int arg, long nanosTimeout)

Kafka进阶篇知识点

[外链图片转存中…(img-eeb31wZd-1715811259510)]

Kafka高级篇知识点

[外链图片转存中…(img-AfcQ9eJJ-1715811259510)]

44个Kafka知识点(基础+进阶+高级)解析如下

[外链图片转存中…(img-JUVd5rz4-1715811259511)]

由于篇幅有限,小编已将上面介绍的**《Kafka源码解析与实战》、Kafka面试专题解析、复习学习必备44个Kafka知识点(基础+进阶+高级)都整理成册,全部都是PDF文档**

本文已被CODING开源项目:【一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码】收录

需要这份系统化的资料的朋友,可以点击这里获取

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值