大厂 Java 面试实录:严肃面试官 VS 水货程序员谢飞机,3轮连问后“回家等通知”
正文
“下一位,谢飞机。”
会议室门缓缓打开,一位穿着格子衫、背着双肩包、头发略显倔强的程序员探进脑袋。
“您好面试官,我叫谢飞机,五年……额,三年半……呃,反正很多年开发经验,主攻 Java 全栈后端架构调优分布式高并发微服务生态闭环。”
面试官抬了抬眼镜:“坐。简历上写你熟悉 Java 核心、JUC、JVM、SpringCloud、Dubbo、Redis、MySQL、Docker、DDD。那我们开始。”
谢飞机正襟危坐:“您问,我这人最大的优点就是,回答问题的时候特别自信。”
面试官:“希望你的技术和自信成正比。”
谢飞机:“那可能要让您失望一半了。”
第一轮:从基础集合到并发,看看你是不是‘真会写 Java’
问题 1:说一下HashMap的底层数据结构,以及为什么线程不安全?
面试官:“先来个基础题。HashMap底层是什么?”
谢飞机:“这个我会。HashMap底层是数组加链表加红黑树。数据先通过 hash 找桶位,发生冲突就挂链表,如果链表太长就转红黑树,提升查询效率。”
面试官点头:“嗯,继续,为什么线程不安全?”
谢飞机:“因为……多个线程一起 put 的时候,大家都很着急,一个不小心就把数据覆盖了。以前好像还能形成死循环,CPU 原地升天。”
面试官:“回答还行,至少不是背错版本。”
谢飞机嘴角上扬:“谢谢老师,我在集合这块一直比较集合。”
问题 2:ArrayList和LinkedList的区别,业务中你怎么选?
面试官:“那ArrayList和LinkedList呢?”
谢飞机:“ArrayList底层是动态数组,查询快,随机访问效率高,尾部插入也还行;LinkedList是双向链表,理论上插入删除快。”
面试官:“为什么说理论上?”
谢飞机:“因为如果你先得遍历半天才能找到位置,那也没多快。实际业务里大部分还是用ArrayList,缓存友好,性能通常更稳。”
面试官难得露出一点赞许:“不错,这个回答比很多只会背八股的人强。”
谢飞机小声嘀咕:“我主要是背过很多次,背出感情了。”
问题 3:synchronized和ReentrantLock有什么区别?
面试官:“进入并发。synchronized和ReentrantLock区别是什么?”
谢飞机:“呃……都是锁。synchronized是 JVM 帮你管,ReentrantLock是 Java 代码自己管。然后ReentrantLock更灵活,可以 tryLock,可以可中断,可以公平锁,还能绑定多个 Condition。”
面试官:“那你平时怎么选?”
谢飞机:“简单同步我就synchronized,代码短,不容易忘记释放锁;复杂并发场景,比如超时获取锁、可中断等待、多条件队列,我就ReentrantLock。”
面试官:“可以。”
谢飞机挺直腰板:“您这句可以,对我很重要。”
问题 4:线程池核心参数有哪些?线上为什么不建议用Executors创建线程池?
面试官:“说说线程池。”
谢飞机:“线程池核心参数有corePoolSize、maximumPoolSize、keepAliveTime、阻塞队列、线程工厂、拒绝策略。”
面试官:“那为什么不建议直接用Executors?”
谢飞机:“因为……它太贴心了,贴心得容易出事。比如有的队列是无界的,任务一多容易把内存撑爆;有的线程数能开到特别大,会把机器打挂。”
面试官:“还算准确。那如果是订单系统异步发通知,你怎么配置线程池?”
谢飞机沉思两秒:“我一般看心情……啊不是,看业务峰值、任务类型、响应时延要求、机器核数来定。CPU 密集型线程数接近 CPU 核数,IO 密集型可以适当高一些。”
面试官:“前半句删掉,后半句还行。”
第二轮:结合业务,从 Spring 到缓存和数据库
面试官翻了翻简历:“你做过电商系统,那我们就按下单链路问。”
谢飞机:“好,我对下单很熟,尤其对没抢到优惠券这件事非常有业务理解。”
问题 1:Spring 中 Bean 的生命周期大致是什么?
面试官:“如果一个订单服务是 Spring Bean,它的生命周期你说一下。”
谢飞机:“这个……先实例化,再属性注入,然后如果实现了一些感知接口就回调,再执行初始化方法,最后就可以被用了。容器关闭时如果配置了销毁方法,还会执行销毁。”
面试官:“那 BeanPostProcessor 在哪?”
谢飞机:“在初始化前后都可以增强 Bean,比如 AOP、代理对象生成很多都和它有关系。”
面试官轻轻点头:“基础还算扎实。”
谢飞机:“谢谢,我在 Bean 生命周期这块,活得也挺完整。”
问题 2:Spring 事务为什么会失效?举个下单场景例子。
面试官:“订单创建、扣库存、写支付流水,Spring 事务在哪些情况下会失效?”
谢飞机:“常见有这些:
- 方法不是
public; - 自调用,自己类里直接调自己的事务方法;
- 异常被吃掉了没有抛出;
- 数据库引擎不支持事务;
- 没有被 Spring 管理;
- 配置的回滚异常类型不对,比如默认对运行时异常回滚。”
面试官:“如果订单方法里 try-catch 把异常吞了会怎样?”
谢飞机:“事务管理器以为你成功了,就给你提交了。结果订单写进去了,库存没扣成,数据就不一致。”
面试官:“不错,这题答得挺顺。”
谢飞机开始膨胀:“我在失败经验这块,积累非常丰富。”
问题 3:MyBatis 的#{}和${}有什么区别?为什么一般不用${}?
面试官:“继续。MyBatis 里参数占位符区别呢?”
谢飞机:“#{}是预编译占位,会转成?,由 PreparedStatement 设置参数,能防 SQL 注入;${}是字符串直接拼接,风险比较大。”
面试官:“那${}完全不能用吗?”
谢飞机:“也不是,比如动态表名、排序字段这类场景可能会用,但必须严格校验,不能让用户随便传。”
面试官:“这个回答没问题。”
谢飞机:“我终于在数据库领域,像个人了。”
问题 4:Redis 缓存穿透、击穿、雪崩分别是什么?电商商品详情怎么设计?
面试官:“商品详情页访问量很高,用 Redis 怎么扛?”
谢飞机:“缓存穿透就是查一个根本不存在的数据,请求每次都打到数据库;击穿是某个热点 key 失效瞬间,大量请求直冲 DB;雪崩是大量 key 同时过期或者 Redis 整体挂了。”
面试官:“那怎么解决?”
谢飞机:“穿透可以缓存空值、布隆过滤器;击穿可以热点 key 永不过期或者互斥锁重建缓存;雪崩可以给过期时间加随机值、做多级缓存、限流降级、Redis 高可用。”
面试官:“如果商品价格经常变化呢?”
谢飞机:“那就……价格单独做 key,或者通过延时双删、旁路缓存更新、订阅变更消息来保证相对一致性。”
面试官:“可以,说明做过一些实际场景。”
谢飞机:“也有可能是事故做多了。”
问题 5:MySQL 索引底层为什么用 B+ 树?
面试官:“最后一个,MySQL 索引为什么常用 B+ 树,不用红黑树或者普通 B 树?”
谢飞机:“因为数据库数据在磁盘上,B+ 树更适合磁盘 IO。它层级低,范围查询也方便,叶子节点还能形成有序链表。”
面试官:“普通 B 树为什么差一些?”
谢飞机:“B 树每个节点都存数据,范围扫描没 B+ 树顺;红黑树树高也容易更高,磁盘访问次数更多。”
面试官:“还可以。”
谢飞机擦了擦额头:“老师,我觉得第二轮我发挥出了带专985的水平。”
面试官:“不要给学历系统制造歧义。”
第三轮:分布式与架构题,开始拉开差距
面试官合上简历:“下面问点复杂的,看看你到底是架构师,还是‘会改配置的 CRUD 工程师’。”
谢飞机咽了口水:“老师您尽量温柔一点,我的分布式知识比较怕生。”
问题 1:Dubbo 一次服务调用大致经过哪些过程?如果超时,你怎么排查?
面试官:“说说 Dubbo 调用链路。”
谢飞机:“消费者启动时会从注册中心拉取提供者地址,做负载均衡,选一个服务节点,然后通过代理发起远程调用,经过序列化、网络传输,到服务端执行,再把结果返回。”
面试官:“如果一个查询订单接口频繁超时呢?”
谢飞机:“先看是不是网络抖动、服务端处理慢、数据库慢 SQL、线程池满了、连接池不够、序列化太重、GC 卡顿……反正哪里都可能慢。”
面试官:“排查步骤呢?”
谢飞机:“呃……先看日志,再看监控,再看机器,再看 JVM,再看数据库,再问同事。”
面试官面无表情:“最后一步倒是很多人都会。”
问题 2:RabbitMQ 如何保证消息不丢?如果重复消费怎么办?
面试官:“下单后要发消息通知库存、积分、物流。RabbitMQ 如何保证消息可靠性?”
谢飞机:“这个问题很大,我尽量组织一下语言。生产者要保证消息发到 MQ,可以用发送确认机制;MQ 本身要做持久化,比如交换机、队列、消息持久化;消费者处理时要手动 ack,处理成功再确认。”
面试官:“如果消费者消费成功了,但 ack 前进程挂了呢?”
谢飞机:“那消息会重新投递,所以业务要保证幂等。比如根据订单号、消息 ID 做去重,或者借助唯一索引、状态机控制避免重复处理。”
面试官:“那消息积压怎么办?”
谢飞机:“扩容消费者,提高并发;检查是不是有慢消息、异常重试风暴;必要时做消息分级处理。”
面试官:“这题答得还算像样。”
谢飞机:“谢谢老师,我在 MQ 这块属于半懂不懂里懂得比较多的。”
问题 3:讲一下 JVM 内存结构、对象创建过程,以及一次 Full GC 可能由什么引起?
面试官:“JVM 来了。说。”
谢飞机眼神开始飘忽:“JVM 内存结构有堆、栈、方法区、程序计数器、本地方法栈。对象一般先在……年轻代里分配,如果放不下可能大对象直接进老年代。对象创建要先类加载检查、分配内存、初始化零值、设置对象头、执行构造方法。”
面试官:“那 Full GC 的常见原因呢?”
谢飞机:“这个……内存不够的时候就会 Full GC。还有 System.gc(),还有那个……元空间满了?老年代满了?反正 GC 不讲武德的时候就会来。”
面试官沉默了两秒:“你这回答,前半段像背过,后半段像许愿。”
谢飞机尴尬一笑:“JVM 比较抽象,我一般和它通过日志沟通。”
问题 4:线程池参数如何根据业务压测结果调优?
面试官:“再来一道多线程和性能相关。线程池调优你怎么做?”
谢飞机:“先区分任务类型,CPU 密集还是 IO 密集。然后看压测 TPS、响应时间、队列堆积、线程活跃数、拒绝次数、CPU 使用率、上下文切换这些指标。”
面试官:“如果线程数一味增加,为什么不一定更快?”
谢飞机:“因为线程太多会带来上下文切换开销、锁竞争、CPU 抢占,还可能把数据库、Redis、下游接口一起压垮。”
面试官:“那你会怎么定?”
谢飞机:“先给一个初始值,压测观察瓶颈,再逐步调 core、max、queue 容量和拒绝策略,找到吞吐和延迟的平衡点。”
面试官:“这题回答还可以。”
谢飞机心里又亮了一盏灯。
问题 5:你理解的 DDD 是什么?如果做一个优惠券系统,如何拆分领域?
面试官:“最后一道。DDD 怎么理解?”
谢飞机:“DDD 就是……领域驱动设计。核心思想是按业务领域建模,不是按数据库表硬拆。会有实体、值对象、聚合、领域服务、应用服务、仓储这些概念。”
面试官:“那优惠券系统呢?”
谢飞机:“可以拆成券模板、券发放、券领取、券核销、规则校验这些领域……大概。然后用聚合根统一管理一致性。再画一堆图,让系统显得很高级。”
面试官:“前半句还行,最后一句很诚实。”
谢飞机:“因为我见过有些项目,业务还没理清,先把包名改成 domain、application、infrastructure 了。”
面试官终于忍不住笑了一下。
面试结束
面试官合上电脑:“今天先到这里。你的基础题回答还可以,部分业务场景也有一定理解;但在 JVM、分布式排障、DDD 落地这些复杂问题上,深度还不够,回答比较发散。”
谢飞机坐得笔直:“老师,我这个人优点就是可塑性强,缺点就是现在还没塑好。”
面试官:“嗯,你先回去等通知吧。”
谢飞机起身,深深鞠躬:“好的老师,希望这个通知不是‘感谢参与’。”
走出会议室后,谢飞机掏出手机记下两句话:
- 基础八股还能救命;
- JVM 不能再靠玄学了。
面试问题标准答案详解
下面对上面所有问题做系统梳理,帮助小白真正学明白。
1. HashMap 底层数据结构,以及为什么线程不安全
底层结构
JDK 1.8 中,HashMap底层是:
- 数组(桶)
- 链表
- 红黑树
数据插入时:
- 先根据 key 的 hash 值计算桶下标;
- 如果该桶为空,直接放入;
- 如果冲突,先挂到链表;
- 当链表长度超过阈值且数组容量达到一定条件后,链表会树化为红黑树。
为什么线程不安全
在多线程下:
- 多线程 put 可能导致数据覆盖;
- 扩容 resize 时可能产生数据丢失或结构异常;
- JDK 1.7 中头插法在并发扩容下可能形成链表死循环;
- 多线程读写没有加锁,数据可见性和原子性都无法保证。
解决方案
- 并发场景优先使用
ConcurrentHashMap - 或者外部加锁控制
2. ArrayList 和 LinkedList 的区别
ArrayList
- 底层:动态数组
- 优点:
- 支持随机访问,
get(index)快 - 尾部插入性能较好
- 内存连续,缓存命中率高
- 支持随机访问,
- 缺点:
- 中间插入、删除需要移动元素
- 扩容会有数组拷贝成本
LinkedList
- 底层:双向链表
- 优点:
- 已知节点位置时,插入删除较方便
- 缺点:
- 随机访问慢,要遍历
- 节点额外保存前驱后继指针,内存开销更大
- CPU 缓存不友好
实际怎么选
绝大多数业务场景优先ArrayList。除非明确需要频繁在链表特定位置插入删除,否则一般不用LinkedList。
3. synchronized 和 ReentrantLock 的区别
synchronized
- Java 内置关键字
- 使用简单,自动加锁和释放锁
- 适合简单同步场景
- 早期性能一般,后来经过偏向锁、轻量级锁等优化后性能不错
ReentrantLock
- JUC 包中的显示锁
- 需要手动
lock()和unlock() - 功能更丰富:
- 支持可中断锁
lockInterruptibly() - 支持超时获取锁
tryLock() - 支持公平锁/非公平锁
- 支持多个条件队列
Condition
- 支持可中断锁
如何选择
- 简单同步:优先
synchronized - 高级并发控制:用
ReentrantLock
4. 线程池核心参数,以及为什么不建议用 Executors
ThreadPoolExecutor 核心参数
corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:非核心线程空闲存活时间workQueue:任务阻塞队列threadFactory:线程工厂RejectedExecutionHandler:拒绝策略
任务执行流程
- 当前线程数 < core,直接创建核心线程执行
- 否则任务进入队列
- 队列满了且线程数 < max,再创建非核心线程
- 如果线程数达到 max 且队列也满,则触发拒绝策略
为什么不建议用 Executors
Executors某些工厂方法隐藏了风险:
newFixedThreadPool():使用无界队列,任务过多可能 OOMnewCachedThreadPool():最大线程数接近无限,可能创建过多线程newSingleThreadExecutor():也是无界队列风险
正确做法
直接使用ThreadPoolExecutor,明确指定参数。
5. Spring Bean 生命周期
大致流程:
- 实例化 Bean
- 属性注入
- 执行 Aware 接口回调
BeanPostProcessor前置处理- 执行初始化方法:
InitializingBean.afterPropertiesSet()- 自定义 init-method
BeanPostProcessor后置处理- Bean 可被使用
- 容器关闭时执行销毁方法:
DisposableBean.destroy()- 自定义 destroy-method
作用
理解生命周期有助于理解:
- AOP 代理生成
- Bean 初始化扩展
- 自定义框架能力实现
6. Spring 事务为什么会失效
常见原因:
- 自调用失效:同类内部方法直接调用,绕过代理
- 方法不是 public:默认代理可能不生效
- 异常被吞掉:事务感知不到异常,导致提交
- 抛出的不是配置回滚的异常类型
- 对象不是 Spring 容器管理的 Bean
- 数据库引擎不支持事务,如 MyISAM
- 传播行为使用不当
实战建议
- 事务方法放到独立 Bean 中
- 不要吞异常
- 明确回滚规则
rollbackFor = Exception.class
7. MyBatis 的 #{} 和 ${}
#{}
- 预编译参数占位符
- 生成 SQL 时用
? - 自动处理参数类型
- 可以防止 SQL 注入
${}
- 字符串直接拼接
- 不会预编译
- 存在 SQL 注入风险
什么时候会用 ${}
- 动态表名
- 动态排序字段
但必须做白名单校验,绝不能直接接收用户输入拼接。
8. Redis 缓存穿透、击穿、雪崩
缓存穿透
查询不存在的数据,缓存和数据库都没有,每次都打到数据库。
解决:
- 缓存空对象
- 布隆过滤器
- 接口参数校验
缓存击穿
热点 key 在失效瞬间,大量请求同时访问数据库。
解决:
- 热点数据永不过期
- 互斥锁/分布式锁重建缓存
- 后台异步刷新
缓存雪崩
大量 key 同时过期,或 Redis 整体不可用,导致请求大量打到 DB。
解决:
- 过期时间加随机值
- Redis 主从、哨兵、集群
- 多级缓存
- 限流、降级、熔断
9. MySQL 索引为什么用 B+ 树
原因一:降低磁盘 IO 次数
B+ 树是多叉平衡树,一个节点能放很多 key,树高低,查找时磁盘 IO 更少。
原因二:范围查询更高效
B+ 树的数据都在叶子节点,并且叶子节点之间有链表连接,非常适合范围扫描。
原因三:更稳定
B+ 树非叶子节点只存索引,不存数据,所以每个节点能容纳更多索引项。
为什么不是红黑树
红黑树是二叉树,树高更高,磁盘 IO 更频繁,不适合数据库索引。
为什么不是普通 B 树
B 树的数据分散在各层,不如 B+ 树适合范围查找和顺序扫描。
10. Dubbo 一次调用过程,以及超时排查思路
调用过程
- 服务提供者启动并注册到注册中心
- 消费者启动,从注册中心订阅服务地址
- 消费者通过负载均衡选择一个提供者
- 通过动态代理发起远程调用
- 请求进行序列化并通过网络发送
- 服务端反序列化、执行业务逻辑
- 结果返回给客户端
超时排查思路
可以按链路逐层排查:
- 客户端:超时时间配置是否合理
- 网络层:网络抖动、丢包、连接数是否异常
- 服务端线程池:是否满载、队列堆积
- 业务逻辑:是否存在耗时计算
- 数据库:慢 SQL、锁等待、连接池不足
- 缓存/下游依赖:Redis、MQ、第三方接口是否慢
- JVM:是否频繁 GC,Stop-The-World 时间过长
- 机器资源:CPU、内存、磁盘、负载是否异常
常用手段
- 查看调用链监控
- 看服务日志
- 看线程 dump
- 看 GC 日志
- 分析慢 SQL
- 看 Prometheus/Grafana 等监控
11. RabbitMQ 如何保证消息不丢,以及如何处理重复消费
如何保证消息不丢
从三个阶段看:
1)生产者到 MQ
- 开启
publisher confirm - 必要时结合
return机制处理路由失败 - 发送失败要重试或记录补偿
2)MQ 自身存储
- 队列持久化
- 交换机持久化
- 消息持久化
- 集群/镜像队列提高可用性
3)MQ 到消费者
- 消费者手动 ack
- 处理成功后再确认
- 失败可重试、转死信队列
重复消费怎么处理
因为网络抖动、消费者重启、ack 失败等都可能导致重复投递,所以业务必须做幂等。
幂等方案
- 唯一消息 ID + 去重表
- Redis setnx 去重
- 数据库唯一索引
- 根据业务状态判断是否已处理
12. JVM 内存结构、对象创建过程、Full GC 原因
JVM 运行时数据区
- 程序计数器:记录当前线程执行字节码的位置
- 虚拟机栈:方法调用时的栈帧
- 本地方法栈:本地方法服务
- 堆:存放对象实例,是垃圾回收的主要区域
- 方法区/元空间:存放类元数据、常量、静态变量等
对象创建过程
- 类加载检查:类是否已加载
- 分配内存:在堆上给对象分配空间
- 初始化零值:成员变量先赋默认值
- 设置对象头:如 hash、GC 分代信息等
- 执行
<init>构造方法
Full GC 常见触发原因
- 老年代空间不足
- 晋升失败
- 大对象直接进入老年代导致空间紧张
System.gc()显式触发- 元空间不足
- CMS/G1 等垃圾收集过程中的特定回收条件触发
如何排查
- 看 GC 日志
- 看堆内存分配情况
- 导出 heap dump 分析对象占用
- 排查内存泄漏、对象生命周期过长问题
13. 线程池如何调优
调优步骤
- 明确业务类型:CPU 密集 / IO 密集
- 估算并发量和任务耗时
- 通过压测观察指标:
- TPS
- RT
- 活跃线程数
- 队列长度
- 拒绝次数
- CPU 使用率
- 上下文切换
- 调整参数:
corePoolSizemaximumPoolSizequeueCapacitykeepAliveTime- 拒绝策略
- 结合下游承载能力一起看,不能只盯线程池本身
为什么线程越多不一定越快
- 上下文切换开销变大
- 锁竞争加剧
- CPU 被打满
- 下游数据库/缓存被压垮
- 内存消耗增大
原则
找到系统吞吐、延迟、资源消耗三者的平衡点。
14. DDD 是什么,优惠券系统如何拆分领域
DDD 核心思想
DDD(领域驱动设计)强调:
- 软件设计要围绕核心业务领域展开
- 技术模型要贴近业务语言
- 通过建模提升复杂业务的可维护性
常见概念
- 实体 Entity:有唯一标识,如优惠券实例
- 值对象 Value Object:无唯一标识,如有效期规则
- 聚合 Aggregate:一组具有一致性边界的对象集合
- 聚合根 Aggregate Root:聚合对外唯一入口
- 领域服务 Domain Service:不适合放到实体中的领域逻辑
- 应用服务 Application Service:负责编排流程
- 仓储 Repository:负责持久化访问
优惠券系统拆分示例
可以拆成以下子域:
- 券模板领域:定义券规则、门槛、折扣方式、有效期
- 发券领域:向用户发放优惠券
- 领券领域:用户主动领取
- 核销领域:下单时校验并使用优惠券
- 规则引擎/校验领域:校验是否满足使用条件
为什么这样拆
因为优惠券系统的核心不是“几张表”,而是:
- 券怎么定义
- 谁能领
- 什么时候能用
- 使用时怎样保证一致性
- 如何防止超发、重复领取、重复核销
DDD 适合复杂业务,不是简单把包名改成domain就算落地了。
15. 补充:Linux、Docker、设计模式面试常见延伸点
虽然故事里没逐个展开,但真实面试中这些也常问。
Linux 常见问题
top:看系统负载ps -ef | grep:查进程netstat -tunlp/ss -lntp:查端口tail -f:实时看日志grep、awk、sed:日志分析df -h:看磁盘空间free -m:看内存
Docker 常见问题
- 镜像是静态模板,容器是运行实例
- 常用命令:
docker psdocker imagesdocker logsdocker exec -it
- 核心价值:环境一致、快速部署、资源隔离
设计模式常见问题
- 单例模式:全局唯一实例
- 工厂模式:封装对象创建
- 策略模式:多种算法/规则可替换
- 模板方法模式:定义流程骨架,子类实现差异
- 代理模式:增强目标对象能力,Spring AOP 常见
- 责任链模式:请求沿链路逐步处理,如审批流、过滤器
总结
这场面试里,谢飞机给我们上了很生动的一课:
- 基础题一定要扎实,集合、并发、Spring、缓存、数据库索引都是高频题;
- 复杂题不能只会背结论,要能结合业务讲原理、讲排查、讲取舍;
- 分布式系统重点在可靠性、性能、幂等、一致性、可观测性;
- JVM 和线程池调优是区分初中高级工程师的重要分水岭;
- DDD不是换目录结构,而是面向复杂业务建模。
如果你也正在准备 Java 面试,不妨把这篇文章里的问题逐个吃透。别让自己在面试现场,像谢飞机一样:基础题满面春风,复杂题开始随风飘零。