面试挂了才知道,基础题不考你会不会
六道基础题,表面考的是广度,实际考的是你有没有把知识嚼碎了咽下去。
2018 年秋天,南京携程大厦。面试官翻过一页纸,说:「我们做几道基础题吧。」
那时候我刚毕业,自认为学校里 Java 基础学得还行。前面的项目聊得不错,我以为稳了。然后他一道接一道抛出来:
- 数据库索引是怎么实现的?
- 谈谈你对事务的理解?
- 谈谈你对 GC 的理解?
- 计算机系统中有哪些地方用到了 cache?
- 手写一下二分查找。
- Java 中实现 Map 的有哪些?
我一道一道答。B+ 树、ACID、标记-清除、CPU 缓存、折半查找、HashMap 和 TreeMap,每个词都说上来了。面试官没有追问,只是偶尔「嗯」一声,在纸上写点什么。
一周后,收到拒信。
我答对了,但没答好
后来的几年里我反复想过这场面试。当时觉得面试官出的题太「八股」了,网上都能搜到标准答案。但回过头看,我输在把「知道」当成了「理解」。
面试官问「数据库索引是怎么实现的」,我说 B+ 树,这没错。但我没说清楚为什么是 B+ 树而不是 B 树,没说清楚磁盘 IO 次数和树高的关系,没说清楚聚簇索引和非聚簇索引对查询性能的影响。
问 GC,我说了标记-清除、复制算法、分代收集。但没说清楚 CMS 和 G1 各自的取舍,没说清楚 Stop-The-World 对线上服务的实际影响,更没有结合真实场景说「我遇到过这样一个 GC 问题,是这样排查的」。
问 Map 的实现,我列了一串类名就完了。但面试官真正想听的,是 HashMap 的链表转红黑树阈值为什么是 8,是 ConcurrentHashMap 在 Java 7 和 8 之间锁粒度的变化,是这些设计背后的工程权衡。
每一个问题都是一扇门,我只站在门口报了个门牌号。
基础题才是照妖镜
后来我自己做了面试官,才明白这类看似普通的题目是最有效的筛选器。
算法题区分的是「刷没刷过」,基础题区分的是「用没用心」。一个候选人如果把「计算机系统中有哪些地方用到了 cache」从 CPU L1/L2/L3 讲到 Page Cache,再讲到 Redis,最后落到自己项目里的缓存策略——这人大概率能用。
反过来,只像报菜名一样罗列名词的,多半只会照着文档搬砖。面试官全程不追问,不是你答得完美,是他已经判断完了。
那六道题覆盖了存储、并发、算法、系统架构四个维度,每一道都至少有三层深度。第一层是名词解释,网上背一背就会;第二层是原理拆解,需要你真正理解设计动机;第三层是实战经验,只有在生产环境踩过坑才能讲出来。面试官要的是第二层,加分项在第三层,而我当时连第一层都没走出去多远。
如果今天的你面对同样的六道题,能答到第几层?
六道题参考答案
这里给出第二层深度(原理拆解)的参考回答,也是面试官真正期望听到的水平。
1. 数据库索引的实现方式: InnoDB 使用 B+ 树实现索引。选择 B+ 树而非 B 树,核心原因是非叶子节点不存数据,每个磁盘页能容纳更多 key,树更矮,IO 次数更少。叶子节点通过链表串联,范围查询只需顺序扫描。InnoDB 的主键索引(聚簇索引)叶子节点存储完整行数据,二级索引的叶子节点只存主键值,查询时需要回表——理解这一点,才能解释为什么「覆盖索引」能提升性能。
2. 对事务的理解: 事务的 ACID 四个特性中,原子性靠 undo log 实现(回滚),持久性靠 redo log 实现(崩溃恢复),隔离性靠锁和 MVCC 实现,一致性是前三者的结果。InnoDB 默认隔离级别是可重复读(RR),通过 MVCC 在每行数据后保存版本链,读操作不加锁,写操作加间隙锁防止幻读。面试中如果能从 log 机制讲到 MVCC 实现,才算过了第一层。
3. 对 GC 的理解: JVM 堆分新生代(Eden + S0/S1)和老年代。新生代用复制算法(Survivor 区轮换),老年代用标记-清除或标记-整理。判断对象存活用 GC Roots 可达性分析,GC Roots 包括栈帧局部变量、静态变量和 JNI 引用。CMS 收集器并发标记-清除,低延迟但产生内存碎片;G1 将堆划分为 Region,可预测停顿时间。面试官想听的不是算法名称,而是你在生产环境如何选择收集器、如何排查 Full GC 频繁的问题。
4. 系统中的 cache: 这是一个考察系统思维广度的题。从硬件到软件,cache 无处不在:CPU L1/L2/L3 缓存解决 CPU 与内存的速度差距,Page Cache 缓存磁盘数据减少 IO,数据库 Buffer Pool 缓存数据页,Redis 作为应用层远程缓存,CDN 缓存静态资源,浏览器缓存减少网络请求。本质上都是同一个思路——用更快的存储介质加速对更慢存储的访问,用空间换时间。
5. 二分查找: 在有序数组中查找目标值,每次比较中间元素将范围缩小一半,时间复杂度 O(log n)。手写时注意两个细节:循环条件是 left <= right(不是 <),计算中点用 left + (right - left) / 2 而非 (left + right) / 2(防止整数溢出)。变体包括查找第一个等于目标的元素、查找最后一个小于等于目标的元素等,这些变体在实际工程中比标准二分更常用。
6. Java 中 Map 的实现: HashMap(数组 + 链表 + 红黑树,链表长度超 8 且数组长度超 64 时转红黑树,阈值选 8 是因为泊松分布下链表长度达到 8 的概率极低)、LinkedHashMap(在 HashMap 基础上维护双向链表,支持插入顺序或访问顺序遍历,可用来实现 LRU 缓存)、TreeMap(红黑树实现,key 有序,支持范围查询)、ConcurrentHashMap(Java 7 用分段锁,Java 8 改为 CAS + synchronized 细粒度锁,锁粒度从 Segment 降到单个桶)、Hashtable(全表锁,已过时,不要用)。