开头:3 年开发,面阿里被问懵的 3 个瞬间
"你说 HashMap 线程不安全,那 ConcurrentHashMap 在 JDK1.8 是怎么保证安全的?""Spring 事务传播机制里,REQUIRES_NEW 和 NESTED 有什么区别?举个实际业务场景。""如果让你设计一个支撑 10 万 TPS 的秒杀系统,从缓存到数据库怎么抗住流量?"
去年这三个问题,把我问得面红耳赤。那时我已经有 3 年 Java 开发经验,简历上写满了 "精通 SpringBoot"" 熟练使用 Redis",却连阿里 P6 的二面都没通过。
后来我花了 3 个月复盘 7 次失败经历,发现大厂面试的「潜规则」:他们要的不是 "用过",而是 "吃透"—— 比如用 Redis 不仅要会 set/get,还要懂持久化机制;说懂线程池,就得能根据业务场景设计参数。
今天这篇文章,把我从 "面试陪跑" 到拿到字节 offer 的逆袭经验,拆成 3 个核心模块,附上面试官亲口说的 "淘汰红线" 和 "加分暗号",帮你避开 90% 的坑!
一、这些「基础题」,90% 的人都答不到点上
1. 面试官问 "String 为什么不可变",别只说 "因为被 final 修饰"
错误回答:"String 的 char 数组被 final 修饰,所以不能改。"(这是最表层的答案,直接暴露你没看过源码)
面试官想听的答案:
- 底层 char 数组被 final 修饰只是 "不能改引用",但可以通过反射修改数组内容(举个代码例子更加分)。
- 真正设计成不可变的原因:
- 线程安全:多线程下无需同步就能直接使用(如作为 HashMap 的 key)。
- 缓存优化:字符串常量池复用相同内容,节省内存("abc" 在常量池只存一份)。
- 安全性:避免被恶意修改(如 URL、文件名等敏感信息)。
加分暗号:能说出 JDK9 后 String 用 byte 数组代替 char 数组(节省空间,因为大部分字符串是 Latin-1 编码,占 1 字节)。
2. 聊线程池时,别说 "核心线程数设为 CPU 核数"
淘汰红线:只会背 "IO 密集型设 2 倍 CPU 核数,CPU 密集型设核数 + 1",却讲不出背后逻辑(这是应届生都知道的标准答案)。
3 年开发该有的回答:
- 核心线程数不是只看 CPU,还要结合 "任务平均耗时" 和 "每秒任务数":
比如每秒有 100 个任务,每个任务平均耗时 0.1 秒,需要 100×0.1=10 个线程才能扛住(这是 Little 定律的实际应用)。 - 举个真实项目案例:
"我们订单系统峰值每秒 300 单,每个订单处理要查 3 次库(耗时约 50ms),最终把核心线程数设为 20,配合 ArrayBlockingQueue (1000),拒绝策略用 CallerRunsPolicy,成功扛住了双 11 流量。"
面试官潜台词:我不在乎你背了多少理论,只关心你能不能解决实际问题。
3. 编程题:手写单例模式,这 3 个坑千万别踩
高频笔试题:实现一个线程安全的单例模式,要求延迟加载。
90% 的人会踩的坑:
- 用了 DCL(双重检查锁)却忘了加 volatile(导致指令重排序,出现半初始化对象)。
- 没考虑反射攻击(通过 setAccessible () 强制调用私有构造器)。
- 没处理序列化 / 反序列化问题(反序列化会创建新对象)。
满分代码:
public class Singleton implements Serializable { // volatile防止指令重排序 private static volatile Singleton instance; // 私有构造器防止外部实例化 private Singleton() { // 防止反射攻击 if (instance != null) { throw new RuntimeException("禁止反射创建实例"); } } // DCL双重检查锁 public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } // 防止反序列化创建新对象 private Object readResolve() { return instance; } }加分项:能说出 "枚举单例是更优解"(天然防反射和序列化,代码更简洁)。
二、JVM 和并发:拉开差距的「分水岭」
1. 被问 "OOM 怎么排查",别只说用 jmap
淘汰回答:"用 jmap dump 堆内存,然后用 MAT 分析。"(这是工具手册内容,体现不出你的分析能力)
3 年开发该有的排查思路:
1. 先判断 OOM 类型:
- 堆 OOM:看是否内存泄漏(如静态集合未清理)或堆设置太小(-Xmx 不够)。
- 栈 OOM:可能是递归太深或线程数太多(如无限创建线程)。
- 元空间 OOM:类加载过多(如频繁动态生成类)。
2. 实战案例:"之前我们服务频繁 OOM,jstat 发现 FGC 后老年代内存没降,dump 后用 MAT 看支配树,发现一个 HashMap 占了 70% 内存,追溯引用链是定时任务没清理缓存,加了 expire 机制后解决。"
面试官想听的:你不仅会用工具,还能通过现象定位根因,并有实际解决经验。
2. 聊 ConcurrentHashMap,说 "分段锁" 就输了
淘汰红线:还在说 "ConcurrentHashMap 用分段锁保证安全"(这是 JDK1.7 的实现,1.8 早就改成 CAS+synchronized 了)。
正确回答:
- JDK1.8 的 ConcurrentHashMap:
- 底层是数组 + 链表 + 红黑树(和 HashMap 一样)。
- 线程安全靠两点:
- 对数组的空节点用 CAS 插入(无锁操作,效率高)。
- 对非空节点用 synchronized 锁定头节点(锁粒度是单个桶,比分段锁更细)。
- 扩容时支持多线程协助迁移(每个线程负责一部分桶,提高效率)。
加分暗号:能说出 "红黑树节点是 TreeNode,链表是 Node,两者继承自 AbstractMap.SimpleEntry"。
三、框架和分布式:大厂后端的「必备杀器」
1. Spring 事务失效的「隐藏场景」,你肯定踩过
高频陷阱:
- 用了 @Transactional 却没加在 public 方法上(Spring AOP 只代理 public 方法)。
- 事务方法内部调用(http://this.xxx ()),绕过了 AOP 代理(解决方案:用 AopContext.currentProxy () 获取代理对象)。
- 嵌套事务里,子事务抛出异常,父事务用 try-catch 捕获(导致子事务回滚失败)。
实战经验:"我们支付系统之前遇到过事务失效,排查发现是因为用了 @Async 异步方法,事务上下文没传递过去。后来改用 TransactionSynchronizationManager 手动绑定上下文才解决。"
2. 设计高并发系统,别只说 "加 Redis 缓存"
面试官想听到的架构思路:以秒杀系统为例:
- 前端限流:按钮置灰 + 验证码,防止用户高频点击。
- 接口层:用 Sentinel 做限流(如每秒 1000 请求),超过直接返回 "拥挤"。
- 缓存层:
- 用 Redis 预存库存,setnx 加分布式锁防止超卖。
- 缓存预热 + 布隆过滤器,避免缓存穿透。
- 数据库:
- 分库分表(按用户 ID 哈希),减轻单库压力。
- 最终一致性:异步扣减库存,用消息队列保证可靠性。
加分项:能说出 "缓存和数据库一致性用最终一致性即可,强一致性会牺牲性能"。
结尾:给 3 年开发的 3 句掏心窝的话
- 别再背面试题了:把每个知识点嚼透(比如看 HashMap 源码时,画一画扩容时的节点迁移过程)。
- 项目经验要「量化」:别说 "优化了性能",要说 "把接口响应时间从 800ms 降到 80ms,支撑了日均 100 万订单"。
- 别怕被问倒:面试官更看重你的分析思路,比如被问不会的问题,可以说 "虽然我没做过,但我觉得可以从 XX 角度入手..."
按这个思路准备,我敢说 90% 的大厂 Java 岗对你来说都不难。最后祝大家都能拿到心仪的 offer,我们岸上见!
篇幅限制下面就只能给大家展示小册部分内容了。整理了一份核心面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题,需要全套面试笔记及答案可以点击下方名片获取