markOop mark = obj->mark(); //获取锁对象的对象头
//判断mark是否为可偏向状态,即mark的偏向锁标志位为1,锁标志位为 01,线程id为null
if (mark->is_biased_anonymously() && !attempt_rebias) {
//这个分支是进行对象的hashCode计算时会进入,在一个非全局安全点进行偏向锁撤销
markOop biased_value = mark;
//创建一个非偏向的markword
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
//Atomic:cmpxchg_ptr是CAS操作,通过cas重新设置偏向锁状态
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) {//如果CAS成功,返回偏向锁撤销状态
return BIAS_REVOKED;
}
} else if (mark->has_bias_pattern()) {//如果锁对象为可偏向状态(biased_lock:1, lock:01,不管线程id是否为空),尝试重新偏向
Klass* k = obj->klass();
markOop prototype_header = k->prototype_header();
//如果已经有线程对锁对象进行了全局锁定,则取消偏向锁操作
if (!prototype_header->has_bias_pattern()) {
markOop biased_value = mark;
//CAS 更新对象头markword为非偏向锁
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(prototype_header, obj->mark_addr(), mark);
assert(!(*(obj->mark_addr()))->has_bias_pattern(), “even if we raced, should still be revoked”);
return BIAS_REVOKED; //返回偏向锁撤销状态
} else if (prototype_header->bias_epoch() != mark->bias_epoch()) {
//如果偏向锁过期,则进入当前分支
if (attempt_rebias) {//如果允许尝试获取偏向锁
assert(THREAD->is_Java_thread(), “”);
markOop biased_value = mark;
markOop rebiased_prototype = markOopDesc::encode((JavaThread*) THREAD, mark->age(), prototype_header->bias_epoch());
//通过CAS 操作, 将本线程的 ThreadID 、时间错、分代年龄尝试写入对象头中
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(rebiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) { //CAS成功,则返回撤销和重新偏向状态
return BIAS_REVOKED_AND_REBIASED;
}
} else {//不尝试获取偏向锁,则取消偏向锁
//通过CAS操作更新分代年龄
markOop biased_value = mark;
markOop unbiased_prototype = markOopDesc::prototype()->set_age(mark->age());
markOop res_mark = (markOop) Atomic::cmpxchg_ptr(unbiased_prototype, obj->mark_addr(), mark);
if (res_mark == biased_value) { //如果CAS操作成功,返回偏向锁撤销状态
return BIAS_REVOKED;
}
}
}
}
…//省略
}
偏向锁的撤销
当到达一个全局安全点时,这时会根据偏向锁的状态来判断是否需要撤销偏向锁,调用 revoke_at_safepoint
方法,这个方法也是在 biasedLocking.cpp
中定义的
void BiasedLocking::revoke_at_safepoint(Handle h_obj) {
assert(SafepointSynchronize::is_at_safepoint(), “must only be called while at safepoint”);
oop obj = h_obj();
//更新撤销偏向锁计数,并返回偏向锁撤销次数和偏向次数
HeuristicsResult heuristics = update_heuristics(obj, false);
if (heuristics == HR_SINGLE_REVOKE) {//可偏向且未达到批量处理的阈值(下面会单独解释)
revoke_bias(obj, false, false, NULL); //撤销偏向锁
} else if ((heuristics == HR_BULK_REBIAS) ||
(heuristics == HR_BULK_REVOKE)) {//如果是多次撤销或者多次偏向
//批量撤销
bulk_revoke_or_rebias_at_safepoint(obj, (heuristics == HR_BULK_REBIAS), false, NULL);
}
clean_up_cached_monitor_info();
}
偏向锁的释放,需要等待全局安全点(在这个时间点上没有正在执行的字节码),首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态。如果线程仍然活着,则会升级为轻量级锁,遍历偏向对象的所记录。栈帧中的锁记录和对象头的Mark Word要么重新偏向其他线程,要么恢复到无锁,或者标记对象不适合作为偏向锁。最后唤醒暂停的线程。
JVM内部为每个类维护了一个偏向锁revoke计数器,对偏向锁撤销进行计数,当这个值达到指定阈值时,JVM会认为这个类的偏向锁有问题,需要重新偏向(rebias),对所有属于这个类的对象进行重偏向的操作成为
批量重偏向(bulk rebias)
。在做bulk rebias时,会对这个类的epoch的值做递增,这个epoch会存储在对象头中的epoch字段。在判断这个对象是否获得偏向锁的条件是:markword的biased_lock:1、lock:01、threadid和当前线程id相等、epoch字段和所属类的epoch值相同
,如果epoch的值不一样,要么就是撤销偏向锁、要么就是rebias; 如果这个类的revoke计数器的值继续增加到一个阈值,那么jvm会认为这个类不适合偏向锁,就需要进行bulk revoke操作
轻量级锁的获取逻辑
轻量级锁的获取,是调用 ::slow_enter
方法,该方法同样位于 synchronizer.cpp
文件中
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
markOop mark = obj->mark();
assert(!mark->has_bias_pattern(), “should not see bias pattern here”);
if (mark->is_neutral()) { //如果当前是无锁状态, markword的biase_lock:0,lock:01
//直接把mark保存到BasicLock对象的_displaced_header字段
lock->set_displaced_header(mark);
//通过CAS将mark word更新为指向BasicLock对象的指针,更新成功表示获得了轻量级锁
if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
TEVENT (slow_enter: release stacklock) ;
return ;
}
// Fall through to inflate() …
}
//如果markword处于加锁状态、且markword中的ptr指针指向当前线程的栈帧,表示为重入操作,不需要争抢锁
else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
assert(lock != mark->locker(), “must not re-lock the same lock”);
assert(lock != (BasicLock*)obj->mark(), “don’t relock with same BasicLock”);
lock->set_displaced_header(NULL);
return;
}
#if 0
// The following optimization isn’t particularly useful.
if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
lock->set_displaced_header (NULL) ;
return ;
}
#endif
//代码执行到这里,说明有多个线程竞争轻量级锁,轻量级锁通过inflate
进行膨胀升级为重量级锁
lock->set_displaced_header(markOopDesc::unused_mark());
ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
}
轻量级锁的获取逻辑简单再整理一下
-
mark->is_neutral()
方法,is_neutral
这个方法是在markOop.hpp
中定义,如果biased_lock:0且lock:01
表示无锁状态 -
如果mark处于无锁状态,则进入步骤(3),否则执行步骤(5)
-
把mark保存到BasicLock对象的displacedheader字段
-
通过CAS尝试将markword更新为指向BasicLock对象的指针,如果更新成功,表示竞争到锁,则执行同步代码,否则执行步骤(5)
-
如果当前mark处于加锁状态,且mark中的ptr指针指向当前线程的栈帧,则执行同步代码,否则说明有多个线程竞争轻量级锁,轻量级锁需要膨胀升级为重量级锁
轻量级锁的释放逻辑
轻量级锁的释放是通过 monitorexit
调用
IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorexit(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
Handle h_obj(thread, elem->obj());
assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
“must be NULL or an object”);
if (elem == NULL || h_obj()->is_unlocked()) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
ObjectSynchronizer::slow_exit(h_obj(), elem->lock(), thread);
// Free entry. This must be done here, since a pending exception might be installed on
// exit. If it is not cleared, the exception handling code will try to unlock the monitor again.
elem->set_obj(NULL);
#ifdef ASSERT
thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END
这段代码中主要是通过 ObjectSynchronizer::slow_exit
来执行
void ObjectSynchronizer::slow_exit(oop object, BasicLock* lock, TRAPS) {
fast_exit (object, lock, THREAD) ;
}
ObjectSynchronizer::fast_exit
的代码如下
void ObjectSynchronizer::fast_exit(oop object, BasicLock* lock, TRAPS) {
assert(!object->mark()->has_bias_pattern(), “should not see bias pattern here”);
// if displaced header is null, the previous enter is recursive enter, no-op
markOop dhw = lock->displaced_header(); //获取锁对象中的对象头
markOop mark ;
if (dhw == NULL) {
// Recursive stack-lock.
// Diagnostics – Could be: stack-locked, inflating, inflated.
mark = object->mark() ;
assert (!mark->is_neutral(), “invariant”) ;
if (mark->has_locker() && mark != markOopDesc::INFLATING()) {
assert(THREAD->is_lock_owned((address)mark->locker()), “invariant”) ;
}
if (mark->has_monitor()) {
ObjectMonitor * m = mark->monitor() ;
assert(((oop)(m->object()))->mark() == mark, “invariant”) ;
assert(m->is_entered(THREAD), “invariant”) ;
}
return ;
}
mark = object->mark() ; //获取线程栈帧中锁记录(LockRecord)中的markword
// If the object is stack-locked by the current thread, try to
// swing the displaced header from the box back to the mark.
if (mark == (markOop) lock) {
assert (dhw->is_neutral(), “invariant”) ;
//通过CAS尝试将Displaced Mark Word替换回对象头,如果成功,表示锁释放成功。
if ((markOop) Atomic::cmpxchg_ptr (dhw, object->mark_addr(), mark) == mark) {
TEVENT (fast_exit: release stacklock) ;
return;
}
}
//锁膨胀,调用重量级锁的释放锁方法
ObjectSynchronizer::inflate(THREAD, object)->exit (true, THREAD) ;
}
轻量级锁的释放也比较简单,就是将当前线程栈帧中锁记录空间中的Mark Word替换到锁对象的对象头中,如果成功表示锁释放成功。否则,锁膨胀成重量级锁,实现重量级锁的释放锁逻辑
锁膨胀的过程分析
重量级锁是通过对象内部的监视器(monitor)来实现,而monitor的本质是依赖操作系统底层的MutexLock实现的。我们先来看锁的膨胀过程,从前面的分析中已经知道了所膨胀的过程是通过 ObjectSynchronizer::inflate
方法实现的,代码如下
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
// Inflate mutates the heap …
// Relaxing assertion for bug 6320749.
assert (Universe::verify_in_progress() ||
!SafepointSynchronize::is_at_safepoint(), “invariant”) ;
for (;😉 { //通过无意义的循环实现自旋操作
const markOop mark = object->mark() ;
assert (!mark->has_bias_pattern(), “invariant”) ;
if (mark->has_monitor()) {//has_monitor是markOop.hpp中的方法,如果为true表示当前锁已经是重量级锁了
ObjectMonitor * inf = mark->monitor() ;//获得重量级锁的对象监视器直接返回
assert (inf->header()->is_neutral(), “invariant”);
assert (inf->object() == object, “invariant”) ;
assert (ObjectSynchronizer::verify_objmon_isinpool(inf), “monitor is invalid”);
return inf ;
}
if (mark == markOopDesc::INFLATING()) {//膨胀等待,表示存在线程正在膨胀,通过continue进行下一轮的膨胀
TEVENT (Inflate: spin while INFLATING) ;
ReadStableMark(object) ;
continue ;
}
if (mark->has_locker()) {//表示当前锁为轻量级锁,以下是轻量级锁的膨胀逻辑
ObjectMonitor * m = omAlloc (Self) ;//获取一个可用的ObjectMonitor
// Optimistically prepare the objectmonitor - anticipate successful CAS
// We do this before the CAS in order to minimize the length of time
// in which INFLATING appears in the mark.
m->Recycle();
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class
/**将object->mark_addr()和mark比较,如果这两个值相等,则将object->mark_addr()
改成markOopDesc::INFLATING(),相等返回是mark,不相等返回的是object->mark_addr()**/
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
if (cmp != mark) {//CAS失败
omRelease (Self, m, true) ;//释放监视器
continue ; // 重试
}
markOop dmw = mark->displaced_mark_helper() ;
assert (dmw->is_neutral(), “invariant”) ;
//CAS成功以后,设置ObjectMonitor相关属性
m->set_header(dmw) ;
m->set_owner(mark->locker());
m->set_object(object);
// TODO-FIXME: assert BasicLock->dhw != 0.
guarantee (object->mark() == markOopDesc::INFLATING(), “invariant”) ;
object->release_set_mark(markOopDesc::encode(m));
if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
TEVENT(Inflate: overwrite stacklock) ;
if (TraceMonitorInflation) {
if (object->is_instance()) {
ResourceMark rm;
tty->print_cr(“Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s”,
(void *) object, (intptr_t) object->mark(),
object->klass()->external_name());
}
}
return m ; //返回ObjectMonitor
}
//如果是无锁状态
assert (mark->is_neutral(), “invariant”);
ObjectMonitor * m = omAlloc (Self) ; 获取一个可用的ObjectMonitor
//设置ObjectMonitor相关属性
m->Recycle();
m->set_header(mark);
m->set_owner(NULL);
m->set_object(object);
m->OwnerIsThread = 1 ;
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class
/**将object->mark_addr()和mark比较,如果这两个值相等,则将object->mark_addr()
改成markOopDesc::encode(m),相等返回是mark,不相等返回的是object->mark_addr()**/
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
//CAS失败,说明出现了锁竞争,则释放监视器重行竞争锁
m->set_object (NULL) ;
m->set_owner (NULL) ;
m->OwnerIsThread = 0 ;
m->Recycle() ;
omRelease (Self, m, true) ;
m = NULL ;
continue ;
// interference - the markword changed - just retry.
// The state-transitions are one-way, so there’s no chance of
// live-lock – “Inflated” is an absorbing state.
}
if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
TEVENT(Inflate: overwrite neutral) ;
if (TraceMonitorInflation) {
if (object->is_instance()) {
ResourceMark rm;
tty->print_cr(“Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s”,
(void *) object, (intptr_t) object->mark(),
object->klass()->external_name());
}
}
return m ; //返回ObjectMonitor对象
}
}
锁膨胀的过程稍微有点复杂,整个锁膨胀的过程是通过自旋来完成的,具体的实现逻辑简答总结以下几点
-
1.
mark->has_monitor()
判断如果当前锁对象为重量级锁,也就是lock:10,则执行(2),否则执行(3) -
2.通过
mark->monitor
获得重量级锁的对象监视器ObjectMonitor并返回,锁膨胀过程结束 -
3.如果当前锁处于
INFLATING
,说明有其他线程在执行锁膨胀,那么当前线程通过自旋等待其他线程锁膨胀完成 -
4.如果当前是轻量级锁状态
mark->has_locker()
,则进行锁膨胀。首先,通过omAlloc方法获得一个可用的ObjectMonitor,并设置初始数据;然后通过CAS将对象头设置为`markOopDesc:INFLATING,表示当前锁正在膨胀,如果CAS失败,继续自旋 -
5.如果是无锁状态,逻辑类似第4步骤
锁膨胀的过程实际上是获得一个ObjectMonitor对象监视器,而真正抢占锁的逻辑,在
ObjectMonitor::enter
方法里面
重量级锁的竞争逻辑
重量级锁的竞争,在 ObjectMonitor::enter
方法中,代码文件在 objectMonitor.cpp
重量级锁的代码就不一一分析了,简单说一下下面这段代码主要做的几件事
-
通过CAS将monitor的
_owner
字段设置为当前线程,如果设置成功,则直接返回 -
如果之前的
_owner
指向的是当前的线程,说明是重入,执行_recursions++
增加重入次数 -
如果当前线程获取监视器锁成功,将
_recursions
设置为1,_owner
设置为当前线程 -
如果获取锁失败,则等待锁释放
void ATTR ObjectMonitor::enter(TRAPS) {
// The following code is ordered to check the most common cases first
// and to reduce RTS->RTO cache line upgrades on SPARC and IA32 processors.
Thread * const Self = THREAD ;
void * cur ;
cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
if (cur == NULL) {//CAS成功
// Either ASSERT _recursions == 0 or explicitly set _recursions = 0.
assert (_recursions == 0 , “invariant”) ;
assert (_owner == Self, “invariant”) ;
// CONSIDER: set or assert OwnerIsThread == 1
return ;
}
if (cur == Self) {
// TODO-FIXME: check for integer overflow! BUGID 6557169.
_recursions ++ ;
return ;
}
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, “internal state error”);
_recursions = 1 ;
// Commute owner from a thread-specific on-stack BasicLockObject address to
// a full-fledged “Thread *”.
_owner = Self ;
OwnerIsThread = 1 ;
return ;
}
// We’ve encountered genuine contention.
assert (Self->_Stalled == 0, “invariant”) ;
Self->_Stalled = intptr_t(this) ;
// Try one round of spinning before enqueueing Self
// and before going through the awkward and expensive state
// transitions. The following spin is strictly optional …
// Note that if we acquire the monitor from an initial spin
// we forgo posting JVMTI events and firing DTRACE probes.
if (Knob_SpinEarly && TrySpin (Self) > 0) {
assert (_owner == Self , “invariant”) ;
assert (_recursions == 0 , “invariant”) ;
assert (((oop)(object()))->mark() == markOopDesc::encode(this), “invariant”) ;
Self->_Stalled = 0 ;
return ;
}
assert (_owner != Self , “invariant”) ;
assert (_succ != Self , “invariant”) ;
assert (Self->is_Java_thread() , “invariant”) ;
JavaThread * jt = (JavaThread *) Self ;
assert (!SafepointSynchronize::is_at_safepoint(), “invariant”) ;
assert (jt->thread_state() != _thread_blocked , “invariant”) ;
assert (this->object() != NULL , “invariant”) ;
assert (_count >= 0, “invariant”) ;
// Prevent deflation at STW-time. See deflate_idle_monitors() and is_busy().
// Ensure the object-monitor relationship remains stable while there’s contention.
Atomic::inc_ptr(&_count);
EventJavaMonitorEnter event;
{ // Change java thread status to indicate blocked on monitor enter.
JavaThreadBlockedOnMonitorEnterState jtbmes(jt, this);
DTRACE_MONITOR_PROBE(contended__enter, this, object(), jt);
if (JvmtiExport::should_post_monitor_contended_enter()) {
JvmtiExport::post_monitor_contended_enter(jt, this);
}
OSThreadContendState osts(Self->osthread());
ThreadBlockInVM tbivm(jt);
Self->set_current_pending_monitor(this);
// TODO-FIXME: change the following for(;😉 loop to straight-line code.
for (;😉 {
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don’t want to enter
// the monitor while suspended because that would surprise the
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
如何快速更新自己的技术积累?
- 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
- 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
- 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
- 学习以后不知道有没有学成,则可以通过面试去检验。
我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目
jt->set_suspend_equivalent();
// cleared by handle_special_suspend_equivalent_condition()
// or java_suspend_self()
EnterI (THREAD) ;
if (!ExitSuspendEquivalent(jt)) break ;
//
// We have acquired the contended monitor, but while we were
// waiting another thread suspended us. We don’t want to enter
// the monitor while suspended because that would surprise the
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。
深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。
[外链图片转存中…(img-Uz3Z3pqx-1711881610241)]
[外链图片转存中…(img-kMxZ1cnF-1711881610242)]
[外链图片转存中…(img-JSh50h20-1711881610242)]
[外链图片转存中…(img-wZNDgGvl-1711881610243)]
[外链图片转存中…(img-injO0Ckv-1711881610243)]
[外链图片转存中…(img-Oap3bk5a-1711881610244)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!
由于文件比较大,这里只是将部分目录大纲截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且后续会持续更新
如果你觉得这些内容对你有帮助,可以添加V获取:vip1024b (备注Java)
[外链图片转存中…(img-hIrCpUxd-1711881610244)]
如何快速更新自己的技术积累?
- 在现有的项目里,深挖技术,比如用到netty可以把相关底层代码和要点都看起来。
- 如果不知道目前的努力方向,就看自己的领导或公司里技术强的人在学什么。
- 知道努力方向后不知道该怎么学,就到处去找相关资料然后练习。
- 学习以后不知道有没有学成,则可以通过面试去检验。
我个人觉得面试也像是一场全新的征程,失败和胜利都是平常之事。所以,劝各位不要因为面试失败而灰心、丧失斗志。也不要因为面试通过而沾沾自喜,等待你的将是更美好的未来,继续加油!
以上面试专题的答小编案整理成面试文档了,文档里有答案详解,以及其他一些大厂面试题目
[外链图片转存中…(img-LLRpfP1m-1711881610245)]
[外链图片转存中…(img-LZQG3ILb-1711881610245)]