AQS源码解读(二)——从acquireQueued探索独占锁实现原理,如何阻塞?如何唤醒?

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

//pred = pred.prev;

//node.pred = pred;

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

pred.next = node;

//跳过取消节点,给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;

}

3、parkAndCheckInterrupt阻塞线程

shouldParkAfterFailedAcquire判断应该阻塞线程,则调用parkAndCheckInterrupt,其内部调用LockSupport.park(this)阻塞当前线程。LockSupportUNSAFE中的parkunpark进行了封装,其能精准阻塞一个线程,也能精准唤醒一个线程(不同于waitnotify)。阻塞唤醒会导致线程进行上下文切换。

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this);

return Thread.interrupted();

}

三、独占模式可中断获取锁


可中断获取锁acquireInterruptibly和不可中断获取锁acquire逻辑类似,区别在于acquireInterruptibly可响应InterruptedException中断异常(外部调用Thread#interrupt,就可能会导致中断)。

public final void acquireInterruptibly(int arg)

throws InterruptedException {

if (Thread.interrupted())

//有被中断 抛异常

throw new InterruptedException();

if (!tryAcquire(arg))

doAcquireInterruptibly(arg);

}

doAcquireInterruptibly阻塞线程并响应中断

doAcquireInterruptibly进入队列以后的逻辑也与acquireQueued差不多,区别也是在于doAcquireInterruptibly可响应InterruptedException中断异常。

private void doAcquireInterruptibly(int arg)

throws InterruptedException {

//新建node,入队列

final Node node = addWaiter(Node.EXCLUSIVE);

boolean failed = true;

try {

for (;😉 {

//判断前驱是否是head

final Node p = node.predecessor();

if (p == head && tryAcquire(arg)) {

setHead(node);

p.next = null; // help GC

failed = false;

return;

}

//1.应该阻塞,调用parkAndCheckInterrupt阻塞线程

//2.不应该阻塞,再给一次抢锁的机会

if (shouldParkAfterFailedAcquire(p, node) &&

parkAndCheckInterrupt())

//遇到中断抛了异常InterruptedException

throw new InterruptedException();

}

} finally {

if (failed)

//没有获取锁,被中断,取消节点

cancelAcquire(node);

}

}

四、独占模式可超时获取锁


可超时获取锁,不仅可以响应中断,还可以将线程阻塞一段时间,自动唤醒。tryAcquireNanos可传入一个纳秒单位的时间nanosTimeout,可超时的逻辑在doAcquireNanos中。

public final boolean tryAcquireNanos(int arg, long nanosTimeout)

throws InterruptedException {

if (Thread.interrupted())

//响应中断

throw new InterruptedException();

return tryAcquire(arg) ||

doAcquireNanos(arg, nanosTimeout);

}

doAcquireNanos阻塞线程自动唤醒响应中断

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

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

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

private boolean doAcquireNanos(int arg, long nanosTimeout)

throws InterruptedException {

if (nanosTimeout <= 0L)

//已经超时直接返回false,获取锁失败

return false;

//计算deadline

final long deadline = System.nanoTime() + nanosTimeout;

//入队列

final Node node = addWaiter(Node.EXCLUSIVE);

boolean failed = true;

try {

for (;😉 {

//判断前驱是否是head

final Node p = node.predecessor();

if (p == head && tryAcquire(arg)) {

setHead(node);

p.next = null; // help GC

failed = false;

return true;

}

nanosTimeout = deadline - System.nanoTime();

//超时返回false,获取锁失败

if (nanosTimeout <= 0L)

return false;

//1.应该阻塞,调用parkAndCheckInterrupt阻塞线程

//2.不应该阻塞,再给一次抢锁的机会

//3.自旋1000纳秒,还没有获取锁就休眠一段时间。1毫秒=110001000纳秒

if (shouldParkAfterFailedAcquire(p, node) &&

nanosTimeout > spinForTimeoutThreshold)

//阻塞一段时间

LockSupport.parkNanos(this, nanosTimeout);

if (Thread.interrupted())

//响应中断

throw new InterruptedException();

}

} finally {

if (failed)

cancelAcquire(node);

}

}

五、独占模式释放锁


释放锁的流程很简单,tryRelease需要子类实现,暂时不考虑,当tryRelease释放锁成功后唤醒后继节点。

public final boolean release(int arg) {

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)

//head不为空,head不是初始化,释放锁成功后唤醒后继节点

unparkSuccessor(h);

return true;

}

return false;

}

unparkSuccessor唤醒后继线程

唤醒后继的条件是h != null && h.waitStatus != 0,head不为null且head的状态不是初始状态,则唤醒后继。在独占模式下h.waitStatus可能等于0,-1:

  • h.waitStatus=0,线程释放锁,同步队列中的节点可能刚入队列,还没有阻塞,所以无需唤醒队列。

  • h.waitStatus=-1,head后继应该正常唤醒。

unparkSuccessor唤醒后继需要做如下两步:

  • h.waitStatus < 0node.waitStatus重置为0。

  • 唤醒后继节点,若后继节点为空或者被取消,则从tail向前找一个距离head最近的正常的节点唤醒。

private void unparkSuccessor(Node node) {

/*

  • If status is negative (i.e., possibly needing signal) try

  • to clear in anticipation of signalling. It is OK if this

  • fails or if status is changed by waiting thread.

*/

int ws = node.waitStatus;

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

/*

  • Thread to unpark is held in successor, which is normally

  • just the next node. But if cancelled or apparently null,

  • traverse backwards from tail to find the actual

  • non-cancelled successor.

*/

//唤醒后继节点的线程,若为空,从tail往后遍历找一个距离head最近的正常的节点

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)

//唤醒线程

LockSupport.unpark(s.thread);

}

node后继是null或者取消状态,为什么要从tail向前找,而不是直接从head向后找到第一个正常的节点就可以返回了?目的在于为了照顾刚入队列的节点。看入队列操作addWaiter

节点入队不是一个原子操作, 虽然用了compareAndSetTail操作保证了当前节点被设置成尾节点,但是只能保证,此时step1和step2是执行完成的,有可能在step3还没有来的及执行到的时候,有其他线程调用了unparkSuccessor方法,此时pred.next的值还没有被设置成node,所以从head往后遍历可能遍历不到尾节点,但是因为尾节点此时已经设置完成,node.prev = pred也被执行过了,所以如果从tail往前遍历,新加的尾节点就可以遍历到了,并且可以通过它一直往前找。

总结来说,之所以从tail往前遍历,是因为在多线程并发条件下,如果一个节点的next属性为null, 并不能保证它就是尾节点(可能是因为新加的尾节点还没来得及执行pred.next = node), 但是一个节点如果能入队, 则它的prev属性一定是有值的,所以反向查找一定是最精确的。(这里需要感谢这篇文章《逐行分析AQS源码(2)——独占锁的释放》,之前唤醒节点从tail往前找我理解错了,看了这位博主分析,才更理解了源码的逻辑。)

private Node addWaiter(Node mode) {

Node node = new Node(Thread.currentThread(), mode);

// Try the fast path of enq; backup to full enq on failure

Node pred = tail;

if (pred != null) {

//设置node节点的上一个节点是tail

node.prev = pred; //step1

//cas设置tail指针指向node

if (compareAndSetTail(pred, node)) {//step2

pred.next = node; //step3

//mode进入尾部成功,返回

return node;

}

}

enq(node);

return node;

}

六、取消获取锁cancelAcquire


什么时候可以取消获取锁呢?AQS并没有对外开放取消节点的操作权限,而是在可中断获取锁的时候,中断导致获取失败,则会被取消获取锁。

取消获取锁的条件是双向队列,一个节点可知前驱和后继,如单向的CLH队列锁就不能有取消的动作。而且取消这个动作还是相对复杂的,但是目的很简单,就是剔除队列中的取消节点。

  • 首先会将node的thread设置为空。

  • 其次检查node前驱是否是取消状态,是则循环跳过,一直为node找一个正常的前驱。

  • 接着node.waitStatus 设置为CANCELLED

  • 判断node是否在尾部,是则tail指针前移到node前驱上,node前驱成为新的tail,其next指针(predNext)设置为null。

  • 若node不是在尾部,判断其前驱是否是head以及是否是正常节点。node前驱不是head且正常节点,则将node后继链接到node前驱next指针(predNext)上(Node next = node.next;compareAndSetNext(pred, predNext, next);),从而使node被剔除。

  • 若node不是在尾部,且node前驱是head,则唤醒node的后继。node前驱不是head,但是不正常节点(刚好被取消的),则也唤醒node的后继,这时的唤醒不是为了让node后继获取锁,而是为node的后继链接一个正常的前驱(node后继自旋判断阻塞时shouldParkAfterFailedAcquire,会链接一个正常的前驱)。

若node的前驱是取消状态,在跳过取消节点,找到一个正确前驱链接给node,此时新找到的前驱pred.next不会指向node,所以pred.next就不是node。类似如下的三角关系。

取消节点,前驱也是取消状态

private void cancelAcquire(Node node) {

// Ignore if node doesn’t exist

if (node == null)

return;

node.thread = null;

// Skip cancelled predecessors

Node pred = node.prev;

while (pred.waitStatus > 0)

//pred = pred.prev;

//node.prev = pred;

node.prev = pred = pred.prev;

//如果node的前驱也是取消节点,则pred.next就不是node

Node predNext = pred.next;

node.waitStatus = Node.CANCELLED;

// If we are the tail, remove ourselves.

//如果node在尾部,tail前移

if (node == tail && compareAndSetTail(node, pred)) {

//node设置为null

compareAndSetNext(pred, predNext, null);

} else {

//node不在尾部

// If successor needs signal, try to set pred’s next-link

// so it will get one. Otherwise wake it up to propagate.

int ws;

//前继节点是个正常阻塞节点

完结

Redis基于内存,常用作于缓存的一种技术,并且Redis存储的方式是以key-value的形式。Redis是如今互联网技术架构中,使用最广泛的缓存,在工作中常常会使用到。Redis也是中高级后端工程师技术面试中,面试官最喜欢问的问题之一,因此作为Java开发者,Redis是我们必须要掌握的。

Redis 是 NoSQL 数据库领域的佼佼者,如果你需要了解 Redis 是如何实现高并发、海量数据存储的,那么这份腾讯专家手敲《Redis源码日志笔记》将会是你的最佳选择。

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

} else {

//node不在尾部

// If successor needs signal, try to set pred’s next-link

// so it will get one. Otherwise wake it up to propagate.

int ws;

//前继节点是个正常阻塞节点

完结

Redis基于内存,常用作于缓存的一种技术,并且Redis存储的方式是以key-value的形式。Redis是如今互联网技术架构中,使用最广泛的缓存,在工作中常常会使用到。Redis也是中高级后端工程师技术面试中,面试官最喜欢问的问题之一,因此作为Java开发者,Redis是我们必须要掌握的。

Redis 是 NoSQL 数据库领域的佼佼者,如果你需要了解 Redis 是如何实现高并发、海量数据存储的,那么这份腾讯专家手敲《Redis源码日志笔记》将会是你的最佳选择。

[外链图片转存中…(img-KNAtB1ml-1714467507159)]

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值