Java面试翻车现场:谢飞机大战严肃面试官,3轮提问笑到头掉!
Java面试翻车现场:谢飞机大战严肃面试官,3轮提问笑到头掉!
场景介绍
时间:一个阳光明媚的下午
地点:某互联网大厂面试间
人物:
- 面试官·张工:头顶微秃,眼神犀利,T恤外面套着格子衬衫,面无表情,手边放着一杯早已凉透的美式咖啡
- 候选人·谢飞机:号称"精通Java",简历上写着"熟练掌握各类中间件",实际上...你懂的
第一轮:Java核心基础の碾压
面试官翻着简历,眉头微微一皱,感觉事情并不简单
面试官:(面无表情)先做个自我介绍吧。
谢飞机:(坐直身子,清了清嗓子)面试官你好,我叫谢飞机,毕业两年,实际工作经验三年半,精通Java、Spring全家桶、Redis、MySQL、消息队列、Docker...基本上后端那一套我都熟!
面试官:(内心OS:两年经验说三年半?简历上写的是精通,现在面试最怕听到"精通")...好,那咱们直接开始技术面。
问题1:HashMap的底层实现原理是什么?
面试官:说说HashMap吧,底层怎么实现的?
谢飞机:(自信满满)HashMap啊,就是用来存键值对的!底层是数组+链表+红黑树。JDK 1.8之后,当链表长度超过8的时候,就会转成红黑树,查询效率从O(n)变成O(log n)!
面试官:(点点头)嗯,那为什么阈值是8呢?
谢飞机:(愣了一下,开始胡说)因为...7是质数啊,8比7大一点!而且您想啊,7上8下嘛,链表长了要"上天"变成树,所以是8!
面试官:(嘴角抽动)...那负载因子为什么是0.75?
谢飞机:(彻底放飞)0.75啊,这是经过大量数学推导的!空间和时间的一个平衡点。但是我觉得吧,如果内存不值钱,负载因子设成1也行,就是碰撞多点呗。就像相亲,标准放低了(负载因子大),见的"候选人"多了,冲突就多了,但没见着合适的还得继续找...
面试官:(赶紧打断)好了好了,下一个问题。
问题2:ArrayList 和 LinkedList 有什么区别?
面试官:ArrayList和LinkedList的区别,说一下。
谢飞机:这个简单!ArrayList底层是数组,LinkedList底层是双向链表。ArrayList查询快,LinkedList增删快。但是!
面试官:(警觉)但是什么?
谢飞机:但是!我上次在ArrayList中间位置插入了100万条数据,慢得我想砸电脑!然后我换成LinkedList,结果更慢了!面试官,您说这是为什么呀?
面试官:(被反客为主,沉默三秒)LinkedList的中间插入虽然是O(1)...但你需要先找到那个位置,而查找是O(n)的。
谢飞机:哦!!!原来如此!那我可以每天跟您学点新知识吗?
面试官:(面无表情)...下一个问题。
问题3:== 和 equals 有什么区别?
面试官:==和equals的区别,你来说说。
谢飞机:这题我会!==比较的是内存地址,equals比较的是内容。但是String是个特例,String重写了equals方法,所以比较的是值!
面试官:嗯,那这段代码输出什么?Integer a = 127; Integer b = 127; System.out.println(a == b);
谢飞机:false!因为...不对,等一下。Integer有缓存池,-128到127之间的整数会被缓存,所以...是true!
面试官:那Integer a = 128; Integer b = 128;呢?
谢飞机:false!因为128超出了缓存范围,会创建新对象。但是面试官,我想问个问题,如果我用new Integer(127)呢?
面试官:(微微点头)那你觉得呢?
谢飞机:new了就是新对象,==肯定是false!这个坑我踩过,之前写代码因为这个问题线上出了bug,被老大骂了三天三夜...
面试官:基础还可以,但细节要注意。
问题4:String、StringBuffer、StringBuilder 的区别?
面试官:说说三者的区别。
谢飞机:String是不可变的,每次拼接都会产生新对象。StringBuffer和StringBuilder是可变的。StringBuffer加了synchronized,线程安全但慢;StringBuilder没加锁,线程不安全但快。
面试官:嗯,那在实际开发中怎么选?
谢飞机:我觉得吧,现在谁还用StringBuffer啊!直接在方法内部用StringBuilder不就行了,方法内不存在线程安全问题。除非您是在多线程环境下共享变量拼接字符串,但这种情况本身就很少见。就像您出门吃个饭,还用得着带个保镖(synchronized)吗?
面试官:(难得地嘴角上扬了一下)有点道理。第一轮还行,咱们继续。
第二轮:多线程 & JUC の 灵魂拷问
面试官喝了一口凉透的咖啡,眼神突然凌厉起来
面试官:基础还行,那我们聊聊多线程。
谢飞机:(搓搓手)来!这个我擅长!
问题1:volatile 关键字你是怎么理解的?
面试官:说说volatile。
谢飞机:(突然正经)volatile是Java提供的最轻量级的同步机制!它有两个特性:第一,保证可见性,一个线程修改了变量,其他线程能立即看到;第二,禁止指令重排序!
面试官:(有点意外)哦?底层怎么实现的?
谢飞机:底层用的是内存屏障!在volatile写操作前插入StoreStore屏障,写操作后插入StoreLoad屏障;在读操作前插入LoadLoad屏障,读操作后插入LoadStore屏障!我看过《深入理解Java虚拟机》!
面试官:(眼神里闪过一丝赞赏)不错。那你觉得volatile能保证原子性吗?
谢飞机:不能!比如count++这种操作,volatile管不了!因为count++不是原子操作,分成了读取、修改、写入三步。就像您数钱,数到一半被人打断,回来就忘了数到哪了。
面试官:那怎么解决?
谢飞机:用AtomicInteger啊!底层是CAS,Unsafe类,自旋!或者加synchronized。
面试官:(点头)嗯,这块学得不错。
问题2:synchronized 和 ReentrantLock 的区别?
面试官:synchronized和ReentrantLock,有什么区别?
谢飞机:synchronized是关键字,JVM层面实现的;ReentrantLock是API层面的锁。synchronized自动加锁解锁,ReentrantLock要手动lock()和unlock()。
面试官:还有呢?
谢飞机:ReentrantLock可以实现公平锁,synchronized只能是非公平的。ReentrantLock可以通过Condition实现分组唤醒线程,就像把线程分到不同的"休息室",想叫醒哪个叫醒哪个!synchronized要么用notify随机唤醒一个,要么用notifyAll全部唤醒,太粗糙了!
面试官:那性能上呢?
谢飞机:现在synchronized优化后,性能跟ReentrantLock差不多了!JDK 1.6之后加了偏向锁、轻量级锁、锁升级机制。不过...面试官,我上次写了个死锁,debug了三天才发现是锁的顺序反了,您说这算不算一种"经验"?
面试官:(扶额)算...算吧。
问题3:线程池的核心参数讲一下
面试官:线程池的核心参数有哪些?
谢飞机:7大参数!corePoolSize(核心线程数)、maximumPoolSize(最大线程数)、keepAliveTime(空闲存活时间)、TimeUnit(时间单位)、workQueue(工作队列)、ThreadFactory(线程工厂)、RejectedExecutionHandler(拒绝策略)!
面试官:那如果corePoolSize=5,maximumPoolSize=10,workQueue容量是3,来了12个任务,过程是怎样的?
谢飞机:(开始手舞足蹈)首先!前5个任务创建核心线程处理!然后!接下来的3个任务进队列排队!再然后!队列满了,剩下的任务...等等我数一下,12-5-3=4个,但最大线程是10,已经用了5个核心线程,还能再创建10-5=5个临时线程,所以4个任务都能用临时线程处理!
面试官:那如果来15个任务呢?
谢飞机:15-5=10个任务排队?不对...前5个核心线程处理,接下来3个进队列(队列容量3),再接下来...5...我算算,5个核心线程处理5个,3个进队列,还能创建5个临时线程处理5个,一共处理了5+3+5=13个,还有2个...触发拒绝策略!
面试官:(点了点头)流程对了。拒绝策略有哪些?
谢飞机:4种!AbortPolicy(抛异常)、CallerRunsPolicy(调用者线程自己执行)、DiscardPolicy(直接丢弃)、DiscardOldestPolicy(丢弃队列最老的任务)。我一般用CallerRunsPolicy,让调用者线程自己跑,就像家长不管孩子了,让孩子自己管自己!
面试官:(若有所思)嗯...第二轮表现不错。
第三轮:框架 & 中间件 & 数据库 の 终极乱斗
面试官推了推眼镜,决定加大难度
问题1:Spring IOC 和 AOP 的理解
面试官:说说Spring的IOC和AOP。
谢飞机:IOC就是控制反转,原来我们new对象,现在把对象创建交给Spring容器管理。就像以前自己买菜做饭,现在点外卖让美团送!
面试官:(忍住没笑)那DI呢?
谢飞机:DI是依赖注入,是IOC的具体实现方式!可以通过构造器注入、setter注入、字段注入。但是我听说Spring官方推荐用构造器注入,因为字段注入容易导致空指针,而且不方便做单元测试。
面试官:AOP呢?
谢飞机:AOP是面向切面编程!可以在不修改源码的情况下增加功能。比如日志、事务、权限校验。底层是动态代理,有接口用JDK动态代理,没接口用CGLIB代理。
面试官:那JDK动态代理和CGLIB有什么区别?
谢飞机:JDK动态代理只能代理有接口的类,通过反射生成代理对象;CGLIB是通过继承生成子类,重写方法实现代理。不过现在SpringBoot默认用CGLIB了。对了面试官,我上次自己写了个AOP切面,结果一直不生效,debug了一天发现是没加@EnableAspectJAutoProxy注解...
面试官:(同情地看了他一眼)SpringBoot其实不需要手动加这个。
谢飞机:对!这就是我后来才知道的...
问题2:Redis缓存穿透、缓存击穿、缓存雪崩
面试官:说说Redis缓存穿透、缓存击穿、缓存雪崩的区别和解决方案。
谢飞机:(深吸一口气)缓存穿透是查一个不存在的数据!缓存和数据库都没有,每次请求都打到数据库。解决方案:缓存空值(设置较短的过期时间),或者用布隆过滤器!
面试官:布隆过滤器原理知道吗?
谢飞机:知道!布隆过滤器用多个哈希函数,把key映射到bitmap上。判断一个key不存在,那它一定不存在;判断一个key存在,它不一定存在。就像我女朋友查我手机,没找到证据不代表我没问题,但找到证据我就肯定有问题!
面试官:(尴尬地咳嗽)...那缓存击穿呢?
谢飞机:缓存击穿是某个热点key过期了,大量请求同时打过来。解决方案:用互斥锁,只让一个线程去查数据库重建缓存,其他线程等待。或者设置热点key永不过期,后台异步更新。
面试官:缓存雪崩?
谢飞机:缓存雪崩是大批量的key在同一时间过期,或者Redis挂了!解决方案:过期时间加随机值,避免集体过期;Redis用集群高可用;本地缓存+限流降级。就像疫情期间大家同时去超市抢菜,菜没了就出大事了,所以要分批去!
面试官:嗯,说得还行。
问题3:MySQL索引失效的场景有哪些?
面试官:哪些情况会导致MySQL索引失效?
谢飞机:这个我熟!第一,对索引列使用函数,比如WHERE SUBSTR(name,1,3)='abc';第二,隐式类型转换,比如字段是int类型,但传了字符串;第三,不符合最左前缀原则;第四,like以%开头;第五,使用OR条件且OR两边不全是索引列;第六,数据量很小的时候MySQL可能会走全表扫描...
面试官:嗯,那联合索引(a, b, c),查询WHERE a=1 AND c=3,用到了吗?
谢飞机:用到了a,但c没用到!因为b断了!就像您追女朋友,第一步认识了,第三步直接求婚,中间第二步约会跳过了,您说能成吗?
面试官:(一脸黑线)...这比喻还挺贴切。
问题4:RabbitMQ消息丢失怎么处理?
面试官:RabbitMQ消息丢失的场景和处理方式。
谢飞机:消息丢失分三种情况!
第一种:生产者发送时丢失。用confirm机制,生产者发送消息后,Broker返回ACK确认。没收到ACK就重发。
第二种:RabbitMQ自身丢失。开启持久化!交换机、队列、消息都要持久化。还要用镜像队列做高可用。
第三种:消费者处理时丢失。用手动ACK!消费成功才手动确认,失败了就不确认,消息会重回队列。
面试官:那消息重复消费怎么办?
谢飞机:幂等性处理!比如数据库用唯一索引,或者Redis存消息ID做去重。不过面试官,我上次把消息队列搞炸了,几百万消息堆积,我直接写了个脚本删了队列重建,被运维大哥追着打了三层楼...
面试官:(擦了擦汗)你们公司运维脾气还挺好,没直接开除你?
谢飞机:因为是我小舅子...
面试官:...
问题5:DDD领域驱动设计你怎么看?
面试官:最后一个问题,你对DDD领域驱动设计有什么理解?
谢飞机:(眼睛一亮)DDD!这个我懂!就是Domain-Driven Design!核心是聚合根、实体、值对象、领域事件、仓储模式!我们用DDD做微服务拆分,限界上下文划分!
面试官:(有点期待)那你们项目具体怎么落地的?
谢飞机:嗯...我们就是...建了几个包,叫domain、infrastructure、application、interfaces...然后...就没有然后了。说白了就是换了个包结构,还是CRUD那一套。
面试官:...所以你们是"DDD伪落地"?
谢飞机:对!就像买了健身卡就等于健身了,建了DDD的包结构就等于DDD落地了!
面试官:(彻底无语)好的,今天的面试就到这吧。
谢飞机:好的好的!那我什么时候能来上班?
面试官:(站起身,拿起公文包)你先回去等通知吧。
谢飞机:等等!我还没说完呢!我还会Docker、K8s、Elasticsearch、Netty、RPC...
面试官:(头也不回地走出门)HR,下一位候选人到了吗?
谢飞机:(对着空荡荡的面试间喃喃自语)我还没说我的薪资要求呢...月薪30k就行...不行25k也行啊...20k!20k总可以了吧!
知识点详细解析(小白必看!)
以下是对面试中所有问题的详细技术解析,认真看完,你也能吊打面试官!
一、HashMap底层原理详解
1.1 数据结构
- JDK 1.7及之前:数组 + 链表
- JDK 1.8及之后:数组 + 链表 + 红黑树
1.2 核心参数
- 数组容量(capacity):默认16,必须是2的幂
- 负载因子(loadFactor):默认0.75
- 扩容阈值(threshold):capacity * loadFactor = 16*0.75=12
1.3 为什么容量是2的幂?
计算索引时用(n - 1) & hash代替%运算,效率更高。只有容量是2的幂时,(n-1)的二进制才是全1,才能充分利用哈希值。
1.4 为什么阈值是8?
这是泊松分布的计算结果。在负载因子0.75的情况下,链表长度达到8的概率极低(约0.00000006)。如果链表长度到了8,说明哈希函数已经严重失效,此时转红黑树可以挽救性能。
1.5 put流程
- 计算key的hash值
- 如果数组为空,先扩容
- 计算索引位置
(n - 1) & hash - 如果该位置为空,直接插入
- 如果不为空,判断key是否相等,相等则覆盖
- 如果是红黑树,按红黑树插入
- 如果是链表,遍历链表,如果找到相同key则覆盖,否则尾部插入
- 插入后判断链表长度是否 >= 8,如果数组长度 < 64,先扩容;>= 64才转红黑树
- 插入后判断size是否 > threshold,是则扩容
二、ArrayList vs LinkedList
| 对比项 | ArrayList | LinkedList | |--------|-----------|------------| | 底层结构 | 动态数组 | 双向链表 | | 随机访问 | O(1) | O(n) | | 尾部插入 | O(1) 均摊 | O(1) | | 中间插入 | O(n) 需移动元素 | O(n) 需先查找位置 | | 内存占用 | 连续内存,有预留空间 | 非连续,每个节点存前后指针 | | 适用场景 | 查询多,尾部插入多 | 头部插入多,删除多 |
注意:LinkedList中间插入虽然是O(1)的链表操作,但需要O(n)的时间找到插入位置,所以总复杂度还是O(n)。
三、== 和 equals 的区别
- ==:比较两个对象的内存地址(是否同一个对象)。对于基本类型,比较的是值。
- equals:Object类的默认实现也是比较地址,但很多类重写了该方法,比如String、Integer等重写为比较内容。
Integer缓存池:-128到127之间的Integer对象会被缓存,用valueOf()返回的是缓存中的对象。但new Integer()一定会创建新对象。
Integer a = 127; // 等价于 Integer.valueOf(127),从缓存取 Integer b = 127; // 同样从缓存取,所以 a == b 为 true Integer c = 128; // 超过缓存范围,创建新对象 Integer d = 128; // 创建新对象,所以 c == d 为 false四、String、StringBuffer、StringBuilder
| 对比项 | String | StringBuffer | StringBuilder | |--------|--------|--------------|---------------| | 可变性 | 不可变 | 可变 | 可变 | | 线程安全 | 安全(不可变天然安全) | 安全(方法加synchronized) | 不安全 | | 性能 | 拼接慢(创建新对象) | 较慢(有锁) | 最快 | | 使用场景 | 少量字符串操作 | 多线程共享变量拼接 | 单线程大量拼接 |
五、volatile 关键字
5.1 两大特性
- 可见性:一个线程修改volatile变量,其他线程立即可见。底层通过缓存一致性协议(MESI)实现。
- 禁止指令重排序:通过内存屏障实现。
5.2 内存屏障
- 写操作:写前插入StoreStore屏障,写后插入StoreLoad屏障
- 读操作:读前插入LoadLoad屏障,读后插入LoadStore屏障
5.3 不保证原子性
volatile不能保证原子性,比如count++不是原子操作。解决方案:
- 使用AtomicInteger(CAS)
- 使用synchronized
- 使用LongAdder(高并发推荐)
5.4 经典应用
- 状态标志位:
volatile boolean flag = true; - DCL单例模式中的防止指令重排序
六、synchronized vs ReentrantLock
| 对比项 | synchronized | ReentrantLock | |--------|-------------|---------------| | 实现层次 | JVM关键字 | API层面(Java类) | | 加锁解锁 | 自动 | 手动(lock/unlock) | | 公平性 | 非公平 | 可设置公平/非公平 | | 可中断性 | 不支持 | 支持lockInterruptibly() | | 条件等待 | 只能notify/notifyAll | 可创建多个Condition | | 锁状态 | 不可判断 | 可通过tryLock()判断 | | 底层原理 | 对象头Mark Word + Monitor | AQS(AbstractQueuedSynchronizer) |
JDK 1.6 synchronized优化:
- 偏向锁:只有一个线程访问时,偏向该线程
- 轻量级锁:少量线程竞争时,自旋获取
- 重量级锁:大量线程竞争时,阻塞等待
- 锁升级:偏向锁 → 轻量级锁 → 重量级锁(不可逆)
七、线程池核心参数
ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, // 核心线程数 maximumPoolSize, // 最大线程数 keepAliveTime, // 空闲线程存活时间 TimeUnit.SECONDS, // 时间单位 workQueue, // 任务队列 threadFactory, // 线程工厂 handler // 拒绝策略 );7.1 执行流程
- 线程数 < corePoolSize:创建核心线程执行
- 线程数 >= corePoolSize:放入workQueue
- workQueue满了 且 线程数 < maximumPoolSize:创建临时线程
- workQueue满了 且 线程数 >= maximumPoolSize:执行拒绝策略
7.2 4种拒绝策略
| 策略 | 说明 | |------|------| | AbortPolicy | 抛RejectedExecutionException(默认) | | CallerRunsPolicy | 调用者线程自己执行 | | DiscardPolicy | 直接丢弃,不抛异常 | | DiscardOldestPolicy | 丢弃队列中最早的任务,然后重新提交 |
7.3 如何合理设置线程数
- CPU密集型:
N+1(N为CPU核数) - IO密集型:
2N或N / (1 - 阻塞系数)
八、Spring IOC 和 AOP
8.1 IOC(控制反转)
- 概念:将对象的创建和管理权从程序代码转移到Spring容器
- DI(依赖注入):IOC的具体实现方式
- 注入方式:构造器注入(推荐)、setter注入、字段注入
8.2 AOP(面向切面编程)
- 核心概念:切面(Aspect)、通知(Advice)、切点(Pointcut)、连接点(JoinPoint)
- 通知类型:@Before、@After、@Around、@AfterReturning、@AfterThrowing
- 底层实现:JDK动态代理(有接口)和CGLIB代理(无接口)
- 应用场景:日志记录、权限校验、事务管理、性能监控
九、Redis缓存三大问题
9.1 缓存穿透
- 现象:查询不存在的数据,请求直达数据库
- 解决方案:
- 缓存空值(设置较短过期时间)
- 布隆过滤器(判断key是否存在)
9.2 缓存击穿
- 现象:热点key过期,大量并发请求直达数据库
- 解决方案:
- 互斥锁(setnx)
- 热点key永不过期(后台异步刷新)
9.3 缓存雪崩
- 现象:大量key同时过期 或 Redis宕机
- 解决方案:
- 过期时间加随机值
- Redis集群(主从+哨兵/Cluster)
- 本地缓存(Caffeine)+ 限流降级
十、MySQL索引失效场景
- 对索引列使用函数:
WHERE SUBSTR(name,1,3)='abc' - 隐式类型转换:字段是int,传字符串
WHERE id='123' - 不符合最左前缀原则:联合索引(a,b,c),查询只用了b和c
- like以%开头:
WHERE name LIKE '%abc' - OR条件不全是索引列:
WHERE a=1 OR b=2,b不是索引 - 数据量极小:MySQL认为全表扫描更快
- 索引列参与计算:
WHERE a+1=100 - 使用不等于:
WHERE a != 1 - IS NOT NULL:某些情况下
十一、RabbitMQ消息可靠性
11.1 消息丢失的三种场景
| 阶段 | 解决方案 | |------|----------| | 生产者 → Broker | Confirm机制 + 重试 | | Broker自身 | 持久化(交换机、队列、消息)+ 镜像队列 | | Broker → 消费者 | 手动ACK + 幂等性处理 |
11.2 消息幂等性
- 数据库唯一索引
- Redis存消息ID(setnx)
- 业务逻辑本身幂等(如状态机)
十二、DDD领域驱动设计核心概念
- 实体(Entity):有唯一标识,可变的业务对象
- 值对象(Value Object):无唯一标识,不可变的属性集合
- 聚合(Aggregate):一组相关对象的集合,包含聚合根
- 聚合根(Aggregate Root):聚合的入口,外部只能通过聚合根操作聚合内部
- 领域事件(Domain Event):领域内发生的重要事件
- 限界上下文(Bounded Context):领域的边界,每个限界上下文有独立的通用语言
- 仓储(Repository):聚合的存储和检索接口
DDD四层架构:
- Interface(接口层):接收请求,返回响应
- Application(应用层):编排业务,不包含业务逻辑
- Domain(领域层):核心业务逻辑
- Infrastructure(基础设施层):技术实现(数据库、MQ等)
写在最后
谢飞机的故事虽然搞笑,但背后反映了很多程序员"知其然不知其所以然"的通病。
面试官让"回去等通知",其实就是委婉的拒绝。所以各位小伙伴们,看完这篇文章,赶紧把上面的知识点吃透,下次面试可别像谢飞机一样翻车啦!
记住:面试不是背答案,而是要真正理解原理。只有把知识内化成自己的,才能在面试中游刃有余!
祝大家都能拿到心仪的Offer!加油!
