java并发锁ReentrantLock源码分析二之Condition实现原理

  •  IllegalMonitorStateException if it fails.
    
  • Block until signalled or interrupted.
  • Reacquire by invoking specialized version of
  •  {@link #acquire} with saved state as argument.
    
  • If interrupted while blocked in step 4, throw InterruptedException.

*/

public final void await() throws InterruptedException {

if (Thread.interrupted()) // @1

throw new InterruptedException();

Node node = addConditionWaiter(); //@2

int savedState = fullyRelease(node); // @3

int interruptMode = 0;

while (!isOnSyncQueue(node)) { //@4

LockSupport.park(this);

if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) // @5

break;

}

if (acquireQueued(node, savedState) && interruptMode != THROW_IE) //@6

interruptMode = REINTERRUPT;

if (node.nextWaiter != null) // clean up if cancelled //@7

unlinkCancelledWaiters();

if (interruptMode != 0) //@8

reportInterruptAfterWait(interruptMode);

}

代码@1:检测当前线程的中断标记,如果中断位为1,则抛出异常。

代码@2:添加等待节点。就是一个简单的链表维护节点的操作,具体参照addConditionWaiter讲解。

代码@3:释放占有的锁,并获取当前锁的state,因为await实现的语意为Object.wait,释放锁并并等待条件的发生。当条件满足后,线程被唤醒后,第一步是需要获取锁,然后在上次await的下一条指令处继续执行。代码3就是实现上述语义的释放锁。

代码@4:isOnSyncQueue 当前节点是否在同步队列中,如果在同步阻塞队列中,则申请锁,去执行;如果不在同步队列中(在条件队列中),阻塞,等待满足条件,新增的节点,默认在条件队列中(Conditon)。isOnSyncQueue 源码解读在下文中;

代码@5:线程从条件等待被唤醒,唤醒后,线程要从条件队列移除,进入到同步等待队列,被唤醒有有如下两种情况,一是条件满足,收到singal信号,二是线程被取消(中断),该步骤是从条件队列移除,加入到同步等待队列,返回被唤醒的原因,如果是被中断,需要根据不同模式,处理中断。处理中断,也有两种方式:1.继续设置中断位;2:直接抛出InterruptedException。请看下文关于checkInterruptWhileWaiting的源码解读。

代码@6:运行到代码6时,说明线程已经结束了释放锁,从条件队列移除,线程运行,在继续执行业务逻辑之前,必须先获取锁。只有成功获取锁后,才会去判断线程的中断标志,才能在中断标志为真时,抛出InterruptException。

代码@7,执行一些收尾工作,清理整个条件队列:

代码@8,处理中断,是设置中断位,还是抛出InterruptException。

那我们先关注一下addConditionWaiter方法:

/**

  • Adds a new waiter to wait queue.

  • @return its new wait node

*/

private Node addConditionWaiter() {

Node t = lastWaiter;

// If lastWaiter is cancelled, clean out.

if (t != null && t.waitStatus != Node.CONDITION) { //@1

unlinkCancelledWaiters();

t = lastWaiter;

}

Node node = new Node(Thread.currentThread(), Node.CONDITION); //@2

if (t == null)

firstWaiter = node;

else

t.nextWaiter = node;

lastWaiter = node;

return node;

}

添加条件等待节点,根据链表的特征,直接在尾部节点的nextWaiter指向新建的节点,并将新建的节点设置为整个链表的尾部,首先要知道如下数据结构:

object {

Node firstWaiter;

Node lastWaiter;

node {

node nextWaiter;

该节点承载的业务数据,比如这里的Thread t;等

}

}

知道上述结构,其实整个链的数据维护,基本一目了然,自己都可以实现下面的逻辑。

代码@1,如果最后一个等待节点的状态不是Node.CONDITION,则,则先删除等待链中节点状态不为Node.CONDITION的节点。具体代码分析请参照下文unlinkCancelledWaiters的解读。

代码@2开始,就是普通链表的节点添加的基本方法。

清除等待节点方法。

/**

  • Unlinks cancelled waiter nodes from condition queue.

  • Called only while holding lock. This is called when

  • cancellation occurred during condition wait, and upon

  • insertion of a new waiter when lastWaiter is seen to have

  • been cancelled. This method is needed to avoid garbage

  • retention in the absence of signals. So even though it may

  • require a full traversal, it comes into play only when

  • timeouts or cancellations occur in the absence of

  • signals. It traverses all nodes rather than stopping at a

  • particular target to unlink all pointers to garbage nodes

  • without requiring many re-traversals during cancellation

  • storms.

*/

private void unlinkCancelledWaiters() {

Node t = firstWaiter; //

Node trail = null; //@1

while (t != null) {

Node next = t.nextWaiter;

if (t.waitStatus != Node.CONDITION) { // @3

t.nextWaiter = null;

if (trail == null) // @4

firstWaiter = next;

else

trail.nextWaiter = next; //@5

if (next == null) // @6

lastWaiter = trail;

}

else // @4

trail = t;

t = next;

}

}

该方法的思路为,从第一节点开始,将不等于Node.CONDITION的节点。

代码@1,设置尾部节点临时变量,用来记录最终的尾部节点。代码@1 第一次循环,是循环第一个节点,如果它的状态为Node.CONDITION, 则该链的头节点保持不变,设置临时尾节点为t,然后进行一个节点的判断,如果节点不为Node.CONDITION, 重置头节点的下一个节点,或尾部节点的下一个节点(@4,@5)。代码@6代表整个循环结束,设置 ConditionObject对象的lastWaiter为trail的值;

await步骤中,释放锁过程源码解析。释放锁的过程,逻辑为unlock,但该方法,返回当前锁的state,因为释放锁后,该方法在条件没有满足前提下,自身需要阻塞。被唤醒后,需要先尝试获取锁,然后才能执行接下来的逻辑。

/**

  • Invokes release with current state value; returns saved state.

  • Cancels node and throws exception on failure.

  • @param node the condition node for this wait

  • @return previous sync state

*/

final int fullyRelease(Node node) {

boolean failed = true;

try {

int savedState = getState();

if (release(savedState)) {

failed = false;

return savedState;

} else {

throw new IllegalMonitorStateException();

}

} finally {

if (failed)

node.waitStatus = Node.CANCELLED;

}

}

await,@4步骤中,isOnSyncQueue 源码解读:

/**

  • Returns true if a node, always one that was initially placed on

  • a condition queue, is now waiting to reacquire on sync queue.

  • @param node the node

  • @return true if is reacquiring

*/

final boolean isOnSyncQueue(Node node) {

if (node.waitStatus == Node.CONDITION || node.prev == null) // @1

return false;

if (node.next != null) // If has successor, it must be on queue // @2

return true;

/*

  • node.prev can be non-null, but not yet on queue because

  • the CAS to place it on queue can fail. So we have to

  • traverse from tail to make sure it actually made it. It

  • will always be near the tail in calls to this method, and

  • unless the CAS failed (which is unlikely), it will be

  • there, so we hardly ever traverse much.

*/

return findNodeFromTail(node);

}

代码@1,如果节点的状态为Node.CONDITION 或 node.prev == null,表明该节点在条件队列中,并没有加入同步阻塞队列(同步阻塞队列为申请锁等待的队列),await方法中,新增的节点,默认满足上述条件,所以返回false,表示在条件队列中,等待条件的发生,条件满足之前,当前线程应该阻塞。这里,先预留一个疑问,那node.prev在什么时候会改变呢?

代码@2,如果node.next不为空,说明在同步阻塞队列中。这个我想毫无疑问。当然也说明next域肯定是在进入同步队列过程中会设置值。

代码@3, 上面的注释也说的比较清楚,node.prev不为空,但也不在同步队列中,这个是由于CAS可能会失败,为了不丢失信号,从同步队列中再次选择该节点,如果找到则返回true,否则返回false,在这里,我就更加对node.prev在什么时候会设置值感兴趣了,请继续await方法向下看,总有水落石出的时候。

await @5 checkInterruptWhileWaiting 代码解读:

/*

  • For interruptible waits, we need to track whether to throw

  • InterruptedException, if interrupted while blocked on

  • condition, versus reinterrupt current thread, if

  • interrupted while blocked waiting to re-acquire.

*/

/** Mode meaning to reinterrupt on exit from wait */

private static final int REINTERRUPT = 1; // 重新设置中断位,中断由上层处理

/** Mode meaning to throw InterruptedException on exit from wait */

private static final int THROW_IE = -1; // 直接抛出 InterruptedException 0:正常

/**

  • Checks for interrupt, returning THROW_IE if interrupted

  • before signalled, REINTERRUPT if after signalled, or

  • 0 if not interrupted.

*/

private int checkInterruptWhileWaiting(Node node) {

return Thread.interrupted() ?

(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :

0;

}

/**

  • Transfers node, if necessary, to sync queue after a cancelled

  • wait. Returns true if thread was cancelled before being

  • signalled.

  • @param current the waiting thread

  • @param node its node

  • @return true if cancelled before the node was signalled

*/

final boolean transferAfterCancelledWait(Node node) {

if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) { //@1

enq(node);

return true;

}

/*

  • If we lost out to a signal(), then we can’t proceed

  • until it finishes its enq(). Cancelling during an

  • incomplete transfer is both rare and transient, so just

  • spin.

*/

while (!isOnSyncQueue(node))

Thread.yield();

return false;

}

/**

  • Inserts node into queue, initializing if necessary. See picture above.

  • @param node the node to insert

  • @return node’s predecessor

*/

private Node enq(final Node node) {

for (;😉 {

Node t = tail;

if (t == null) { // Must initialize

if (compareAndSetHead(new Node()))

tail = head;

} else {

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

重点关注@1,首先需要知道一点,如果收到正常的singal()信号而被唤醒的节点【这个再singal方法时重点分析】,状态为Node.SINGAL,不会是Node.CONDITION状态,所以如果代码1compareAndSetWaitStatus设置成功,说明线程是调用了t.interrupt方法而使得LockSupport.park解除阻塞的,然后将该节点加入到同步队列中,使得 while( ! isOnSyncQueue(node)) 的条件为真,结束 await的等待条件触发语义,,进入到 抢占锁阶段。【再次重申Object wait语义,释放当前锁,然后等待条件的触发【条件队列】,,条件发生后,要先重新去抢占锁,获取锁则继续执行,否则阻塞在获取锁【同步队列】】,所以当 线程阻塞在  await 方法时,调用 t.interrupt方法时只是中断条件队列的等待,并不能马上取消执行,马上抛出InterrupterException。

await方法流程图:

1.2 signal()方法详解


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

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

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

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

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

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

img

总结

我个人认为,如果你想靠着背面试题来获得心仪的offer,用癞蛤蟆想吃天鹅肉形容完全不过分。想必大家能感受到面试越来越难,想找到心仪的工作也是越来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
来越难,高薪工作羡慕不来,却又对自己目前的薪资不太满意,工作几年甚至连一个应届生的薪资都比不上,终究是错付了,错付了自己没有去提升技术。

这些面试题分享给大家的目的,其实是希望大家通过大厂面试题分析自己的技术栈,给自己梳理一个更加明确的学习方向,当你准备好去面试大厂,你心里有底,大概知道面试官会问多广,多深,避免面试的时候一问三不知。

大家可以把Java基础,JVM,并发编程,MySQL,Redis,Spring,Spring cloud等等做一个知识总结以及延伸,再去进行操作,不然光记是学不会的,这里我也提供一些脑图分享给大家:

[外链图片转存中…(img-nyVnSwKb-1713445777603)]

[外链图片转存中…(img-eg6mhm9E-1713445777605)]

[外链图片转存中…(img-PzW0SRlE-1713445777607)]

希望你看完这篇文章后,不要犹豫,抓紧学习,复习知识,准备在明年的金三银四拿到心仪的offer,加油,打工人!
《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 10
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值