《面试实录》百度二面,Sychronized 原理详解

小龙coding
  • 对线面试官
  • 面试实录
  • Java
  • Java
  • Map
大约 8 分钟

本期是【面试实录】系列文章的第1期,持续更新中.....。

  • 欢迎关注+订阅,持续更新中!!!致力打造校招核心面试攻略~
  • 根据秋招春招上岸大厂面试经历以及身边朋友上岸面试录音模拟面试现场,并整合面试常考知识点,通俗有趣的去讲解 八股文,不一样的系列,轻松掌握知识~

面试实录】专栏系列目前已经连载 1 篇了,据说看了这个系列的朋友都拿到了大厂offer~

序言

大家好,我是小龙。

曾几何时,大家是否有这样的困惑?

知道相关的知识点,但是面试时面试官换一种问法,你便不知道怎样回答,给你一个场景,你就联系不到相关的知识点上,或者不知道用对应知识去分析相关问题。

比如 "你知道垂直定理,但是你遇到相关几何题,就是想不到用这个定理去解决问题"

新的篇章将开启,本系列会结合小龙面试经历以及身边上岸大厂的朋友们的面试经历、面试录音、面试分享,尽可能还原面试现场

带大家感受面试现场结合情景学会灵活的应用知识。看一看大佬们都是怎样面试的。

记得每期结束,问问自己,你真的对知识掌握了吗?

考题速查

本期会通过面试模拟复现百度提前批二面,关于 synchronized 的考察,以此讲解其原理。

本期题改编自 ——2022届秋招 百度 二面

面试现场

叮叮叮......

面试官:“你好,我是XX面试官,请问是小龙吗?”

小龙:“您好,面试官,我是小龙”

面试官:“好的,现在有空吗,我们开始面试吧”

小龙:“嗯嗯,准备好啦”

.......

other questions

.......

面试官:“synchronized 分别修饰了一个静态方法和一个实例方法,现在对其并发访问,这是否线程安全?”

独白:“我记不太清原问题咋问的了,反正大概考的知识点就是这个,不是直白的考,而是给你一个实际例子叫你分析”

独白:“当时可能刚开始面试第一个问题还没进入状态,又有点紧张,面试官说话又不清楚,所以一下还没反应过来,卡了几秒,映像比较深刻。”

小龙:“这个当然不安全,因为访问静态 synchronized 方法占用的锁是当前类的锁,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。”

面试官:“好的,那你知道 synchronized 实现加锁的本质原理吗?”

独白:“这个不就是 monitorenter 那两条命令吗,拿捏~”

小龙:“如果 synchronized 修饰代码块,javac 编译时,会在代码块前后生成 monitorentermonitorexit ,当执行遇到 monitorenter指令便会去尝试获取锁。”

独白:“以为拿捏了,为了让面试官觉得我理解的很透彻,再补充了一点。”

小龙:“使用 synchronized 在出现异常时,还可以保证锁的释放。因为它还会隐式的加一个 try-finnaly,finnaly 中也有 monitorexit 命令以便出现异常可以释放锁。”

面试官:“好的,你说的这些没问题。那你知道当执行到 monitorenter 指令时,它是怎样去尝试获取锁的吗?这个锁究竟是啥?你还是没说明白呢,哈哈。”

独白:“原来想问 monitor 噢,还是很基础的。”

小龙:“其实追根朔源,每个对象都有一个 monitor 与之关联,而当且一个 monitor 被持有后,它便处于锁定状态啦,而线程执行到 monitorenter 指令时,便会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。”

Moniter=WaitSet+EntryList(当锁被占用,其他线程来就会进入阻塞队列,等锁释放再一起竞争)+Owner(指向持有锁的线程)

img

面试官:“真的是这样吗?还有吗?”

小龙:"你别急嘛,我还没说完,上面说法其实不完全正确,那是针对重量级锁。"

小龙:“如果是 synchronized 没被优化之前,它是重量级锁,仅依赖对象对应的 moniter,但是后面进行了优化。”

面试官:“噢,展开说说。”

小龙:“我们的对象总的来说是由 对象头、实例数据、对齐填充构成,而对象头里面就存了 Mark Word,Mark Word 里面存了对象自身的一些运行信息,比如:hashcode、GC分代年龄、锁状态标志、持有的锁。”

小龙:“若 synchronized 给该对象加锁后,那么该对象头的 Mark Word 就会发生相应的变化,优化后的 synchronized 会迎合不同场景升级锁,随着锁升级,这个变化也不同。”

小龙:“偏向锁依赖 当前线程ID,重量级锁依赖 monitor ,轻量级锁依赖 锁记录lock-record。”

面试官:“好的,你说详细说一下锁的升级过程吗?”

小龙:“synchronized 总的升级流程是这样:无锁 ----> 偏向锁 ----> 轻量级锁----> 锁自旋 ----> 重量级锁。”

偏向锁

小龙:“首先会判断Mark Word里面是否有当前线程Id,若有则处于偏向锁,若无则尝试用 CAS 将 Mark Word 替换为线程Id,若成功则偏向锁设置成功,失败则有竞争要升级成轻量级锁。”

轻量级锁

小龙:“而对于轻量级锁里面涉及的就更复杂,详细展开说就是,开始会创建锁记录(Lock Record)对象,然后我们每个线程的栈帧都会包含一个锁记录的结构,内部可以用来存储锁定对象的 Mark Word;”

锁记录(Lock Record)包含了 lock record 地址 00、Object reference

对象(Obejct)组成上面说过了,此处不再赘述

img

小龙:“然后让锁记录中 Object reference 指向锁对象,并尝试用 CAS(原子操作)替换 Object 的 Mark Word,将 Mark Word 的值存入锁记录;”

img

小龙:“如果 CAS 替换成功,对象头中存储了锁记录地址和状态00,表示由该线程给对象加锁;”

img

面试官:“那如果失败又是怎样处理的呢?”

小龙:“如果cas失败,有两种情况:”

  • 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程(也就是升级成重量级锁---monitor)
  • 如果是自己执行了 synchronized 锁重入,那么再添加一条 Lock Record 作为重入的计数。

同一线程对同一对象加了多次锁--锁重入

img

小龙:“当退出 synchronized 代码块(解锁时)如果有取值为 null 的锁记录,表示有重入,这时重置锁记录表示重入计数减一;”

小龙:“当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 CAS 将 Mark Word 的值恢复给对象头(也就是将之前锁记录和对象CAS替换的部分又替换回来,换回原来各自的);”

小龙:“若上面操作成功,则解锁成功;失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。”

重量级锁

面试官:“那重量级锁又是怎么回事呢?”

小龙:“如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。”

小龙:“当 T1 进行轻量级加锁时,T0 已经对该对象加了轻量级锁。”

小龙:“这时 T1 加轻量级锁失败,进入锁膨胀流程 (因为 T1 加锁失败,被 T0 占了,但是 T1 不能在这干耗着啊,于是进入锁膨胀,申请一个monitor 去阻塞,升级成重量级锁--这时才有阻塞)”

小龙:“实际上就是为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址。然后自己进入 Monitor 的 EntryList BLOCKED ”

img

小龙:“当 T0 退出同步块解锁时,使用 CAS 将 Mark Word 的值恢复给对象头,失败(因为object mark-word 里放的是 monitor对象的地址了,不是T0 的 Lock Recode里面那个地址了)。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner为 null,唤醒 EntryList 中 BLOCKED 线程。”

面试官:“给个大大的赞,继续加油!”

知识总结

本期我们通过面试模拟深入探讨了 synchronized 的底层实现原理,下期再见。订阅+关注 持续追更。

面试重点

synchronized加锁原理锁特性锁升级过程