先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里p7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上java开发知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加v获取:vip1024b (备注java)
正文
当调用condition.await()方法后会使得当前获取lock的线程进入到等待队列,如果该线程能够从await()方法返回的话一定是该线程获取了与condition相关联的lock。接下来,我们还是从源码的角度去看,只有熟悉了源码的逻辑我们的理解才是最深的。await()方法源码为:
public final void await() throws interruptedexception {
if (thread.interrupted())
throw new interruptedexception();
// 1. 将当前线程包装成node,尾插入到等待队列中
node node = addconditionwaiter();
// 2. 释放当前线程所占用的lock,在释放的过程中会唤醒同步队列中的下一个节点
int savedstate = fullyrelease(node);
int interruptmode = 0;
while (!isonsyncqueue(node)) {
// 3. 当前线程进入到等待状态
locksupport.park(this);
if ((interruptmode = checkinterruptwhilewaiting(node)) != 0)
break;
}
// 4. 自旋等待获取到同步状态(即获取到lock)
if (acquirequeued(node, savedstate) && interruptmode != throw_ie)
interruptmode = reinterrupt;
if (node.nextwaiter != null) // clean up if cancelled
unlinkcancelledwaiters();
// 5. 处理被中断的情况
if (interruptmode != 0)
reportinterruptafterwait(interruptmode);
}
代码的主要逻辑请看注释,我们都知道当前线程调用condition.await()方法后,会使得当前线程释放lock然后加入到等待队列中,直至被signal/signalall后会使得当前线程从等待队列中移至到同步队列中去,直到获得了lock后才会从await方法返回,或者在等待时被中断会做中断处理。那么关于这个实现过程我们会有这样几个问题:1. 是怎样将当前线程添加到等待队列中去的?2.释放锁的过程?3.怎样才能从await方法退出?而这段代码的逻辑就是告诉我们这三个问题的答案。具体请看注释,在第1步中调用addconditionwaiter将当前线程添加到等待队列中,该方法源码为:
private node addconditionwaiter() {
node t = lastwaiter;
// if lastwaiter is cancelled, clean out.
if (t != null && t.waitstatus != node.condition) {
unlinkcancelledwaiters();
t = lastwaiter;
}
//将当前线程包装成node
node node = new node(thread.currentthread(), node.condition);
if (t == null)
firstwaiter = node;
else
//尾插入
t.nextwaiter = node;
//更新lastwaiter
lastwaiter = node;
return node;
}
这段代码就很容易理解了,将当前节点包装成node,如果等待队列的firstwaiter为null的话(等待队列为空队列),则将firstwaiter指向当前的node,否则,更新lastwaiter(尾节点)即可。就是通过尾插入的方式将当前线程封装的node插入到等待队列中即可,同时可以看出等待队列是一个不带头结点的链式队列,之前我们学习aqs时知道同步队列是一个带头结点的链式队列,这是两者的一个区别。将当前节点插入到等待对列之后,会使当前线程释放lock,由fullyrelease方法实现,fullyrelease源码为:
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;
}
}
这段代码就很容易理解了,调用aqs的模板方法release方法释放aqs的同步状态并且唤醒在同步队列中头结点的后继节点引用的线程,如果释放成功则正常返回,若失败的话就抛出异常。到目前为止,这两段代码已经解决了前面的两个问题的答案了,还剩下第三个问题,怎样从await方法退出?现在回过头再来看await方法有这样一段逻辑:
while (!isonsyncqueue(node)) {
// 3. 当前线程进入到等待状态
locksupport.park(this);
if ((interruptmode = checkinterruptwhilewaiting(node)) != 0)
break;
}
很显然,当线程第一次调用condition.await()方法时,会进入到这个while()循环中,然后通过locksupport.park(this)方法使得当前线程进入等待状态,那么要想退出这个await方法第一个前提条件自然而然的是要先退出这个while循环,出口就只剩下两个地方:1. 逻辑走到break退出while循环;2. while循环中的逻辑判断为false。再看代码出现第1种情况的条件是当前等待的线程被中断后代码会走到break退出,第二种情况是当前节点被移动到了同步队列中(即另外线程调用的condition的signal或者signalall方法),while中逻辑判断为false后结束while循环。总结下,就是当前线程被中断或者调用condition.signal/condition.signalall方法,当前节点移动到了同步队列后 ,这是当前线程退出await方法的前提条件。当退出while循环后就会调用acquirequeued(node, savedstate)
,这个方法在介绍aqs的底层实现时说过了,若感兴趣的话可以去看这篇文章,该方法的作用是在自旋过程中线程不断尝试获取同步状态,直至成功(线程获取到lock)。这样也说明了退出await方法必须是已经获得了condition引用(关联)的lock。到目前为止,开头的三个问题我们通过阅读源码的方式已经完全找到了答案,也对await方法的理解加深。await方法示意图如下图:
如图,调用condition.await方法的线程必须是已经获得了lock,也就是当前线程是同步队列中的头结点。调用该方法后会使得当前线程所封装的node尾插入到等待队列中。
condition还额外支持了超时机制,使用者可调用方法awaitnanos,awaitutil。这两个方法的实现原理,基本上与aqs中的tryacquire方法如出一辙,关于tryacquire可以仔细阅读这篇文章。
要想不响应中断可以调用condition.awaituninterruptibly()方法,该方法的源码为:
public final void awaituninterruptibly() {
node node = addconditionwaiter();
int savedstate = fullyrelease(node);
boolean interrupted = false;
while (!isonsyncqueue(node)) {
locksupport.park(this);
if (thread.interrupted())
interrupted = true;
}
if (acquirequeued(node, savedstate) || interrupted)
selfinterrupt();
}
这段方法与上面的await方法基本一致,只不过减少了对中断的处理,并省略了reportinterruptafterwait方法抛被中断的异常。
signal/signalall实现原理
调用condition的signal或者signalall方法可以将等待队列中等待时间最长的节点移动到同步队列中,使得该节点能够有机会获得lock。按照等待队列是先进先出(fifo)的,所以等待队列的头节点必然会是等待时间最长的节点,也就是每次调用condition的signal方法是将头节点移动到同步队列中。我们来通过看源码的方式来看这样的猜想是不是对的,signal方法源码为:
public final void signal() {
//1. 先检测当前线程是否已经获取lock
if (!isheldexclusively())
throw new illegalmonitorstateexception();
//2. 获取等待队列中第一个节点,之后的操作都是针对这个节点
node first = firstwaiter;
if (first != null)
dosignal(first);
}
signal方法首先会检测当前线程是否已经获取lock,如果没有获取lock会直接抛出异常,如果获取的话再得到等待队列的头指针引用的节点,之后的操作的dosignal方法也是基于该节点。下面我们来看看dosignal方法做了些什么事情,dosignal方法源码为:
private void dosignal(node first) {
do {
if ( (firstwaiter = first.nextwaiter) == null)
lastwaiter = null;
//1. 将头结点从等待队列中移除
first.nextwaiter = null;
//2. while中transferforsignal方法对头结点做真正的处理
} while (!transferforsignal(first) &&
(first = firstwaiter) != null);
}
具体逻辑请看注释,真正对头节点做处理的逻辑在transferforsignal放,该方法源码为:
final boolean transferforsignal(node node) {
/*
- if cannot change waitstatus, the node has been cancelled.
*/
//1. 更新状态为0
if (!compareandsetwaitstatus(node, node.condition, 0))
return false;
/*
-
splice onto queue and try to set waitstatus of predecessor to
-
indicate that thread is (probably) waiting. if cancelled or
-
attempt to set waitstatus fails, wake up to resync (in which
-
case the waitstatus can be transiently and harmlessly wrong).
*/
//2.将该节点移入到同步队列中去
node p = enq(node);
int ws = p.waitstatus;
if (ws > 0 || !compareandsetwaitstatus(p, ws, node.signal))
locksupport.unpark(node.thread);
return true;
}
关键逻辑请看注释,这段代码主要做了两件事情1.将头结点的状态更改为condition;2.调用enq方法,将该节点尾插入到同步队列中,关于enq方法请看aqs的底层实现这篇文章。现在我们可以得出结论:调用condition的signal的前提条件是当前线程已经获取了lock,该方法会使得等待队列中的头节点即等待时间最长的那个节点移入到同步队列,而移入到同步队列后才有机会使得等待线程被唤醒,即从await方法中的locksupport.park(this)方法中返回,从而才有机会使得调用await方法的线程成功退出。signal执行示意图如下图:
sigllall与sigal方法的区别体现在dosignalall方法上,前面我们已经知道dosignal方法只会对等待队列的头节点进行操作,而dosignalall的源码为:
private void dosignalall(node first) {
lastwaiter = firstwaiter = null;
do {
node next = first.nextwaiter;
first.nextwaiter = null;
transferforsignal(first);
first = next;
} while (first != null);
}
该方法只不过时间等待队列中的每一个节点都移入到同步队列中,即“通知”当前调用condition.await()方法的每一个线程。
文章开篇提到等待/通知机制,通过使用condition提供的await和signal/signalall方法就可以实现这种机制,而这种机制能够解决最经典的问题就是“生产者与消费者问题”,关于“生产者消费者问题”之后会用单独的一篇文章进行讲解,这也是面试的高频考点。await和signal和signalall方法就像一个开关控制着线程a(等待方)和线程b(通知方)。它们之间的关系可以用下面一个图来表现得更加贴切:
如图,线程awaitthread先通过lock.lock()方法获取锁成功后调用了condition.await方法进入等待队列,而另一个线程signalthread通过lock.lock()方法获取锁成功后调用了condition.signal或者signalall方法,使得线程awaitthread能够有机会移入到同步队列中,当其他线程释放lock后使得线程awaitthread能够有机会获取lock,从而使得线程awaitthread能够从await方法中退出执行后续操作。如果awaitthread获取lock失败会直接进入到同步队列。
最后
最后,强调几点:
- 1. 一定要谨慎对待写在简历上的东西,一定要对简历上的东西非常熟悉。因为一般情况下,面试官都是会根据你的简历来问的; 能有一个上得了台面的项目也非常重要,这很可能是面试官会大量发问的地方,所以在面试之前好好回顾一下自己所做的项目;
- 2. 和面试官聊基础知识比如设计模式的使用、多线程的使用等等,可以结合具体的项目场景或者是自己在平时是如何使用的;
- 3. 注意自己开源的github项目,面试官可能会挖你的github项目提问;
我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
面试答案
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加v获取:vip1024b (备注java)
一个人可以走的很快,但一群人才能走的更远!不论你是正从事it行业的老鸟或是对it行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
是更美好的未来,继续加油!
面试答案
[外链图片转存中…(img-soamdykt-1713597596515)]
[外链图片转存中…(img-uvbnxagd-1713597596516)]
[外链图片转存中…(img-slej8ydx-1713597596517)]
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
需要这份系统化的资料的朋友,可以添加v获取:vip1024b (备注java)
[外链图片转存中…(img-vdnrlpqr-1713597596517)]
一个人可以走的很快,但一群人才能走的更远!不论你是正从事it行业的老鸟或是对it行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
发表评论