八股文精选50题

从海量笔记中精选出最有价值的50个面试高频问题

目录


并发编程

1. 死锁产生的四个条件是什么?如何避免?

四个必要条件:

  • 互斥条件:线程之间资源不可共享,必须独占
  • 持有并保持条件:线程持有资源并等待其他资源
  • 不可被剥夺条件:线程所持有的资源不能被强行剥夺
  • 循环等待条件:线程之间形成环形等待资源的关系

避免策略:

  • 按照相同的顺序申请资源,破坏循环等待条件

2. 线程安全的三个方面及实现方式?

  • 原子性:synchronized关键字、Lock锁
  • 可见性:volatile关键字、synchronized关键字、Lock锁
  • 有序性:volatile关键字(禁止指令重排序)

3. wait() 和 sleep() 的区别?

对比维度 wait() sleep()
所属类 Object类的实例方法 Thread类的静态方法
锁操作 会释放锁,让其他线程可以获取 不会释放锁
使用场景 必须配合synchronized使用 不需要synchronized
唤醒方式 需要notify/notifyAll唤醒(也可加时间参数) 时间到期自动唤醒
设计目的 线程间协作与通信 让线程休眠暂停执行

4. 线程池的核心参数及作用?

  • corePoolSize:核心线程数,即使没有任务也会存活
  • maximumPoolSize:最大线程数,超过核心线程数的线程会被回收
  • keepAliveTime:非核心线程的存活时间,超过这个时间会被回收
  • workQueue:任务队列,存放等待执行的任务
  • threadFactory:线程工厂,用于创建线程
  • handler:拒绝策略,当线程池满了无法处理新任务时的处理方式

5. 如何设置合理的核心线程数?

  • IO密集型任务:核心线程数 = CPU核心数 × 2

    • 原因:执行IO任务时,线程大多数时间在等待网络/磁盘,CPU处于空闲,2倍可充分利用CPU资源
  • CPU密集型任务:核心线程数 = CPU核心数 + 1

    • 原因:留出一个用来处理阻塞状态,避免过多线程导致上下文切换开销

注意:线程的上下文切换是指CPU在多个线程之间切换时,需要保存当前线程状态并加载下一个线程状态,这个过程会消耗CPU资源。


6. ThreadLocal的原理及内存泄漏问题?

原理:

  • ThreadLocal用于存储线程私有变量
  • 实际上是由ThreadLocalMap来存储value
  • 每个线程都有一个独立的ThreadLocalMap(存储在Thread类的成员变量中)
  • ThreadLocalMap由Entry数组组成,其中key为ThreadLocal对象(弱引用),value为对应的值(强引用)

内存泄漏问题:

  • key是弱引用会被GC回收,但value是强引用不会被回收
  • 导致Entry中key为null但value仍存在,造成内存泄漏
  • 解决方案:使用完后手动调用remove()方法清除

7. ReentrantLock如何实现公平锁?

ReentrantLock通过在CAS操作前增加一个判断:

  • 检查队列是否为空
  • 如果队列不为空,则上锁失败,将当前线程加入到队列中
  • 保证先到先得,实现公平性

8. CompletableFuture相比Future的优势?

  • 非阻塞:Future的get()方法是阻塞的,CompletableFuture可以通过thenAccept、thenRun等方法异步处理结果
  • 内置异常处理:exceptionally方法可以优雅处理异常,而Future需要手动try-catch
  • 链式调用:支持函数式编程,可以组合多个异步操作

9. synchronized锁升级过程?

  1. 无锁状态:线程获取锁时,如果锁未被持有,直接获取
  2. 偏向锁:记录线程ID,同一线程再次获取锁无需CAS
  3. 轻量级锁:lock_record和mark_word进行CAS操作,竞争不激烈时使用
  4. 重量级锁:竞争激烈时,升级为重量级锁,使用操作系统的互斥量

10. 线程池的四种拒绝策略?

  • AbortPolicy:抛出异常(默认策略)
  • CallerRunsPolicy:由调用者线程执行任务
  • DiscardPolicy:直接丢弃任务
  • DiscardOldestPolicy:丢弃队列中最旧的任务

集合框架

11. ArrayList的扩容机制?

  • 初始化时容量为0
  • 第一次添加元素时扩容到10
  • 之后每次扩容为原容量的1.5倍
  • 每次扩容都会进行数组拷贝,性能开销较大

12. HashMap的底层结构及put流程?

底层结构:数组 + 链表 + 红黑树

put流程:

  1. 对key进行hash运算,获得数组索引位置
  2. 如果该位置没有元素,直接插入
  3. 如果该位置有元素:
    • 判断key是否相同(通过hashCode和equals)
    • 相同则覆盖value
    • 不同则作为链表后继节点插入
  4. 如果链表长度>8且数组长度≥64,转换为红黑树

13. HashMap如何判断key相等?

使用两个指标:

  1. hashCode()相等
  2. equals()返回true

只有两者都满足,才判断为同一个key,否则视为hash冲突,需要在链表中继续查找或插入。


14. HashMap为什么容量必须是2的幂次方?

  • 2的幂次方在二进制中只有一位为1
  • capacity - 1 与hash值进行与运算的效率远高于取模运算
  • 例如:16-1=15(二进制1111),任何数与15进行&运算相当于取模16

15. HashSet的底层实现?

  • HashSet底层基于HashMap实现
  • 只存储元素的值(作为HashMap的key)
  • value是一个固定的Object常量(名为present)
  • 利用HashMap的key唯一性保证Set的元素不重复

Java基础

16. 为什么需要Integer包装类?

  • 泛型要求:泛型只能使用引用类型,不能使用基本类型
  • 集合存储:集合中只能存储引用类型
  • 类型转换:提供丰富的类型转换方法
  • 工具方法:提供parseInt等实用方法

17. Integer缓存池机制?

  • Integer内部有一个缓存池,默认缓存 -128 到 127 之间的Integer对象
  • 使用Integer.valueOf()方法时,值在缓存范围内会直接返回缓存对象,否则创建新对象
  • 可通过JVM参数-XX:AutoBoxCacheMax=1000改变缓存上限
  • 注意new Integer()总是创建新对象,不走缓存

18. 什么是反射?有哪些特性?

定义:对于任何一个类都能够知道它的方法和属性,对任何一个对象都可以调用它的任意方法和属性,这种动态获取类信息和动态调用对象方法的功能就叫做反射。

特性:

  • 可以在运行时获取类的信息
  • 可以在运行时动态创建对象
  • 可以在运行时动态调用方法
  • 可以在运行时修改属性值

19. 接口和抽象类的区别?

对比项 接口 抽象类
设计目的 定义行为规范 共享代码和部分实现
继承方式 支持多实现 只支持单继承
成员变量 只能是常量 可以有各种类型的变量
方法 默认抽象(Java 8+可有默认方法) 可以有抽象和具体方法
实例化 不能实例化 不能实例化

抽象类为什么不能多继承?
会产生同名方法或属性冲突,无法确定使用哪个父类的实现。


20. 浅拷贝和深拷贝的区别?

  • 浅拷贝

    • 创建新对象,复制基本类型属性值
    • 引用类型属性只复制引用(地址),两个对象内部引用指向同一个对象
  • 深拷贝

    • 创建新对象,复制基本类型属性值
    • 引用类型属性会创建新的对象,并复制原对象的值
    • 两个对象内部引用指向不同的对象,完全独立

JVM

21. JVM运行时数据区域有哪些?

线程私有:

  • 程序计数器(PC):记录当前线程执行位置,上下文切换时恢复
  • 虚拟机栈:存储局部变量、方法调用信息
  • 本地方法栈:为Native方法服务

线程共享:

  • :存储对象实例,GC主要区域
  • 方法区/元空间:存储类信息、常量、静态变量等

22. String创建对象的区别?

String s1 = "abc";  
// 如果常量池有"abc"不创建,没有则在常量池创建

String s2 = new String("abc");
// 在堆中创建一个新对象
// 如果常量池没有"abc",还会在常量池创建一个
// 共创建1个或2个对象

23. 内存泄漏和内存溢出的区别?

  • 内存泄漏:不再使用的对象仍被引用,导致GC无法回收,大量堆积
  • 内存溢出:内存已满,无法继续分配
    • 堆溢出:对象过多,OutOfMemoryError: Java heap space
    • 栈溢出:递归过深,StackOverflowError

24. 对象创建的完整过程?

  1. 类加载检查:检查常量池中是否有对应类信息,没有则加载类
  2. 分配内存:在堆中为对象分配内存空间
  3. 初始化零值:分配的内存空间全部初始化为0
  4. 设置对象头:设置对象的哈希码、所属类、GC信息等
  5. 执行构造方法:执行方法,初始化对象

25. 类加载的完整过程?

1. 加载

  • 通过类名找到.class文件
  • 将字节码加载成动态数据结构
  • 在内存中生成一个Class对象

2. 连接

  • 验证:验证字节码是否符合规范,是否有安全问题
  • 准备:为类的静态变量分配内存并设置默认值
  • 解析:将符号引用转换为直接引用(内存地址)

3. 初始化

  • 执行类的静态代码块
  • 初始化静态变量
  • 执行父类的静态代码块(递归到Object)

4. 卸载

  • 当类不再被使用时,可以被卸载释放内存

26. 什么是符号引用和直接引用?

  • 符号引用:指向常量池中类信息的索引,常量池存储类的全限定名(包名+类名)
  • 直接引用:直接指向内存地址的指针
  • 解析阶段:将符号引用转换为直接引用,提高运行时效率

27. static变量存储在哪里?

  • static变量是类级别的信息,存储在方法区/元空间
  • 只要类被加载,static变量就会分配内存
  • 所有该类的对象共享同一个static变量
  • 生命周期和类一致,不依赖对象的创建和销毁

28. 每个类是否有自己的常量池?

是的,每个类都有自己独立的常量池:

  • 位置:.class文件的一部分,加载后存储在方法区/元空间
  • 内容:符号引用(类名、方法名、字段名)、字面量(字符串、数字常量)
  • 作用:支持动态链接、方法调用和字段访问
  • 不共享:每个类的常量池独立,不与其他类共享

Spring框架

29. SpringBoot自动配置原理?

  1. @SpringBootApplication 包含 @EnableAutoConfiguration
  2. @EnableAutoConfiguration 引入 AutoConfigurationImportSelector
  3. 该类扫描 META-INF/spring.factories 文件,加载所有候选配置类
  4. 使用 @Conditional 系列注解判断是否满足条件
  5. 满足条件的配置类被加载到Spring容器

核心:约定大于配置,根据classpath中的依赖自动配置组件


30. Bean的生命周期?

  1. 实例化:创建Bean对象
  2. 属性赋值:依赖注入,填充属性
  3. 初始化前:调用BeanPostProcessor的postProcessBeforeInitialization
  4. 初始化:调用init-method或@PostConstruct方法
  5. 初始化后:调用BeanPostProcessor的postProcessAfterInitialization(AOP在此生成代理)
  6. 使用Bean
  7. 销毁:调用destroy-method或@PreDestroy方法

31. Spring Bean默认是单例还是多例?

  • 默认是单例(Singleton):在Spring容器中只存在一个实例,多次注入复用同一对象
  • Prototype(原型):每次获取都创建新实例
  • 其他作用域:request、session、application(Web环境)

32. Spring常用注解有哪些?

  • @Autowired:自动注入,按类型注入
  • @Component:标记为Spring组件
  • @Service:业务层组件
  • @Controller:控制层组件
  • @Repository:数据访问层组件
  • @Configuration:配置类
  • @Bean:在配置类中定义Bean

33. Spring如何解决循环依赖?

三级缓存机制:

  • 一级缓存(singletonObjects):存储完整的Bean实例
  • 二级缓存(earlySingletonObjects):存储早期暴露的Bean实例
  • 三级缓存(singletonFactories):存储Bean工厂对象

解决流程:

  1. A创建实例(空壳),放入三级缓存
  2. A填充属性,发现依赖B
  3. B创建实例(空壳),放入三级缓存
  4. B填充属性,发现依赖A,从三级缓存获取A的早期引用
  5. B完成创建,放入一级缓存
  6. A获取到B,完成创建,放入一级缓存

注意:构造器循环依赖无法解决,只能解决setter注入的循环依赖


34. 为什么需要三级缓存?

为了处理动态代理对象的问题:

  • 动态代理会创建一个新的代理对象(地址不同)
  • 三级缓存的ObjectFactory可以提前判断是否需要代理
  • 如果需要代理,返回代理对象而不是原始对象
  • 确保循环依赖时注入的是正确的对象(代理对象 or 原始对象)

35. Spring中常用的设计模式?

  • 单例模式:Bean默认单例
  • 工厂模式:BeanFactory创建Bean
  • 代理模式:AOP使用动态代理
  • 模板方法模式:JdbcTemplate、RestTemplate
  • 观察者模式:ApplicationListener事件监听

36. 单例Bean和Prototype Bean生命周期区别?

对比项 单例Bean 多例Bean
创建时机 容器启动时 每次获取时
实例数量 整个应用只有一个 每次创建新实例
销毁管理 容器关闭时调用销毁方法 Spring不管理销毁,需手动处理
适用场景 无状态Bean 有状态Bean

MySQL

37. MySQL执行一条SQL的完整流程?

  1. 建立连接:客户端与MySQL服务器建立TCP连接
  2. 查询缓存(8.0已废弃):检查缓存是否有结果
  3. 解析器:词法分析、语法分析、构建语法树
  4. 预处理器:检查表和字段是否存在,将*展开为具体字段
  5. 优化器:查询重写、选择最优执行计划
  6. 执行器:调用存储引擎API执行查询
  7. 返回结果

38. 什么是索引?为什么要用B+树?

定义:帮助MySQL高效获取数据的数据结构

为什么用B+树?

  • 高度低:一个节点存储多个key,减少IO次数
  • 范围查询快:叶子节点有序且有链表连接
  • 稳定性好:所有数据都在叶子节点,查询性能稳定

39. 索引失效的常见情况?

  1. 对索引列进行表达式运算或使用函数
  2. 不符合最左前缀原则(联合索引)
  3. like左模糊匹配('%abc')或左右模糊匹配('%abc%')
  4. 使用**!=、<>、not in**等
  5. 类型隐式转换(字符串不加引号)

40. MySQL的三大日志及作用?

binlog(二进制日志)

  • Server层实现
  • 记录所有增删改操作(逻辑日志)
  • 用于数据恢复和主从复制

redo log(重做日志)

  • InnoDB引擎实现
  • 记录物理页的修改
  • 用于事务持久性和崩溃恢复
  • 循环写,固定大小

undo log(回滚日志)

  • InnoDB引擎实现
  • 记录事务修改前的数据
  • 用于事务回滚和MVCC

41. 什么是两阶段提交?为什么需要?

目的:保证redo log和binlog的一致性

流程:

  1. Prepare阶段:将事务ID写入redo log,标记为prepared状态
  2. Commit阶段:写入binlog,将redo log标记为committed状态

崩溃恢复:

  • 如果redo log是prepared且binlog有对应事务ID → 提交
  • 如果redo log是prepared且binlog无对应事务ID → 回滚

42. 什么是索引下推?

场景SELECT * FROM user WHERE name LIKE '张%' AND age = 10

传统方式

  • 找到所有name以'张'开头的主键ID
  • 回表查询完整数据
  • 在Server层过滤age=10

索引下推

  • 在索引遍历过程中,直接过滤age=10
  • 只回表查询满足两个条件的记录
  • 减少回表次数,提高效率

优点:将部分过滤条件下推到存储引擎层执行


43. count(*) 和 count(1) 和 count(column) 的区别?

  • count(column):统计该字段不为NULL的行数
  • count(1):统计所有行(1永远不为NULL)
  • count(*):底层等同于count(0),统计所有行

性能

  • count(1) ≈ count(*) > count(主键) > count(字段)
  • InnoDB会优化count(*)和count(1),扫描最小的二级索引

44. MySQL的锁有哪些类型?

按粒度分:

  • 表锁:锁整张表(共享表锁、独占表锁、意向锁)
  • 行锁:锁单行数据(记录锁、间隙锁、临键锁)

按功能分:

  • 共享锁(S锁):读锁,多个事务可同时持有
  • 排他锁(X锁):写锁,只有一个事务可持有

特殊锁:

  • 意向锁:表级锁,表示事务对某行加锁的意图,避免加表锁时逐行检查

Redis

45. Redis的五种数据类型及底层实现?

类型 底层实现 使用场景
String SDS(简单动态字符串) 缓存、计数器
Hash 哈希表/压缩列表 对象存储
List 双向链表/压缩列表 消息队列、时间线
Set 哈希表/整数集合 标签、共同好友
ZSet 跳表+哈希表 排行榜、延时队列

46. Redis为什么选择跳表而不是B+树实现ZSet?

  1. 内存友好:跳表结构简单,只需存储指针和层高,内存占用低;B+树会产生内存碎片
  2. CPU友好:跳表指针调整只影响相邻节点,符合CPU局部性原理;B+树调整会影响整个树结构
  3. 代码简单:跳表实现简单,易于维护;B+树实现复杂
  4. 磁盘无关:Redis是内存数据库,不需要B+树的磁盘友好特性

47. Redis持久化方式及优缺点?

RDB(快照)

  • 原理:fork子进程,生成某一时刻的数据快照
  • 优点:文件小、加载快,适合大数据量
  • 缺点:可能丢失最后一次快照后的数据,不保证强一致性

AOF(追加文件)

  • 原理:记录每一条写操作命令
  • 写回策略:Always(每次写入)、Everysec(每秒写入)、No(OS决定)
  • 优点:数据安全性高,可保证强一致性
  • 缺点:文件大、加载慢

混合持久化(推荐):

  • RDB快照 + AOF增量命令,兼顾效率和安全

48. Redis除了缓存还有哪些应用场景?

  • 分布式锁:使用SET NX EX命令
  • 消息队列:使用List的LPUSH/RPOP或Stream
  • 排行榜:使用ZSet的ZADD/ZRANGE
  • 计数器:使用String的INCR
  • Session共享:分布式系统的会话存储
  • 限流:使用INCR + EXPIRE实现滑动窗口

消息队列

49. 消息队列如何保证消息不丢失?

生产者丢失

  • 只有收到MQ的ACK才认为发送成功,否则重传

服务端丢失

  • 将队列和消息持久化到磁盘

消费者丢失

  • 只有消费者成功处理完消息后才发送ACK,否则MQ会重传

整体方案:持久化 + ACK机制 + 重试机制


50. 如何保证消息队列的幂等性?

方案:

  1. 分布式锁:使用Redis确保同一消息只被处理一次
  2. 数据库唯一约束:业务主键设置唯一索引,重复插入会失败
  3. 消息唯一ID:生产者为每条消息生成唯一ID,消费者处理前检查该ID是否已处理

核心思想:让重复消费不产生副作用


总结

这50道题涵盖了面试中最核心的知识点:

  • 并发编程:死锁、线程池、ThreadLocal、锁升级
  • 集合框架:HashMap、ArrayList的底层原理
  • JVM:内存模型、类加载、垃圾回收
  • Spring:自动配置、Bean生命周期、循环依赖
  • MySQL:索引、日志、锁、执行流程
  • Redis:数据结构、持久化、应用场景
  • 消息队列:可靠性、幂等性

复习建议

  1. 每天复习10题,5天一轮
  2. 重点理解原理,而不是死记硬背
  3. 结合实际项目经验,准备案例说明
  4. 画图辅助理解,如HashMap结构、三级缓存等

祝你面试顺利!🚀