八股文精选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锁升级过程?
- 无锁状态:线程获取锁时,如果锁未被持有,直接获取
- 偏向锁:记录线程ID,同一线程再次获取锁无需CAS
- 轻量级锁:lock_record和mark_word进行CAS操作,竞争不激烈时使用
- 重量级锁:竞争激烈时,升级为重量级锁,使用操作系统的互斥量
10. 线程池的四种拒绝策略?
- AbortPolicy:抛出异常(默认策略)
- CallerRunsPolicy:由调用者线程执行任务
- DiscardPolicy:直接丢弃任务
- DiscardOldestPolicy:丢弃队列中最旧的任务
集合框架
11. ArrayList的扩容机制?
- 初始化时容量为0
- 第一次添加元素时扩容到10
- 之后每次扩容为原容量的1.5倍
- 每次扩容都会进行数组拷贝,性能开销较大
12. HashMap的底层结构及put流程?
底层结构:数组 + 链表 + 红黑树
put流程:
- 对key进行hash运算,获得数组索引位置
- 如果该位置没有元素,直接插入
- 如果该位置有元素:
- 判断key是否相同(通过hashCode和equals)
- 相同则覆盖value
- 不同则作为链表后继节点插入
- 如果链表长度>8且数组长度≥64,转换为红黑树
13. HashMap如何判断key相等?
使用两个指标:
- hashCode()相等
- 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. 对象创建的完整过程?
- 类加载检查:检查常量池中是否有对应类信息,没有则加载类
- 分配内存:在堆中为对象分配内存空间
- 初始化零值:分配的内存空间全部初始化为0
- 设置对象头:设置对象的哈希码、所属类、GC信息等
- 执行构造方法:执行
方法,初始化对象
25. 类加载的完整过程?
1. 加载:
- 通过类名找到.class文件
- 将字节码加载成动态数据结构
- 在内存中生成一个Class对象
2. 连接:
- 验证:验证字节码是否符合规范,是否有安全问题
- 准备:为类的静态变量分配内存并设置默认值
- 解析:将符号引用转换为直接引用(内存地址)
3. 初始化:
- 执行类的静态代码块
- 初始化静态变量
- 执行父类的静态代码块(递归到Object)
4. 卸载:
- 当类不再被使用时,可以被卸载释放内存
26. 什么是符号引用和直接引用?
- 符号引用:指向常量池中类信息的索引,常量池存储类的全限定名(包名+类名)
- 直接引用:直接指向内存地址的指针
- 解析阶段:将符号引用转换为直接引用,提高运行时效率
27. static变量存储在哪里?
- static变量是类级别的信息,存储在方法区/元空间
- 只要类被加载,static变量就会分配内存
- 所有该类的对象共享同一个static变量
- 生命周期和类一致,不依赖对象的创建和销毁
28. 每个类是否有自己的常量池?
是的,每个类都有自己独立的常量池:
- 位置:.class文件的一部分,加载后存储在方法区/元空间
- 内容:符号引用(类名、方法名、字段名)、字面量(字符串、数字常量)
- 作用:支持动态链接、方法调用和字段访问
- 不共享:每个类的常量池独立,不与其他类共享
Spring框架
29. SpringBoot自动配置原理?
@SpringBootApplication包含@EnableAutoConfiguration@EnableAutoConfiguration引入AutoConfigurationImportSelector- 该类扫描
META-INF/spring.factories文件,加载所有候选配置类 - 使用
@Conditional系列注解判断是否满足条件 - 满足条件的配置类被加载到Spring容器
核心:约定大于配置,根据classpath中的依赖自动配置组件
30. Bean的生命周期?
- 实例化:创建Bean对象
- 属性赋值:依赖注入,填充属性
- 初始化前:调用BeanPostProcessor的postProcessBeforeInitialization
- 初始化:调用init-method或@PostConstruct方法
- 初始化后:调用BeanPostProcessor的postProcessAfterInitialization(AOP在此生成代理)
- 使用Bean
- 销毁:调用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工厂对象
解决流程:
- A创建实例(空壳),放入三级缓存
- A填充属性,发现依赖B
- B创建实例(空壳),放入三级缓存
- B填充属性,发现依赖A,从三级缓存获取A的早期引用
- B完成创建,放入一级缓存
- 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的完整流程?
- 建立连接:客户端与MySQL服务器建立TCP连接
- 查询缓存(8.0已废弃):检查缓存是否有结果
- 解析器:词法分析、语法分析、构建语法树
- 预处理器:检查表和字段是否存在,将*展开为具体字段
- 优化器:查询重写、选择最优执行计划
- 执行器:调用存储引擎API执行查询
- 返回结果
38. 什么是索引?为什么要用B+树?
定义:帮助MySQL高效获取数据的数据结构
为什么用B+树?
- 高度低:一个节点存储多个key,减少IO次数
- 范围查询快:叶子节点有序且有链表连接
- 稳定性好:所有数据都在叶子节点,查询性能稳定
39. 索引失效的常见情况?
- 对索引列进行表达式运算或使用函数
- 不符合最左前缀原则(联合索引)
- like左模糊匹配('%abc')或左右模糊匹配('%abc%')
- 使用**!=、<>、not in**等
- 类型隐式转换(字符串不加引号)
40. MySQL的三大日志及作用?
binlog(二进制日志):
- Server层实现
- 记录所有增删改操作(逻辑日志)
- 用于数据恢复和主从复制
redo log(重做日志):
- InnoDB引擎实现
- 记录物理页的修改
- 用于事务持久性和崩溃恢复
- 循环写,固定大小
undo log(回滚日志):
- InnoDB引擎实现
- 记录事务修改前的数据
- 用于事务回滚和MVCC
41. 什么是两阶段提交?为什么需要?
目的:保证redo log和binlog的一致性
流程:
- Prepare阶段:将事务ID写入redo log,标记为prepared状态
- 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?
- 内存友好:跳表结构简单,只需存储指针和层高,内存占用低;B+树会产生内存碎片
- CPU友好:跳表指针调整只影响相邻节点,符合CPU局部性原理;B+树调整会影响整个树结构
- 代码简单:跳表实现简单,易于维护;B+树实现复杂
- 磁盘无关: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. 如何保证消息队列的幂等性?
方案:
- 分布式锁:使用Redis确保同一消息只被处理一次
- 数据库唯一约束:业务主键设置唯一索引,重复插入会失败
- 消息唯一ID:生产者为每条消息生成唯一ID,消费者处理前检查该ID是否已处理
核心思想:让重复消费不产生副作用
总结
这50道题涵盖了面试中最核心的知识点:
- 并发编程:死锁、线程池、ThreadLocal、锁升级
- 集合框架:HashMap、ArrayList的底层原理
- JVM:内存模型、类加载、垃圾回收
- Spring:自动配置、Bean生命周期、循环依赖
- MySQL:索引、日志、锁、执行流程
- Redis:数据结构、持久化、应用场景
- 消息队列:可靠性、幂等性
复习建议:
- 每天复习10题,5天一轮
- 重点理解原理,而不是死记硬背
- 结合实际项目经验,准备案例说明
- 画图辅助理解,如HashMap结构、三级缓存等
祝你面试顺利!🚀