面试官与水货程序员的Battle:西安电子科技大学cjc同学的大厂面试奇遇记
面试官与水货程序员的Battle:西安电子科技大学cjc同学的大厂面试奇遇记
楔子
2024年的一个明媚下午,位于北京中关村某互联网大厂的会议室里,气氛略显紧张。今天要面试的是一位来自西安电子科技大学的应届生——cjc同学。面试官是一位经验丰富、技术功底深厚的老炮儿,面容严肃,眼神犀利。
而我们的主角cjc同学,穿着一身略显宽松的格子衬衫,背着一台充满贴纸的MacBook,信心满满地推开了会议室的门。
"请坐,我是今天的面试官,先简单介绍一下自己吧。"面试官推了推眼镜淡淡说道。
"面试官好,我叫cjc,来自西安电子科技大学,计算机科学与技术专业,喜欢写代码,尤其喜欢Java..."cjc同学顿了顿,"以及切图。"
面试官眉头微皱:"好了,我们开始吧。"
第一轮:Java基础与集合
面试官提问
"那我们先从基础开始。HashMap的底层数据结构是什么?"
cjc同学眼睛一亮:"这个我会!HashMap底层是数组+链表+红黑树,当链表长度超过8且数组长度大于64时,会转换成红黑树。"
"不错,那ArrayList和LinkedList的区别是什么呢?"
"ArrayList底层是数组,查询快,增删慢;LinkedList底层是双向链表,增删快,查询慢。"cjc对答如流。
"很好,HashMap是线程安全的吗?"
"不是,线程不安全。ConcurrentHashMap是线程安全的。"cjc回答得很溜。
面试官点了点头:"基础还可以,那我来问一下,HashMap在JDK1.7和JDK1.8有什么区别?"
cjc同学陷入了沉思:"这个...JDK1.7是用数组+链表,JDK1.8是...那个...加入了红黑树...然后...那个头插法改成了尾插法?"
"不错,还知道头插尾插,那你知道为什么改成尾插吗?"
cjc抓了抓头:"为了...解决并发问题?"
"主要是为了避免环形链表导致的死循环问题。好了,下一题,HashMap的扩容机制是怎样的?"
cjc开始结巴了:"就是...当元素数量超过负载因子乘以容量的时候...会扩容...扩容是2倍...然后重新计算索引...那个..."
"threshold了解吗?"
"threshold就是...临界值...大于这个值就扩容...等于负载因子乘以容量..."cjc勉强回答。
面试官没有继续为难:"行,基础部分过得去,我们继续。"
第二轮:JUC与多线程
面试官提问
"好,我们来聊聊多线程。创建线程的方式有哪几种?"
"继承Thread类,实现Runnable接口,实现Callable接口,还有使用线程池!"cjc回答得很积极。
"可以,那Thread和Runnable的区别是什么?"
"Thread是类,Runnable是接口。Thread继承了Runnable...不对,Thread实现了Runnable...实现Runnable更好,因为Java单继承...那个..."cjc有点混乱。
"Thread实现了Runnable接口,所以Thread本质上也是Runnable。那我问你,为什么有了Runnable还要有Callable?"
cjc想了想:"Callable可以有返回值,Runnable没有。Callable可以抛异常,Runnable不行。"
"对,那线程池的核心参数有哪些?"
" corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue、threadFactory、handler!"cjc扳着手指头数。
"很好,线程池的执行流程是什么?"
cjc开始比划:"首先...任务来了...先看核心线程数有没有满...没满就创建核心线程...满了就加入队列...队列满了就创建临时线程...临时线程也满了就拒绝任务..."
"回答得不错。那我问你一个实际场景,如果你的系统需要执行大量短耗时任务,你会怎么选择线程池?"
cjc愣住了:"这个...用newFixedThreadPool?"
"短耗时任务建议使用CachedThreadPool或者自定义线程池,核心线程数可以设大一点。好了,volatile关键字了解吗?"
"volatile是轻量级同步机制,可以保证可见性和有序性,不能保证原子性。"cjc回答得很标准。
"那synchronized和Lock的区别是什么呢?"
cjc开始胡扯:"synchronized是Java内置的,Lock是API层面的...synchronized会自动释放锁,Lock需要手动释放...synchronized是阻塞式的...Lock可以非阻塞...那个..."
"好了,synchronized是隐式锁,Lock是显式锁,这是最主要的区别。"
cjc如释重负:"对对对,就是这样。"
第三轮:框架与分布式
面试官提问
"好,我们聊聊Spring和SpringBoot。SpringBoot的自动配置原理是什么?"
cjc绞尽脑汁:"就是...@SpringBootApplication注解...里面包含@EnableAutoConfiguration...然后...那个...SpringFactoriesLoader...加载META-INF/spring.factories..."
"还有呢?"
"还有...@Conditional...条件装配...根据环境判断..."cjc勉强挤出几句。
"基本正确,那Spring的事务传播行为有哪些?"
"REQUIRED、REQUIRES_NEW、SUPPORTS、NOT_SUPPORTED...还有MANDATORY、NEVER、NESTED..."cjc背得很熟。
"OK,那MyBatis的#{}和${}有什么区别?"
"#{}是预编译SQL,防止SQL注入,${}是直接拼接SQL,有SQL注入风险。"cjc回答得很快。
"不错,那Redis的数据类型有哪些?"
"String、Hash、List、Set、ZSet,还有HyperLogLog、Bitmap、Geospatial..."cjc扳着指头数。
"了解很全面,那Redis的持久化机制呢?"
"RDB和AOF,RDB是快照,AOF是追加日志..."cjc回答得很溜。
"好,那我来问一下实际的,如果Redis某个key过期了,但是内存还没释放,是什么原因?"
cjc开始瞎猜:"那个...可能是内存碎片?或者...那个...淘汰策略没触发?"
"这种情况通常是因为Redis的淘汰策略设置为no-eviction,或者内存分配器有问题。好了,MySQL的索引结构是什么?"
"B+树!"cjc毫不犹豫。
"那为什么用B+树而不是B树?"
cjc又卡住了:"因为...B+树更适合范围查询...所有数据都在叶子节点...非叶子节点只存索引...树更矮...查询更快..."
"回答得不错,那MySQL的事务隔离级别有哪些?"
"READ UNCOMMITTED、READ COMMITTED、REPEATABLE READ、SERIALIZABLE。默认是REPEATABLE READ。"
"好,那幻读是什么?是怎么解决的?"
cjc开始胡说:"幻读就是...就是读取的时候出现了幻觉...是MVCC和Next-Key Lock解决的...不对...是间隙锁...就是那个..."
"行了,大概知道就行。最后一个问题,你了解DDD领域驱动设计吗?"
cjc彻底懵了:"DDD...就是...领域驱动设计...那个...有实体、值对象、聚合根、领域服务...分层架构...基础设施层...应用层...领域层..."
面试官抬手看了看表:"好了,今天的面试就到这儿。你先回去等通知吧。"
cjc同学收拾东西:"面试官,那个...我表现怎么样?"
"还可以,基础挺扎实的,后面部分还需要加强。回去等通知吧。"
"好的,谢谢面试官!"cjc同学起身离开,心里默默祈祷能收到offer。
技术答案详解
第一轮答案
1. HashMap的底层数据结构
JDK1.7:数组 + 链表(头插法)
JDK1.8:数组 + 链表 + 红黑树(尾插法)
- 当链表长度超过8且数组长度大于64时,链表转换为红黑树
- 红黑树节点数小于等于6时,退化为链表
- 使用尾插法是为了避免并发情况下形成环形链表导致死循环
2. ArrayList vs LinkedList
| 特性 | ArrayList | LinkedList | |------|-----------|------------| | 底层结构 | 动态数组 | 双向链表 | | 随机访问 | O(1) | O(n) | | 头部插入/删除 | O(n) | O(1) | | 尾部插入/删除 | 均摊O(1) | O(1) | | 内存占用 | 小,仅存数据 | 大,需存前后指针 |
3. HashMap线程不安全的原因
- JDK1.7:并发扩容时使用头插法,可能形成环形链表,导致死循环
- JDK1.7:put操作时可能导致数据覆盖
- JDK1.8:并发put可能导致数据覆盖,但解决了环形链表问题
- 建议使用ConcurrentHashMap
4. HashMap扩容机制
- 负载因子默认为0.75
- 当size >= threshold(capacity * loadFactor)时触发扩容
- 扩容为原来容量的2倍
- 重新计算每个元素的索引位置(hash and (newCap - 1))
- threshold也会翻倍
第二轮答案
1. 创建线程的方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口(配合FutureTask或线程池)
- 使用线程池(Executors或ThreadPoolExecutor)
2. Thread vs Runnable
- Thread是类,Runnable是接口
- Thread实现了Runnable接口
- Runnable更适合资源共享,避免单继承限制
- 两种方式都需要通过start()启动线程
3. Runnable vs Callable
| 特性 | Runnable | Callable | |------|-----------|----------| | 返回值 | void | V | | 异常 | 不可抛异常 | 可抛异常 | | 执行方法 | run() | call() |
4. 线程池核心参数
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数
- keepAliveTime:空闲线程存活时间
- unit:时间单位
- workQueue:任务队列
- threadFactory:线程工厂
- handler:拒绝策略
5. 线程池执行流程
- 提交任务,检查核心线程数是否已满
- 未满,创建核心线程执行任务
- 已满,将任务加入队列
- 队列满,检查最大线程数是否已满
- 未满,创建临时线程执行任务
- 已满,执行拒绝策略
6. volatile关键字
- 可见性:一个线程修改volatile变量,其他线程立即可见
- 有序性:禁止指令重排序
- 不保证原子性:如i++操作
- 底层实现:Lock前缀指令 + MESI缓存一致性协议
7. synchronized vs Lock
| 特性 | synchronized | Lock | |------|--------------|------| | 级别 | JVM内置 | API | | 释放 | 自动释放 | 手动释放 | | 阻塞 | 阻塞式 | 可tryLock非阻塞 | | 公平 | 非公平 | 可设置公平/非公平 | | 条件变量 | 无 | 可多个wait/notify |
第三轮答案
1. SpringBoot自动配置原理
- SpringBootApplication = SpringBootConfiguration + EnableAutoConfiguration + ComponentScan
- EnableAutoConfiguration = AutoConfigurationPackage + Import(AutoConfigurationImportSelector.class)
- SpringBoot启动时通过SpringFactoriesLoader加载META-INF/spring.factories中的自动配置类
- 通过Conditional系列注解进行条件装配
- 按需加载,自动配置类不会全部加载
2. Spring事务传播行为
- REQUIRED:如果当前有事务,加入;否则创建新事务(默认)
- REQUIRES_NEW:总是创建新事务,挂起当前事务
- SUPPORTS:如果当前有事务,加入;否则非事务执行
- NOT_SUPPORTED:非事务执行,挂起当前事务
- MANDATORY:必须在事务中执行,否则抛异常
- NEVER:必须不在事务中执行,否则抛异常
- NESTED:嵌套事务,使用Savepoint
3. MyBatis #{} vs ${}
- #{}:预编译SQL参数占位,防止SQL注入
- ${}:直接字符串拼接,存在SQL注入风险
- #{}会加上单引号,${}不会
- 动态表名、排序字段等需要使用${}
4. Redis数据类型
- String:字符串、计数器、缓存
- Hash:对象存储
- List:消息队列、列表
- Set:去重、标签、好友关系
- ZSet:排行榜、延迟队列
- HyperLogLog:基数统计
- Bitmap:位图、签到
- Geospatial:地理位置
5. Redis持久化机制
RDB(快照)
- 定时生成数据快照
- 适合备份和灾难恢复
- 可能会丢失最近数据
AOF(追加文件)
- 记录所有写操作命令
- 可配置同步策略(always/everysec/no)
- 文件较大,恢复慢
建议:同时开启RDB和AOF
6. MySQL索引结构
B+树
- 所有数据都在叶子节点
- 叶子节点之间用双向链表连接
- 适合范围查询
- 树高通常为2-4层
为什么不用B树?
- B+树非叶子节点不存数据,同样数据量下树更矮
- B+树范围查询更高效,只需遍历叶子节点链表
- B+树查询更稳定,必须到叶子节点
7. MySQL事务隔离级别
| 隔离级别 | 脏读 | 不可重复读 | 幻读 | |----------|------|------------|------| | READ UNCOMMITTED | 是 | 是 | 是 | | READ COMMITTED | 否 | 是 | 是 | | REPEATABLE READ | 否 | 否 | 是 | | SERIALIZABLE | 否 | 否 | 否 |
默认级别:REPEATABLE READ
8. 幻读及解决
幻读:同一事务中,两次查询结果不一致,第二次读到了新插入的行
解决方案:
- MVCC:多版本并发控制,通过版本号控制
- Next-Key Lock:行锁 + 间隙锁,锁定范围包括记录和间隙
- SERIALIZABLE:串行化,完全避免幻读
写在最后
cjc同学的面试之旅虽然有些地方回答得磕磕巴巴,但整体表现还算中规中矩。面试官那句"回去等通知",既是常规话术,也给了cjc一丝希望。
技术面试就是这样,会的问题要答准,不会的问题也要尽量扯一扯,态度诚恳最重要。各位正在找工作的小伙伴们,基础知识一定要扎实,项目经验要能讲清楚,面试官一般不会为难你的。
最后,祝cjc同学能顺利拿到offer,也祝各位读者面试顺利!
代码改变世界,面试改变命运,且面且珍惜。
本文纯属娱乐虚构,如有雷同纯属巧合。
