大厂 4 年经验 Java 面试题深入解析(10 道)
这篇文章不是面向校招,也不是面向只会背八股的初级候选人,而是针对已经有 4 年左右实际项目经验、准备冲击大厂的 Java 工程师。
大厂面试更看重你是否能把基础原理、线上问题、设计取舍和工程落地讲透。所以这 10 道题我会按题目、关键点、标准答案、可能追问四个模块展开。页面上只展示正常标题,特殊标记保留在 HTML 源码注释中,方便后续程序继续识别。
第 1 题:说一下 synchronized、ReentrantLock、CAS 三者分别适合解决什么问题?
题目
- 说一下 synchronized、ReentrantLock、CAS 三者分别适合解决什么问题?如果线上高并发场景下锁竞争严重,你会怎么分析和优化?
关键点
- 悲观锁和乐观锁的适用边界。
- synchronized 在 JDK 1.6 之后的锁升级机制。
- ReentrantLock 的可中断、可超时、公平锁、条件队列等能力。
- CAS 的 ABA 问题、自旋开销和适用前提。
- 如何从线程状态、锁持有时间、热点代码路径分析竞争瓶颈。
标准答案
- synchronized适合临界区清晰、代码块级同步、对功能要求不复杂的场景。它是 JVM 原生语义,使用简单,可读性高,在低到中等竞争下性能已经足够。
- ReentrantLock适合需要更强控制能力的场景,例如可中断获取锁、定时尝试加锁、多个条件队列、排查死锁或实现公平策略。它本质上基于 AQS,扩展性更强。
- CAS适合无锁化、短时间原子更新的场景,例如计数器、状态位切换、队列头尾指针更新。前提是冲突不能过高,否则自旋失败成本会放大。
- 线上锁竞争严重时,先确认问题是锁粒度过大、持锁时间过长、热点对象竞争,还是不合理的串行化逻辑。可以通过 Arthas、jstack、JFR、async-profiler 观察 BLOCKED 线程、热点方法和锁等待时间。
- 优化时优先考虑缩小临界区、拆分锁、把串行逻辑改成分段并行、把粗粒度对象锁改成更细粒度结构,或者直接使用并发容器、LongAdder、无锁队列等替代方案。
可能追问
- 偏向锁、轻量级锁、重量级锁分别解决什么问题?
- LongAdder 为什么比 AtomicLong 在高并发下性能更好?
- AQS 的核心数据结构是什么?
第 2 题:你如何理解线程池参数,线上线程池应该怎么设?
题目
- 你如何理解 corePoolSize、maximumPoolSize、workQueue、keepAliveTime 等线程池参数?如果一个接口 RT 飙高并伴随线程堆积,你会如何调整?
关键点
- 线程池创建线程和入队的执行顺序。
- CPU 密集型和 IO 密集型任务的配置差异。
- 无界队列导致的问题。
- 拒绝策略与系统降级的关系。
- 动态监控池活跃度、队列长度、任务耗时的重要性。
标准答案
- 线程池不是参数背诵题,核心是理解任务模型。提交任务时通常先补齐核心线程,再尝试入队,队列满了才扩到最大线程数,最后触发拒绝策略。
- CPU 密集型任务一般线程数接近 CPU 核数;IO 密集型任务可以适当放大,但不能脱离下游资源能力,否则只是把堵塞转移到数据库、Redis 或 RPC 下游。
- 线上不建议默认使用无界队列。无界队列会掩盖流量突刺,最终把问题拖成内存膨胀、请求超时和 Full GC,而不是第一时间暴露容量边界。
- 如果 RT 飙高并伴随线程堆积,先看是任务变慢还是流量激增。任务变慢要继续分析慢 SQL、远程调用、锁等待、下游超时;流量激增则要限流、隔离、快速失败,而不是一味加线程。
- 线程池需要业务命名、指标监控和分场景隔离,不能所有任务共用一个池。否则某个慢任务类型会把整个应用的异步能力拖垮。
可能追问
- 为什么阿里规范不建议直接使用 Executors 创建线程池?
- CallerRunsPolicy 在什么场景下反而是比较好的策略?
- 线程池队列积压时,如何判断是扩容还是降级?
第 3 题:说一下 JVM 内存结构、垃圾回收器选型以及 Full GC 排查思路。
题目
- 说一下 JVM 内存结构、垃圾回收器选型以及 Full GC 排查思路。如果线上频繁 Full GC,但堆看起来并不大,你会怎么判断根因?
关键点
- 程序计数器、虚拟机栈、本地方法栈、堆、方法区的职责。
- 新生代、老年代、对象晋升、分配担保。
- CMS、G1、ZGC 的适用场景。
- 内存泄漏、对象存活过长、晋升过快、元空间膨胀等不同问题类型。
- jstat、jmap、MAT、GC 日志分析方法。
标准答案
- JVM 内存结构是基础,但面试更关注你有没有线上经验。堆主要放对象实例,栈主要放栈帧和局部变量,方法区承载类元数据和常量等内容。
- GC 选型不能只说“G1 适合大堆”。要结合停顿目标、堆大小、对象生命周期和业务 RT 要求。4 年经验候选人至少要能讲清为什么项目从 CMS 切到 G1,或者为什么没有切。
- 频繁 Full GC 时,不能只看堆总量,要看 Old 区增长速度、晋升失败、Humongous 对象、元空间、直接内存和引用链持有关系。很多问题并不是“堆不够”,而是对象回不掉。
- 排查时通常先看 GC 日志,确认 Full GC 触发原因;再通过 jmap 导堆、MAT 看 dominator tree 和大对象路径;如果是短时流量问题,再结合应用日志和监控看是否有缓存击穿、批量加载、消息堆积。
- 如果是元空间导致 Full GC,要重点看动态代理、Groovy/Janino、频繁类加载和自定义类加载器泄漏,不要只盯着 Java 堆。
可能追问
- 对象什么时候会进入老年代?
- G1 的 Region、Remembered Set、Mixed GC 是怎么工作的?
- MAT 里看到一个大 Map 持有大量对象,你接下来怎么判断是不是泄漏?
第 4 题:Spring 事务为什么会失效?你在线上遇到过哪些坑?
题目
- Spring 事务为什么会失效?你在线上遇到过哪些常见事务坑,分别是怎么定位和修复的?
关键点
- AOP 代理机制是事务生效的前提。
- 同类内部调用、private 方法、final 方法的代理问题。
- 默认回滚规则只覆盖 RuntimeException。
- 事务边界与 RPC、MQ、异步线程的关系。
- 长事务、批量事务、锁范围扩大的风险。
标准答案
- Spring 声明式事务本质上依赖代理拦截,所以最常见的失效场景是同类内部方法调用绕过代理,导致
@Transactional根本没有被织入。 - 第二类坑是异常处理。默认情况下只有
RuntimeException和Error才会触发回滚,如果你捕获异常后吃掉了,或者抛出的是受检异常却没显式配置回滚规则,事务也可能提交。 - 第三类坑是边界错位。例如在事务里调用远程接口、发 MQ、做长时间计算,会导致数据库连接长时间占用,放大锁持有范围,严重时引发雪崩。
- 线上定位通常会结合 SQL 执行时间、事务日志、慢查询、死锁日志和业务代码调用路径一起看。很多所谓“事务不生效”,其实是事务开得太大或者异常处理方式有问题。
- 修复时要么调整服务拆分和代理调用路径,要么把事务边界缩到真正需要原子性的数据库操作上,并通过本地消息表、最终一致性方案替代跨系统强事务。
可能追问
- 事务传播行为里 REQUIRED、REQUIRES_NEW、NESTED 的区别是什么?
- 为什么不建议把事务包住整个 for 循环批处理?
- 分布式事务你做过哪些方案?
第 5 题:MySQL 索引为什么会失效?你如何判断 SQL 需要怎么改?
题目
- MySQL 索引为什么会失效?如果一个查询明明建了联合索引但执行计划仍然很差,你会怎么分析?
关键点
- B+Tree 索引结构与回表原理。
- 最左前缀原则和范围查询对后续列的影响。
- 函数操作、隐式类型转换、模糊查询、低区分度列导致的失效。
- 执行计划里 type、rows、filtered、extra 的含义。
- 索引设计要结合查询模式,而不是只看字段频率。
标准答案
- 索引失效不是单一原因,常见场景包括条件没命中最左前缀、对索引列做函数运算、类型不匹配引发隐式转换、
like '%xx'无法利用前缀有序性,以及优化器判断全表扫描成本更低。 - 联合索引设计必须围绕高频查询路径。字段顺序通常要综合考虑过滤性、排序、分组和覆盖查询,而不是简单把“区分度最高的字段放最前面”。
- 如果执行计划差,先看
explain,再结合show warnings、实际 SQL 参数、表统计信息、数据分布确认是不是优化器误判。很多线上问题是测试数据太少,看不出真实代价。 - SQL 优化不是只会加索引。必要时要改写 SQL、分离冷热数据、避免大分页、引入冗余字段或预聚合表,把复杂实时查询改成离线或异步计算。
- 面试里如果你能讲到“索引命中不代表一定快,扫描行数和回表成本才关键”,说明你的 SQL 调优视角是成熟的。
可能追问
- 覆盖索引为什么能提升性能?
- 什么情况下 order by 能利用联合索引?
- 深分页除了延迟关联还有哪些优化方式?
第 6 题:Redis 在缓存设计里最容易出什么问题?
题目
- Redis 在缓存设计里最容易出什么问题?如果线上同时出现缓存穿透、击穿、雪崩,你会怎么分层处理?
关键点
- 缓存穿透、击穿、雪崩的定义与区别。
- 布隆过滤器、空值缓存、互斥锁、逻辑过期等方案。
- 热点 key 识别和多级缓存思路。
- 缓存一致性设计优先级和可接受的不一致窗口。
- 大 key、热 key、慢查询、持久化对 Redis 稳定性的影响。
标准答案
- 缓存穿透是请求根本不存在的数据,击穿是热点 key 恰好失效导致大量请求打到数据库,雪崩则是大量 key 同时失效或 Redis 集群异常导致请求整体回源。
- 穿透一般用布隆过滤器、参数校验、空值短 TTL 缓存解决;击穿一般用互斥重建、逻辑过期、后台刷新解决;雪崩则要做 TTL 打散、多级缓存、限流降级、集群高可用和业务兜底。
- 缓存一致性不能只说“先删缓存再写数据库”。你要明确是读多写少、是否允许短暂不一致、是否需要消息补偿,以及删缓存失败后的重试机制。
- Redis 本身也会出问题,比如 big key 导致网络阻塞,热 key 导致单分片打满,AOF 重写或 RDB 持久化带来抖动。这些都需要监控命中率、慢日志、网络吞吐和实例 CPU。
- 更成熟的回答是把 Redis 放回系统设计里讨论,而不是当成“背概念工具”。
可能追问
- 延时双删为什么不能保证强一致?
- 布隆过滤器为什么会有误判?
- 热点 key 很集中时,除了加本地缓存还能怎么做?
第 7 题:分库分表之后,哪些问题最难处理?
题目
- 分库分表之后,哪些问题最难处理?除了路由规则,你觉得真正影响系统复杂度的点有哪些?
关键点
- 分片键选择与数据倾斜。
- 跨库分页、跨库排序、聚合统计的代价。
- 全局唯一 ID、扩容迁移、一致性和事务问题。
- 业务侧是否接受查询能力下降。
- 中间件方案和业务侵入式方案的取舍。
标准答案
- 分库分表最难的不是“写个路由器”,而是承受由它带来的查询能力下降、运维复杂度上升和数据迁移成本。尤其是二次扩容,往往比第一次拆分更痛。
- 分片键选择决定了系统上限。如果按用户 ID 分,但业务高频查询是按订单状态和时间范围扫单,那后续很多查询都要跨分片聚合,性能和开发成本都很差。
- 跨库事务、全局 ID、分布式唯一约束、数据修复和历史数据回迁都需要提前设计。很多团队只解决“写进去”,没解决“怎么查、怎么改、怎么扩”。
- 当数据量还没逼到必须拆分时,优先通过索引优化、冷热分离、归档、读写分离、表结构优化提升容量,别过早上分库分表。
- 真正成熟的回答应该体现你知道分库分表是业务和架构共同承担复杂度,而不是数据库层的魔法开关。
可能追问
- 你们为什么选择这个分片键?有没有踩过数据倾斜的坑?
- 扩容时怎么做双写或数据迁移校验?
- 跨库分页查询你会怎么设计?
第 8 题:一次线上接口 RT 突然升高,你的完整排查路径是什么?
题目
- 一次线上接口 RT 突然升高,但错误率不高。你会如何在最短时间内完成定位?请给出一个完整排查路径。
关键点
- 先看范围,是单机、单接口、单机房还是全链路问题。
- 区分 CPU 打满、线程阻塞、GC 抖动、数据库慢、Redis 慢、下游超时。
- 监控、日志、线程栈、火焰图、链路追踪联动使用。
- 先止血再定位,避免一边排查一边扩大故障。
- 输出可复盘的方法论,而不是零散工具名。
标准答案
- 我会先判断故障范围:是某个接口、某个实例、某个可用区还是全站问题。这个步骤决定了是代码问题、机器问题还是下游依赖问题。
- 然后看四类核心指标:应用层 RT/QPS/错误率,JVM 的 GC/堆/线程,系统层 CPU/内存/负载/网络,依赖层的 DB、Redis、MQ、RPC 耗时。先建立时间线,再缩小嫌疑范围。
- 如果线程堆积,就抓线程栈看是否卡在锁、SQL、网络 IO 或某段业务代码;如果 CPU 飙高,就看火焰图;如果 RT 高但 CPU 不高,往往是等待型问题,重点查下游超时或连接池耗尽。
- 真正好的排查不是“什么都查”,而是先做止血动作,例如摘流量、限流、降级、回滚,再继续做根因定位,避免故障窗口扩大。
- 最后要能沉淀成预案:如何提前监控、如何自动扩容、如何加熔断隔离、如何优化慢依赖,而不是只会事后救火。
可能追问
- 如果 CPU 不高但线程很多都在 WAITING,你会优先怀疑什么?
- Arthas 里你最常用哪几个命令?
- 一次故障复盘文档你通常会怎么写?
第 9 题:设计一个高并发秒杀系统,你重点解决哪些问题?
题目
- 设计一个高并发秒杀系统,你重点解决哪些问题?请不要只回答“用 Redis 扣库存”。
关键点
- 流量削峰、限流、验证码、预约、静态化。
- 库存一致性、超卖控制、幂等处理。
- 异步削峰、MQ、最终一致性。
- 热点数据、本地缓存、分片和隔离。
- 风控、防刷、监控和降级策略。
标准答案
- 秒杀的关键不是“扣库存”这一个动作,而是从入口到下游全链路都要控流。入口要做静态化、CDN、验证码、预约、前置限流,尽量把无效请求挡在最外层。
- 库存处理要兼顾性能和一致性。常见做法是 Redis 预扣减加 MQ 异步下单,但要明确失败补偿、重复消费、订单超时回补、消息堆积时的处理策略。
- 数据库层不能承受所有瞬时流量,因此需要异步化和削峰。真正的大厂实现里,库存、资格、幂等、风控、订单通常不是一个点状方案,而是多层防线。
- 系统设计时要考虑热点隔离、服务降级、活动开关、黑白名单、防刷策略,以及如何快速熔断某些失败链路,避免把核心交易系统拖垮。
- 如果你能把“高并发”回答成“容量规划 + 一致性设计 + 稳定性治理”的组合,而不是 Redis 八股,就更像有真实经验的人。
可能追问
- Redis 预扣成功但下单失败,库存怎么补?
- 如何防止用户重复下单?
- 如果 MQ 积压严重,你优先保障什么功能?
第 10 题:如果让你做一次老系统性能优化,你会怎么分阶段推进?
题目
- 如果让你接手一个老系统做性能优化,你会怎么分阶段推进?如何避免“优化了很多点,但整体收益很小”的情况?
关键点
- 先建立基线和瓶颈分布,再决定优化优先级。
- 区分单点优化和系统性优化。
- 从慢 SQL、缓存、线程模型、序列化、网络调用、对象创建、GC 等层面找收益点。
- 性能优化需要压测验证和回归保障。
- 优化目标应绑定业务指标,而不是只看单机 TPS。
标准答案
- 我会先拿到现状基线,包括核心接口 RT、TP99、QPS、错误率、机器资源占用、GC 情况和依赖耗时分布。没有基线的优化基本都不专业。
- 然后做瓶颈拆解,区分是数据库瓶颈、缓存命中率问题、线程模型问题、代码热点、序列化开销,还是远程调用链路过长。优先做收益大、风险可控、回滚简单的项。
- 优化不能只盯代码。很多时候真正的收益来自索引改造、缓存分层、批量化、异步化、连接池参数、对象复用和网络协议优化。
- 每一轮优化都要通过压测或灰度验证收益,并观察副作用,例如内存上涨、缓存不一致、下游压力转移、长尾延迟恶化。只看平均 RT 是不够的。
- 最终目标是把优化结果映射回业务指标,比如峰值承载提升、资源成本下降、超时率下降,而不是停留在“某段代码快了 20%”这种局部结论。
可能追问
- 你做过最有价值的一次性能优化是什么?
- 为什么很多系统平均 RT 下降了,但用户体验没有明显改善?
- 性能优化和稳定性治理之间,你会怎么排优先级?
总结
如果你准备的是大厂 4 年经验 Java 岗位,这类题目已经不会满足于“概念背诵”,而是会继续追问你是否经历过真实流量、线上故障、容量瓶颈和架构取舍。
建议你复习时不要只背答案,而是把每道题都补充成“原理 + 项目场景 + 故障案例 + 优化动作 + 最终结果”五段式。能讲到这个层次,面试官会明显感受到你不是只会看面经,而是真的做过事。