☰
Java面试核心考点:Bean生命周期、三级缓存、MySQL索引与线程池
2026/10/29 13:18:39 网站建设 项目流程

简介:《Java高频面试题.pdf》是一份面向Java求职者与初中级开发者的面试复习资料,围绕高频考点做结构化梳理,适合临考前的查漏补缺与知识框架重建。全文以1个PDF文件交付,压缩包约396KB,篇幅紧凑,便于在手机或电脑上快速翻阅。内容覆盖Spring Bean生命周期中的实例化、依赖注入、初始化与销毁,Spring MVC从DispatcherServlet到视图渲染的完整执行链路,以及Spring Boot自动装配的启动类注解、@EnableAutoConfiguration与条件装配机制;同时整理SQL执行计划字段释义、常见优化技巧和索引失效场景,并补充B-Tree、Hash、全文、组合、覆盖等索引类型及B+树底层特点,线程池种类、核心参数与拒绝策略也有涉及。已有180人学习,适合用来串联零散知识点、对照面试题自查薄弱环节,形成可复述的答题线索。

1. 一张 Bean 生命周期图,能问出七个问题

Java高频面试题里有一类题看着像背诵题,实际是追问题——Bean 的生命周期。多数人能顺口说完实例化、属性注入、初始化、销毁四个阶段,面试官接一句“构造器注入的循环依赖,三级缓存为什么救不了”,话就断了。这场面试真正考的不是你记住几个名词,而是能不能把一条流程讲成因果链:谁调用了谁、为什么必须卡在这一步、少一步会出什么错。这份题库覆盖的 Spring 全家桶、MySQL 索引与 explain、线程池与并发、集合与事务,套路完全一致,先给结论,再给顺序,最后给边界。适合已经背过一轮、但一被追问就散架的 java 候选人,尤其是工作一到三年、正在准备跳槽的 backend。

2. Spring 高频考点:Bean 生命周期、三级缓存、MVC 流程与自动装配的四段答法

Spring 这几道题几乎必出,而且它们不是四道独立的题,是一条线上的四个观测点。面试官问 Bean 生命周期,是想看你知不知道对象从“造出来”到“能干活”中间隔了什么;问循环依赖,是想看你有没有真正理解“谁先谁后”;问 MVC 流程,是看你能不能把框架职责分工讲清;问自动装配,是看你知不知道 Spring Boot 到底替你省了哪一步。四段答法共用一套结构:阶段 → 谁在调用 → 少这一步会怎样。

2.1 Bean 生命周期:把四个阶段拆成六个可追问的动作

四个阶段是骨架,六个动作才是肉。标准答法先说四个阶段,再在初始化和销毁里各拆成“接口回调”和“配置方法”两步,这样面试官追问“谁先谁后”你才有东西可答。

public class OrderService implements InitializingBean, DisposableBean { private String name; public OrderService() { // 第 1 步:实例化,反射或工厂方法创建对象,此时所有属性还是默认值 System.out.println("constructor"); } public void setName(String name) { // 第 2 步:依赖注入,把属性值和依赖的 Bean 塞进来 this.name = name; } @Override public void afterPropertiesSet() { // 第 3 步:InitializingBean 回调,属性注入完成后由容器调用 System.out.println("afterPropertiesSet"); } public void initMethod() { // 第 4 步:init-method,配置里指定的初始化方法,排在接口回调之后 System.out.println("init-method"); } @Override public void destroy() { // 第 5 步:DisposableBean 回调,容器关闭时触发 System.out.println("destroy"); } public void destroyMethod() { // 第 6 步:destroy-method,配置里指定的销毁方法,排在接口回调之后 System.out.println("destroy-method"); } }

这段代码的价值在于它把“顺序”写死了。初始化的顺序是afterPropertiesSet()在前、init-method在后;销毁的顺序是destroy()在前、destroy-method在后。原因不玄学:容器先判断这个 Bean 有没有实现接口,实现了就直接按接口约定调用,再去读配置里的方法名反射调用。所以接口回调永远优先于配置方法。

答这道题时最容易漏的是“实例化方式”这一层。原文给了两种:反射和工厂方法。反射是容器拿到类的全限定名之后自己 new,工厂方法是容器去调你指定的工厂对象的方法拿返回值。两种方式的差别在于控制权在谁手里——反射是容器主导,工厂方法是开发者主导。面试官如果追问“工厂方法和 FactoryBean 有什么区别”,就是从这里往下挖的。

再补一个高频追问:Bean 是单例的,它线程安全吗?正确答案是“单例只保证容器里只有一个实例,不保证这个实例的成员变量线程安全”。有状态成员就要自己做同步,无状态的服务类天然安全。这句话说出来,比背十遍生命周期都管用。

2.2 循环依赖:三种注入方式,谁能被自动解决

循环依赖是 A 依赖 B、B 又依赖 A,形成闭环。原文把它拆成四种形态:Bean 自我依赖、两个 Bean 相互依赖、多个对象依赖成环、以及按注入方式区分能不能解。

注入方式Spring 能否自动解决原因
字段注入(@Autowired 打在字段上)能对象先 new 出来,属性后塞,中间有“半成品”窗口
setter 注入能同上,注入动作发生在实例化之后
构造器注入不能构造参数必须在 new 的那一刻就备齐,没有半成品窗口
原型作用域不能每次都要新实例,缓存机制用不上

这张表的关键是第三行。构造器注入解不了,不是 Spring 偷懒,而是逻辑上无解:要造 A 得先有 B,要造 B 得先有 A,谁都不肯先变成半成品。面试里答出“构造器注入没有中间态可用”,比背“Spring 用三级缓存解决循环依赖”高一个档次。

原文给的解决路径有三条:重构代码消除循环依赖、改用字段注入或 setter 注入、加@Lazy。三条要按代价排序说。重构最彻底,但影响面不可控;改注入方式最快,但等于把设计问题藏起来;@Lazy是折中,被标记的依赖在真正被使用时才创建,构造器注入的场景下也能生效。面试官最想听的是你知道“哪个是治标、哪个是治本”,而不是只会说“加个 @Lazy 就好了”。

2.3 三级缓存:为什么第二层存半成品,第三层存工厂

三级缓存的名字要能脱口而出,但重点在每一层存什么、为什么要多这一层。

  • 一级singletonObjects:存完整的单例 Bean,拿起来就能用。
  • 二级earlySingletonObjects:存实例化完成、但属性还没注入完的半成品。
  • 三级singletonFactories:存的是对象工厂ObjectFactory,不是对象本身。

查找顺序是从一级到三级,逐层往下找;从三级拿到对象之后,会把它放进二级,同时把三级里的那条记录删掉。这个“上移并清理”的动作要说出来,说明你读过源码而不是只背了名字。

真正的加分点在“为什么非要第三层”。如果只为了存半成品,二级缓存就够了。第三层存在的意义是给 AOP 代理留位置:有些 Bean 需要被代理增强,代理对象和原始对象的引用不是同一个,如果在实例化阶段就把代理对象做出来放进二级,那些根本没有循环依赖的普通 Bean 也会被提前代理,等于给所有 Bean 加了不必要的开销。用工厂的好处是延迟——只有真的发生循环依赖、真的从三级缓存去取的时候,才调用工厂生成那个可能被代理的引用。

注意,这段话是面试官判断你“背过”还是“懂了”的分水岭。答的时候可以用一句收尾:“三级缓存本质上是把代理的时机推迟到了确有必要的那一刻。”

2.4 Spring MVC 九步流程与自动装配的三个注解

MVC 流程背九个名词不难,难在说清“谁必须把控制权交回来”。原始资料里有一句很关键:除了控制器处理请求、视图渲染和响应客户端,其余环节都要把结果返回给前端控制器。这句话就是答题主线。

完整链条是:客户端发 HTTP 请求 → DispatcherServlet 接收 → HandlerMapping 按 URL 找到处理器方法 → HandlerAdapter 调用控制器 → Controller 执行业务逻辑、准备 Model → 返回 ModelAndView 给适配器 → 回到 DispatcherServlet → ViewResolver 解析出真实视图 → 渲染 → 响应客户端。

DispatcherServlet 之所以是核心,就是它全程只做一件事:调度。它不写业务、不渲染页面,所有组件都是它雇来的。面试里被问“为什么叫前端控制器”,答“它是唯一入口,负责拦截所有请求并把活分下去”就够了。

自动装配这条线,从注解拆解开始讲最稳。@SpringBootApplication是三个注解的组合:@SpringBootConfiguration说明当前类是配置类,@ComponentScan默认扫描启动类所在包及其子包,@EnableAutoConfiguration才是自动装配的开关。第三层往下挖:@EnableAutoConfiguration底层封装了@Import,导入的是AutoConfigurationImportSelector,它调用getAutoConfigurationEntry(),再通过getCandidateConfigurations()拿到候选配置类清单。

@Configuration public class CacheAutoConfiguration { @Bean @ConditionalOnMissingBean public CacheManager cacheManager() { // 条件满足时,这个方法的返回值才会被注册进 IOC 容器 return new LocalCacheManager(); } }

这段配置说明自动装配的落地形态:配置类里定义带@Bean的方法,方法上再叠@Conditional系列注解。条件是“容器里还没有这个类型的 Bean”,满足才调用方法、把返回值注册进 IOC。也就是说自动装配不是“无条件帮你创建”,而是“按条件挑着创建”。这段逻辑讲清楚,@ConditionalOnMissingBean、@ConditionalOnClass这些注解就不用一个个背了,它们是同一套机制的不同判断条件。

2.5 真题演练:四道高频追问的答法

第一题:Bean 是单例的,线程安全吗?答题骨架:先分清“单例”和“线程安全”是两个维度 → 单例指容器里只创建一个实例 → 安全与否取决于有没有可变的成员状态 → 无状态服务类天然安全,有状态的要靠局部变量、ThreadLocal 或同步手段。收尾一句“Spring 不替你做线程安全,它只负责对象管理”。

第二题:三级缓存能解决构造器注入的循环依赖吗?答题骨架:直接给结论“不能” → 说原因“构造器注入要求依赖在实例化那一刻就就绪,没有半成品可用” → 给替代方案“改字段/setter 注入、加 @Lazy、或者重构消除环” → 补一句“治本还是重构”。

第三题:DispatcherServlet 和普通 Servlet 有什么区别?答题骨架:普通 Servlet 自己处理请求、自己决定跳转;DispatcherServlet 只做分发,把请求交给 HandlerMapping 和 HandlerAdapter → 好处是职责单一、组件可替换 → 一句话点题“它是调度中心,不是执行者”。

第四题:@ComponentScan 和自动装配是不是一回事?答题骨架:不是 →@ComponentScan扫的是你自己写的、带@Component之类注解的类 → 自动装配加载的是框架预置的候选配置类,还要过@Conditional判断 → 两者一个管“你的代码”,一个管“框架的默认配置”。

3. MySQL 高频考点:explain 字段、索引失效与 B+ 树层数估算

数据库这块的面试有个特点:问法很散,考点很集中。绕来绕去就三件事——这条 SQL 走没走索引、为什么没走、索引底层长什么样。把 explain 的字段按“能不能看出索引生效”重新分组,比按字母顺序背十个字段效率高得多。

3.1 explain 十个字段,按“索引有没有用上”分三组

第一组是定位信息,回答“这条 SQL 在干嘛”:id、select_type、table。id相同表示从上往下顺序执行,id不同表示 id 大的优先执行;select_type区分普通查询、子查询、联合查询;table就是这一行讲的是哪张表。

第二组是索引判断,回答“用没用上索引”:type、possible_keys、key、key_len、ref。这五个字段连起来读就是一条完整结论。

字段看什么危险信号
type访问类型,性能从好到坏出现 index 或 ALL
possible_keys理论上可用的索引为空说明压根没有相关索引
key实际用上的索引为 null 表示没走索引
key_len用到的索引长度组合索引里长度明显偏短,说明只用上前几列
ref哪个列或常量和索引一起筛选与预期不符说明关联条件写歪了

type这一列要能背出好坏顺序:system最好,往下是const、eq_ref、ref、range、index,最差是ALL全表扫描。中间还有几个特殊值:ref_or_null表示除了匹配索引值还要找 null,index_merge表示多个索引合并使用,unique_subquery和index_subquery用在子查询场景。答题时不必十个全背,把主序列顺畅说出来,再补一句“特殊值出现在特定查询形态下”,就够了。

第三组是代价信息,回答“这条 SQL 有多贵”:rows和Extra。rows是优化器估算要扫多少行,注意是估算不是精确值;Extra信息量最大,出现Using filesort说明排序没走索引,出现Using temporary说明用了临时表,出现Using index反而是好事,表示覆盖索引生效、不用回表。

3.2 索引失效的七种写法与改写方案

索引失效的场景,面试官问的不是“有哪些”,而是“为什么会失效”。失效的本质只有一个:数据库没法用索引的有序性去定位数据。凡是让列值在索引里的顺序变得不可预测的写法,都会失效。

-- 失效:索引列上做运算,B+ 树没法按原值定位 SELECT * FROM orders WHERE id + 1 = 100; -- 改写:把运算挪到等号右边 SELECT * FROM orders WHERE id = 99; -- 失效:前置百分号的模糊查询,左边无序可用 SELECT * FROM orders WHERE name LIKE '%张'; -- 可用:只有右模糊,前缀仍然有序 SELECT * FROM orders WHERE name LIKE '张%'; -- 失效:对索引列使用函数 SELECT * FROM orders WHERE DATE(create_time) = '2024-01-01'; -- 改写:改成范围比较 SELECT * FROM orders WHERE create_time >= '2024-01-01 00:00:00' AND create_time < '2024-01-02 00:00:00';

第一条和第二条是一组,讲的是“表达式破坏了索引列的有序可比较性”;第三条和第四条是一组,讲的是“模糊查询只有前缀确定时索引才能用”;第五条和第六条是一组,讲的是“函数把列值整体换掉了”。三组共用一个原理,答的时候先给原理再举例子,比列清单有说服力。

剩下的几种:NOT和!=这类否定条件,优化器判断扫描范围太大,往往直接放弃索引;or连接的条件里只要有一侧没索引,整个查询就走全表;组合索引要满足最左前缀,只查后面的列用不上;in的值给得太多,优化器会认为不如全扫。还有一条常被追问的补充:字符串类型的列拿数字去比较,会触发隐式类型转换,等价于在列上套了一层函数,索引同样失效。

顺带把 SQL 写法层面的优化讲完,这些都是原文里给的:查询时写明具体字段而不是select *;能内连接就别用外连接;排序字段没索引就尽量不排序;union all能替代union就替代,省掉去重开销;分页只有一页数据时加limit 1提前终止。

3.3 B+ 树为什么三层够用:一次算给你看

这道题答“因为 B+ 树矮胖”,等于没答。要给出估算过程。

B+ 树是多路平衡查找树,非叶子节点只存索引不存数据,所有数据都在叶子节点,叶子节点之间用指针连成链表,所以范围查询特别顺——找到起点顺着链表走就行,不用回到根节点。

估算的关键在非叶子节点能装多少条。一个页按 16KB 算,一个索引 key 一般 8 字节,一个指针一般 6 字节,加起来 14 字节左右,16384 除以 14 约等于 1000 出头。也就是说,一个非叶子节点大约能指向 1000 个下级节点。那么两层就能覆盖百万级数据,三层就能到千万级甚至更多。这就是为什么实际生产里的 B+ 树通常只有两到四层——层数少意味着一次查询只要几次磁盘 IO。

注意这是量级估算,不是精确值。行数据变大、key 变长,能装的条数都会下降。面试里主动说出“这是量级估算,具体取决于行大小”,比报一个精确数字更可信。

反过来问“为什么不用 B 树”,答案就在“非叶子节点存不存数据”上:B 树非叶子节点也存数据,同样的页能放的指针就少,树就更高,磁盘 IO 更多;而且 B 树做范围查询要在树里来回跳,不像 B+ 树能顺着叶子链表扫。问“为什么不用哈希索引”,答案是哈希只支持等值查询,范围查询和排序都用不上。

3.4 真题演练:一条慢 SQL 从哪儿开口

题目:线上有一条查询变慢了,你怎么排查。

答题骨架分四步。第一步先确认现象,是偶发还是持续、是单条慢还是整体慢,排除掉数据库整体负载的问题。第二步拿 explain 看执行计划,按上一节的三组字段读一遍,重点看type有没有掉到index或ALL、key是不是 null、rows估算多大、Extra有没有 filesort 或 temporary。第三步判断是索引缺失、索引失效还是 SQL 写法问题,分别对应建索引、改写条件、调整语句结构。第四步验证,改完再跑一次 explain 对比rows和type。

这道题最忌讳一上来就说“加索引”。加索引是结论,不是过程,面试官要听的是你怎么定位到“该加索引”。

题目:组合索引 (a, b, c),查询条件只有 b = 1,能用上吗。

答案是用不上,因为不满足最左前缀。但可以补一句“如果查询只涉及 a、b、c 三列,可能走覆盖索引的扫描,但这不是精确定位”。这句补充能体现你知道覆盖索引和前缀匹配是两回事。

题目:深分页怎么优化。

答题骨架:先解释慢在哪——limit 1000000, 10要扫描并丢弃前面一百万行;再说思路——先用覆盖索引把主键捞出来,再拿主键回表取数据,减少回表次数;最后补一句“业务上能限制翻页深度就从业务上限制”。这类题通常这么答,重点是展示你理解“为什么要回表”而不是背一个模板。

4. 并发与集合高频考点:线程池七参数、六种状态与 HashMap 扩容

并发这块是最容易答成名词堆砌的地方。线程池问你七个参数,你能背出来;再问“任务提交之后到底先去哪”,就乱了。这一章的答题原则是:任何参数、任何状态,都要能放进一条时间线里说。

4.1 线程池:四个快捷工厂方法背后的七个参数

先给结论:四个Executors工厂方法都是七个参数的预设组合,面试官问它们,本质是问“你敢不敢在生产环境直接用”。

工厂方法线程数特征队列实际风险
newFixedThreadPool固定数量无界队列任务堆积不拒绝,堆内存容易被撑爆
newCachedThreadPool可动态调整无界队列最大线程数极大,短任务洪峰下线程数失控
newSingleThreadExecutor只有一个线程无界队列任务串行,一个卡住全堵
newScheduledThreadPool支持定时与周期按需周期任务异常会静默影响后续调度

七个参数是:corePoolSize核心线程数、maximumPoolSize最大线程数(等于核心线程数加救急线程数)、keepAliveTime救急线程的生存时间、unit时间单位、workQueue任务队列、threadFactory线程工厂、handler拒绝策略。

任务提交后的走向是一条固定时间线:核心线程没满就开核心线程 → 核心线程满了进队列排队 → 队列满了才创建救急线程 → 救急线程也到上限,触发拒绝策略。这条线要说顺,因为这解释了为什么“无界队列 + 固定线程数”的组合很危险——队列永远不满,救急线程永远创建不出来,最大线程数这个参数形同虚设。

ThreadPoolExecutor executor = new ThreadPoolExecutor( 4, // 核心线程数:常驻处理任务的数量 8, // 最大线程数:核心 + 救急线程的上限 60L, // 救急线程空闲多久后回收 TimeUnit.SECONDS, // 时间单位 new ArrayBlockingQueue<>(200), // 有界队列:满了才会去创建救急线程 r -> new Thread(r, "order-worker"), // 线程工厂:给线程起名,排查问题时能认出来 new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:让提交任务的线程自己跑 ); executor.execute(() -> { // 提交一个任务 });

这段代码里每个参数都带注释,重点看第四和第七个。队列用有界队列,救急线程才有机会被创建;拒绝策略选CallerRunsPolicy,队列打满时让调用方线程自己去执行,等于自动给上游降速,比直接抛异常温和。

四种拒绝策略要能对比:AbortPolicy直接抛异常,是默认策略;CallerRunsPolicy用调用者线程执行;DiscardOldestPolicy丢掉队列里最靠前的一个任务,再执行当前任务;DiscardPolicy静默丢弃新任务。答题时加一句“默认的抛异常策略最安全,因为问题能被看见”,比单纯罗列强。

4.2 线程状态与优雅退出

线程状态的标准说法是六种:新建、可运行、阻塞、等待、超时等待、终止。有些资料会把“可运行”再拆成“就绪”和“运行中”讲成七种,那是因为操作系统调度和 JVM 状态不是一回事——线程拿到执行资格不代表 CPU 立刻给它时间片。这个细节提一句,能显出你不是只背了枚举值。

三种“停住”的状态要分清:阻塞是等锁,等待是等别人通知(wait()、join()、park()),超时等待是带时限的等待(sleep(ms)、wait(timeout))。区别在于触发条件和谁来唤醒。

线程池自身的状态是另一套:运行中、关闭(shutdown,不再接新任务但会做完队列里的)、停止(stop,不再接新任务并尝试中断正在执行的)、整理中(任务都执行完,正在回收资源)、已终止(资源全部释放)。这套状态和shutdown()/shutdownNow()的语义直接对应:前者等已提交任务跑完再关,后者立刻停并返回没执行的任务列表。

优雅退出的四步是原文给的:设退出标志位让线程自己收尾、对阻塞中的线程调interrupt()打断阻塞、用join()等它真正结束、最后清理文件句柄和连接这类资源。四步顺序不能乱,先通知、再打断、再等待、最后清理,跳步就是资源泄漏。

4.3 三大特性与各自的解法

特性问题从哪来常见解法
原子性复合操作被拆成多条指令,中间可能被切走synchronized、Lock
可见性线程各自的工作内存不共享,改了别人看不见volatile、锁
有序性编译器优化和 CPU 指令重排改变执行顺序volatile、锁

答题时举i++这个例子最省事:它看着是一行,实际是读、加、写三步,任何一步之间都可能被其他线程插进来,所以不是原子操作。要保证正确就加锁,或者用原子类。

可见性要说清 volatile 的边界:它保证每次读都读到最新值、保证写读之间的顺序,但不保证复合操作的原子性。所以volatile int i然后i++依然是错的。这个“坑”面试官很爱听,因为它是区分“背过 volatile”和“用过 volatile”的分界线。

4.4 HashMap 与集合选型

HashMap 的答法按版本分两段讲。JDK 1.7 及以前是数组加链表,扩容时用头插法迁移数据,多线程环境下可能出现链表成环,造成查询死循环。JDK 1.8 改成数组加链表加红黑树,扩容迁移改成尾插法,成环问题解决;链表长到一定长度会转红黑树,查询从线性退化变成对数。

但要补一句:1.8 解决的是成环,不是线程安全。并发场景下依然可能丢数据、size 不准,该用并发容器还是得用。这句话是这道题的标准收尾。

集合选型按“有序性 + 去重 + 随机访问 + 线程安全”四个维度答,比一个个背实现类高效。

需求首选理由
随机访问多、增删少ArrayList数组结构,按下标访问是常数时间
头尾增删多LinkedList / ArrayDeque链表结构,插入删除不用挪元素
去重且不在意顺序HashSet哈希定位,判重接近常数时间
去重且要保留插入顺序LinkedHashSet哈希加链表,兼顾性能与顺序
要按大小排序TreeSet / TreeMap红黑树,天然有序
键值查找HashMap哈希表实现,查询高效
要按插入顺序遍历键值LinkedHashMap链表维护顺序

List 保持顺序、允许重复;Set 用于去重和存在性判断;Map 用于键值查找。这三句话是三句结论,答完再补实现类差异,顺序反过来就成了背书。

4.5 真题演练

题目:线程池参数怎么设。答题骨架:先分类,任务是 CPU 密集型还是 IO 密集型 → CPU 密集型核心线程数不宜超过核数太多,IO 密集型可以适当放大,因为线程大部分时间在等 → 再说队列必须有界,否则最大线程数没意义 → 最后说拒绝策略和监控,池子要有名字、要能看到活跃线程数和队列积压。收尾一句“参数没有标准答案,压测出来的才算数”,比报一个公式可信。

题目:为什么阿里规约不让用 Executors 创建线程池。答题骨架:因为四个工厂方法里有三个用无界队列、一个最大线程数接近无限 → 无界队列会让最大线程数失效、任务无限堆积 → 结论是手动 new ThreadPoolExecutor,把七个参数明确写出来。

题目:volatile 能保证 i++ 安全吗。答题骨架:不能 → 说明它只保证可见性和有序性 →i++是读改写三步复合操作 → 要原子性得用锁或原子类。

题目:HashMap 为什么线程不安全。答题骨架:分段说版本 → 1.7 头插法扩容可能成环 → 1.8 改尾插解决了成环,但并发写入依然可能覆盖、size 不准 → 结论是并发场景换并发容器。

5. 易错与失分点:六道对比题,答错都因为少说一层

面试里真正拉开差距的不是难题,是那些“以为自己会”的对比题。这类题的共同特征是:你能说出定义,但说不出边界;能说出结论,但说不出为什么。下面六道,每条都按“错在哪 → 为什么错 → 怎么答对”拆开。

5.1 事务失效:四类场景的判断顺序

错在哪。大多数人能背出七八种失效场景,但被追问“你怎么知道它失效了”,答不上来。场景是并列的,没有判断顺序,说出来就是一盘散沙。

为什么错。因为事务失效的本质只有一个:调用没有经过代理对象。Spring 的事务是靠 AOP 代理加进去的,只要这次调用绕开了代理,@Transactional就是个普通注解,什么也不做。

怎么答对。按“代理有没有生效”这个统一视角重新组织四类场景。

第一类,方法不是 public。代理拦不住非 public 方法,注解直接失效。第二类,同类内部调用。A 方法调本类的 B 方法,走的是this.b(),不经过代理,B 上的注解不生效。第三类,异常被吃掉或者类型不匹配。默认只在抛出运行时异常和 Error 时回滚,抛出受检异常不回滚;抛了异常又被 try-catch 吞掉、没往外抛,同样不回滚。第四类,传播行为配错。子方法配成REQUIRES_NEW,会另开一个独立事务,子事务提交了、外层回滚,数据就脏了。另外还有几个容易漏的:方法被final修饰没法被代理、类没交给 Spring 管理、多线程环境里新线程不在原事务上下文、表本身不支持事务。

答题时先说“事务失效九成是代理没生效”,再往上挂具体场景,最后补一句“验证手段是看调用有没有跨代理对象,或者打开事务日志看有没有真的开事务”。这三层给出来,比背清单高一个段位。

隔离级别和传播行为也别混。隔离级别管的是“多个事务之间互相能看到多少”,从读未提交、读已提交、可重复读到串行化,一致性越来越高、并发越来越差;传播行为管的是“一个事务方法调用另一个事务方法时,事务怎么传下去”,默认的REQUIRED是有就加入、没有就新建。七种传播行为里最常被问的是REQUIRED、REQUIRES_NEW和NESTED三个,前两个是“加入还是新建”的区别,第三个是嵌套,外层回滚会带着内层一起回滚。

5.2 拦截器与过滤器:执行顺序说反就是硬伤

错在哪。把两者的执行顺序说成“拦截器在外、过滤器在内”,或者干脆说“差不多,都是拦截请求”。

为什么错。两者根本不在一个层次上。过滤器属于 Servlet 容器级别,请求进容器先过它,静态资源、动态请求都拦得住,连 HTTP 头都能改。拦截器属于框架级别,只有进到框架管理的请求才会被它拦,能拿到方法参数和目标对象。层次不同,顺序就定了:过滤器在前,拦截器在后。

怎么答对。用一张对比表把五个维度一次性铺开。

维度过滤器拦截器
所属层次Servlet 容器框架层(如 Spring MVC)
实现机制实现 Filter 接口,函数回调动态代理,反射调用
生命周期随 Web 应用启动创建、关闭销毁随请求创建、请求结束销毁
能拿到什么请求和响应对象方法参数、目标对象
擅长什么编码、安全校验、日志这类与业务无关的全局处理权限判断、业务前置校验这类与业务相关的处理

执行顺序再补一层细节:过滤器链按配置顺序依次调用doFilter,处理完还能在响应返回前再拦一次;拦截器是preHandle正序执行、postHandle逆序执行、afterCompletion也是逆序执行。这个“正序进、逆序出”的细节说出来,说明你真的跟过调用栈。

5.3 集合与 Redis 类型:选型题别背定义

错在哪。问“Redis 有哪些数据类型”,答“字符串、哈希、列表、集合、有序集合”,然后没了。问“什么时候用有序集合”,答“需要排序的时候”。

为什么错。数据类型题考的是选型,不是名词。每种类型对应一类访问模式,答不出模式就等于没答。

怎么答对。每种类型都绑一个典型场景说。字符串存任何形式的字符串,包括数字、文本、序列化对象,适合做计数和缓存;哈希存字段值对,适合存对象的多个属性,改一个字段不用整体重写;列表能当队列和栈用,适合消息队列、任务队列;集合无序不重复,支持并集交集差集,适合去重和集合运算;有序集合每个成员带一个分数、按分数排序,适合排行榜和带权重的队列。

再补一句选型心法:先问“要不要去重”,再问“要不要有序”,最后问“要不要按分数排”。三个问题问完,类型就定了。这句话比背五段定义有用。

5.4 死锁与重复提交:两个场景题的完整闭环

死锁。错在只答“两个线程互相等对方的锁”,没有下文。答对的顺序是:先给定义(多个线程无限期等待彼此释放资源),再说四个必要条件(互斥、占有并等待、不可抢占、循环等待),再给避免手段——统一加锁顺序、避免嵌套加锁、设置获取锁的超时时间、用资源分配图检测环路、必要时引入抢占。最后补一句“破坏其中任意一个条件,死锁就成立不了”,这句话是收口。

重复提交。错在只说“用 Redis 加个锁”,没有闭环。完整的答法是拦截器或过滤器加 Redis 组合:拿到请求对象,取请求路径、请求参数、登录用户 id,三者拼成一个 key;对这个 key 执行自增;返回值是 1 说明是第一次提交,给它设 15 秒过期;返回值大于 1 说明 15 秒内重复提交了,直接拦掉。

// 拦截器里的核心逻辑,key 由路径、参数、用户 id 拼成 String key = "repeat:" + userId + ":" + path + ":" + params; Long count = redis.incr(key); if (count != null && count == 1L) { // 第一次提交,设置 15 秒窗口,窗口内不允许重复 redis.expire(key, 15); } else { // 已经是第二次及以后,直接拦截 throw new IllegalStateException("请勿重复提交"); }

这段代码的关键在incr的原子性:单次自增不会被并发打断,所以两个请求同时到达时,一定是一个拿到 1、一个拿到 2,不会都拿到 1。expire设 15 秒是给这个 key 一个自然消失的窗口,过了窗口用户又能正常提交,不会永久锁死。key 里带用户 id 是为了区分人,带路径和参数是为了区分接口和内容——只按用户 id 做 key,会把用户在不同页面的正常操作也拦掉。这个细节面试官很爱追问,答出来就是加分项。

6. 答题技巧:把背过的点组织成面试官想听的顺序

面试答题和写文档是两回事。文档可以铺垫,面试不行,前三句话没落到点上,面试官的注意力就跑了。我后来固定用一个三段式,几乎能套住这份题库里的所有题:先给结论,再给顺序,最后给边界。

结论就是一句话回答“是什么”。问 Bean 生命周期,第一句是“四个阶段,初始化和销毁各拆成接口回调和配置方法两步”。问索引失效,第一句是“本质是破坏了索引列的有序可比较性”。先把结论扔出去,面试官立刻知道你抓到点了,后面的展开他才愿意听。

顺序是把结论铺开成时间线或因果链。Bean 生命周期按“实例化 → 注入 → 初始化 → 使用 → 销毁”走;线程池按“核心线程 → 队列 → 救急线程 → 拒绝策略”走;MVC 按九个组件依次交接走。凡是带“流程”“原理”“执行”字样的题,都用时间线答,别用并列结构。

边界是最容易漏也最容易加分的一层。答完主流程,主动补一句“这个方案在什么情况下不成立”。三级缓存补一句“构造器注入救不了”;volatile 补一句“不保证原子性”;线程池补一句“无界队列会让最大线程数失效”。这一句往往就是面试官准备追问的那个点,你先说了,他就得换题。

追问预判可以做成一张自查表,准备的时候对着过一遍:

你答的内容面试官大概率追问提前准备好的那句话
三级缓存解决循环依赖构造器注入为什么不行构造参数必须在实例化那一刻就绪,没有半成品窗口
自动装配靠 @Conditional条件和配置类谁先谁后先加载候选配置类,再逐个判断条件是否满足
索引失效因为用了函数那 like 前置百分号呢同样是破坏了前缀有序性,无法定位起点
线程池有七个参数队列为什么必须有界无界队列会让救急线程永远创建不出来
事务靠 AOP 代理同类内部调用为什么失效走的是 this 调用,没经过代理对象

这张表的用法是倒着用:先想面试官会问什么,再倒推自己那句话该怎么说得更准确。准备时间有限的时候,把每道题的“边界句”单独抄一遍,比通读十遍资料管用。

还有一个血泪经验:别在面试里主动展开你没把握的细节。有人答 HashMap 顺口提了一句红黑树旋转,被追问“左旋右旋分别在什么条件下触发”,直接翻车。答题的边界感比知识量更影响评分——说到的每个点都要能接住第二层追问,接不住的就别提。

从那以后我每次准备面试,都会强制走一遍“结论 → 顺序 → 边界”这三步,再对着追问表自问自答一轮,答不上来的当场补进笔记。这份 Java 高频面试题整理的就是这么一轮一轮筛下来的东西,Spring 全家桶、MySQL 索引与 explain、线程池与并发、集合与事务,按考点分块,每块都能直接拿来对着练。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询