前言
相信冲浪的时候,经常会看到发生类似「明明全答出来,也答得不错,却不知道为什么挂了」的情况,或者自己就是亲身经历者,导致这种情况的原因往往是:你以为的标准答案和面试官以为的标准答案不同,或者其他竞争者的答案更接近面试官以为的标准答案。这里我给出两种方案:
- 优化你以为的标准答案
- 优化面试官以为的标准答案
但是本文,只针对「如何优化你以为的标准答案」的方案来落实,也不推荐你尝试第二种方案...

通常面试者易犯的毛病有:
- 背了错误的八股;
- 审题不清导致的答非所问(比如把「synchronized实现原理」与「锁升级流程」划等号);
- 答得不够完整,没有到面试官的预期
先叠个甲:本文仅给出我以为的标准答案,并不是面试官以为的标准答案,更不是客观的标准答案,仅供参考,如果有不同的意见也欢迎在评论区指正。
可以看问题自己先回答,再看答案,效果更佳。另外由于篇幅原因,本文给出的答案都是精简版,链接会提供更详细系统的答案,下面我们就正式开始吧。
1、synchronized轻量级锁自旋10次失败才升级为重量级锁
这个问题是synchronized锁升级的一个细节。
常见错误:自旋10次,还没拿到锁,才发生升级。
先说结论:synchronized的轻量级锁,不会自旋,只要一次CAS获取锁失败,就会升级成重量级锁。
关于这个问题,《Java并发编程的艺术》(2015)和《深入理解Java虚拟机》(2019)给出的答案是不同的,存在一些历史原因,可以参考R大的回答:HotSpot VM参数-XX:PreBlockSpin=n是怎么回事?
关于synchronized的实现原理更详细可以参考:synchronized的轻量级锁居然不会自旋?深度解析synchronized实现原理
2、synchronized实现原理
常见错误:把「synchronized实现原理」与「锁升级流程」划等号。
想要说清synchronized实现原理,是要费点时间的。一个常见的扩展点是:wait/notify的实现原理。
- 第一部分:基础知识,需要包括使用,内存语义,可见性的保证,异常等等
- 第二部分:说实现原理,你可以说你喜欢深入源码,看了synchronized字节码,然后是JVM源码,四种锁状态详述,轻量级锁的自旋误区,包括重量级锁的加解锁都可以细说
- 第三部分:可以扩展说说wait/notify实现原理
- 第四部分:说说包装类踩坑,与Lock的比较,Policy,QMode参数调整等实战方面的东西
现场画图也是不错的。

更详细的synchronized实现原理同样请移步:synchronized的轻量级锁居然不会自旋?深度解析synchronized实现原理
3、说一说对JMM的理解
常见错误:
- 分不清「Java内存模型」和「Java内存结构」
- 把「Java内存模型」和「线程本地内存」划等号
先来说一说「JMM」:也就是「Java内存模型」
硬件,编译器等处在编程语言下层的东西,会向上带来三个问题:
- 可见性问题
- 原子性问题
- 有序性问题
而JMM需要向程序员屏蔽这三种问题,包括:
- 解决可见性:内存的抽象划分,这就是大部分人熟悉的「线程本地内存」
- 解决有序性:禁止重排序
- 解决原子性:CAS与锁
具体是如何做的,由于篇幅原因就不在这展开了,请参考:面试官:从零开始设计个JMM吧,说说你的思路,写的很详细了。
再说下「Java内存结构」,也就是「运行时数据区」
JVM内存结构主要包括:
- 栈
- 堆
- 本地方法栈
- 方法区
- 程序计数器
那网上介绍这个东西的资料是太多太多了,我这里就稍微介绍一下容易被人忽视的「运行时常量池」,实际上「运行时常量池」扮演着非常重要的角色。
运行时常量池
运行时常量池是方法区的一部分。
Class 文件中有个常量池表 :用于存放编译期生成的各种字面量 (Literal)和符号引用 (Symbolic Reference)。关于「常量池表」请参考深度解析字节码文件的常量池章节。
- 符号引用 :符号引用本身可以是任何形式的字面量,它的实现取决于JVM,只要能定位到目标即可。 符号引用的目标不一定被加载到内存中。
- 直接引用 :直接指向目标的引用。直接引用的目标一定在内存中存在。
常量池表会在类加载后存放到方法区的运行时常量池中。
Java虚拟机规范对运行时常量池的实现没什么细节要求。一般来说,运行时常量池还会存储符号引用转化为的直接引用。
4、进/线程间如何通信
相信只要是看过Java八股的都见过这个问题。首先,请不要把「进程和线程」搞混了
关于进程
坑不多,请参考:进程间有哪些通信方式
关于线程
这就有点坑了。
如果你把本文的「说一说对JMM的理解」搞明白了,或者看过面试官:从零开始设计个JMM吧,说说你的思路,你就知道:Java线程隐式通信,显式同步。
非常多的创作者在谈论Java线程间如何通信时上来就直述什么锁啊,wait/notify的,个人认为是有可能误导初学者同步就等同于通信的。那实际上这两个概念还是需要区分的。
Java采用共享内存并发模型: 线程A修改共享内存中的某个数据,B可以通过某种方式,比如轮询,感知到这个数据的变化,从而改变自己的行为。即:隐式通信,显式同步。
所以在Java世界里,线程间的通信靠的是同步,下面以wait为例:
- wait源码是将线程放入monitor的WaitSet中阻塞,这个过程和别的线程交换信息了吗?其实没有,但调用wait方法会「释放锁」。
- 而「释放锁」这个行为,对开发者来说,是同步,但会隐式地将线程本地内存的数据回写到主存中,开发者感知不到,但是实打实做了,这是synchronized的内存语义保证的,这就是「隐式通信,显式同步」。
当然还有AQS,ReentrantLock和Condition等同步机制,请参考:从ReentrantLock到AQS,到底和synchronized有啥区别
第二个麻烦事是:你知道Java的线程模型吗?
JVM规范里是没有规定的------具体实现用1:1(内核线程)、N:1(用户线程)、M:N(混合)模型的任何一种都完全OK。Java并不暴露出不同线程模型的区别,上层应用是感知不到差异的。
而HotSpot VM,在这个JVM的较新版本所支持的所有平台上,它都是使用1:1线程模型的,推荐阅读:JVM线程源码浅析-JVM线程如何映射到操作系统线程
最后的扩展点:
JUC提供了许多工具类帮助你做同步/互斥/通信,部分源码也非常推荐你看一看:
CountDownLatch与Semaphore快速上手与实现原理
深度拆解ConcurrentHashMap核心源码,彻底搞懂扩容机制
CAS与锁的应用之:原子类、LongAdder、阻塞队列详解
5、说一说对final关键字的理解
那这个问题主要是表述不完整。
这个问题,相信大家都知道的是:
- final 修饰类,不可被继承
- 修饰方法,不可被重写
- 修饰变量,不可被修改;修饰对象,表示对象的指针不可修改
再进一步,你可能知道:final会限制重排序。
但是:
- 为什么final修饰的变量才能被lambda表达式捕获?
- 什么是宏变量?
- 内联加速?
虽然final关键字相信连初学者都能略知一二,但能说的扩展点还是蛮多的。
- 满足:「被final修饰符修饰;在定义该final变量时就指定了初始值;该初始值在编译时就能够唯一指定」就成为「宏变量」,编译器会把程序所有用到宏变量的地方直接替换成该变量的值。
- Java的lambda是capture-by-value,即只能捕获值,做法是把外部的变量拷贝一份到内部。因此lambda对变量的修改是对局部变量的修改,不影响外部的a的值。Java 8允许捕获事实上不变量 。摆脱这种限制的方法是用数组。
- 使用final关键字修饰方法,JVM会显式地主动对方法、变量及类进行内联优化。
更详细请参考:深入理解final关键字,没那么简单
最后
许多看似简单的基础的问题,其实都能聊非常久,这取决于我们的知识深度,本文只是知识海洋的冰山一角。如果觉得这篇文章还不错或者对你有帮助的话,也请帮忙点个赞吧(✪ω✪)。