深度探索一些易错的,有坑的,不好答的Java八股

前言

相信冲浪的时候,经常会看到发生类似「明明全答出来,也答得不错,却不知道为什么挂了」的情况,或者自己就是亲身经历者,导致这种情况的原因往往是:你以为的标准答案和面试官以为的标准答案不同,或者其他竞争者的答案更接近面试官以为的标准答案。这里我给出两种方案:

  • 优化你以为的标准答案
  • 优化面试官以为的标准答案

但是本文,只针对「如何优化你以为的标准答案」的方案来落实,也不推荐你尝试第二种方案...

通常面试者易犯的毛病有:

  • 背了错误的八股;
  • 审题不清导致的答非所问(比如把「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关键字,没那么简单

最后

许多看似简单的基础的问题,其实都能聊非常久,这取决于我们的知识深度,本文只是知识海洋的冰山一角。如果觉得这篇文章还不错或者对你有帮助的话,也请帮忙点个赞吧(✪ω✪)。

相关推荐
原野心存5 分钟前
java基础进阶——继承、多态、异常捕获(2)
java·java基础知识·java代码审计
进阶的架构师10 分钟前
互联网Java工程师面试题及答案整理(2024年最新版)
java·开发语言
黄俊懿10 分钟前
【深入理解SpringCloud微服务】手写实现各种限流算法——固定时间窗、滑动时间窗、令牌桶算法、漏桶算法
java·后端·算法·spring cloud·微服务·架构
木子020419 分钟前
java高并发场景RabbitMQ的使用
java·开发语言
夜雨翦春韭30 分钟前
【代码随想录Day29】贪心算法Part03
java·数据结构·算法·leetcode·贪心算法
2401_8574396937 分钟前
“衣依”服装销售平台:Spring Boot技术应用与优化
spring boot·后端·mfc
大霞上仙1 小时前
jmeter学习(1)线程组与发送请求
java·学习·jmeter
Jerry.ZZZ1 小时前
系统设计,如何设计一个秒杀功能
后端
笃励1 小时前
Java面试题二
java·开发语言·python
易雪寒2 小时前
IDEA在git提交时添加忽略文件
java·git·intellij-idea