小米面试官问到最后,画风突然崩了

昨晚十一点半,我一个读者突然甩给我一张截图。

小米 offer。

我盯着屏幕愣了两秒,第一反应是:卧槽,真拿下了?

第二反应是:这兄弟这几个月天天在后台问我 ArrayList 和 LinkedList 有啥区别,问到我都能背出来了。

现在好了,人上岸了,问题还留给我了。

那今天就把这套面经原样甩出来。小米的,纯 Java 向,一共十来道题,说实话,比大厂难度低一截,但每一道都是地基级的东西,答不上来一样凉。

你要是最近也在投 Java 岗,建议直接收藏。


先问个送命题:ArrayList 和 LinkedList,你能三句话说清楚吗?

很多人张口就是"一个数组一个链表",然后就卡壳了。

这谁顶得住啊。

其实往下追一层就行:

数组连续存储,查快,O(1)。链表零散存储,查慢,得一个个找,O(n)。

反过来,插入删除呢?

数组中间插一个,后面全得挪位置,O(n)。链表不用挪,改改指针就行,O(1)。

内存呢?链表每个节点还得多存两个指针,所以同样多的数据,链表更占地方。

你发现没有?面试官问的从来不是"背没背过",而是"你有没有真的理解过一次"。


HashMap 的 put,到底经历了什么

这题几乎是 Java 面试的标配,年年问,年年有人答不利索。

流程说白了就八步:

算哈希、定位置、判断是否为空。

空的话,直接建个新节点放进去。

不空呢?先看第一个节点的键是不是一样的,一样就直接覆盖旧值。

不一样,那就得往下找。链表就一个个比对,红黑树就按树的规则查。

找到了,覆盖。没找到,插进去。

插进去之后还没完,还得看两件事:

链表长度超过 8 且数组长度够 64,直接升级成红黑树。

负载因子超过 0.75,直接扩容,翻倍。

你看,一个 put 方法,里面藏了这么多弯弯绕绕。

不过话说回来,这也正是 HashMap 这么多年扛得住高并发查询的原因。

对了,有个细节容易被问懵:HashMap 的键和值都能是 null。别被这个坑到。


HashMap 为啥线程不安全?

老版本(1.7)扩容的时候,多线程一起搞,链表能搞出个死循环,数据说丢就丢。

新版本(1.8)虽然把死循环问题解决了,但多线程同时 put,还是可能出现数据覆盖。

所以别管哪个版本,HashMap 就一个字:

别在多线程环境里用它。

想安全,换 ConcurrentHashMap。


那 get 方法总安全了吧?

不一定。

你要是用 null 当 key 去 get,前提是 HashMap 本身已经初始化了,那没问题,HashMap 支持 null 键。

但如果 HashMap 都没初始化,直接空指针异常。

还有更坑的:你这边线程在 get,那边线程在改结构,没做好同步,读出来的结果可能就是错的,甚至直接抛异常。

这事要是搁你身上,你怎么选?

答案还是那句话:多线程场景,老老实实用 ConcurrentHashMap。


ConcurrentHashMap 凭啥线程安全

1.7 版本靠的是"分段锁"。

把整个 Map 切成一段一段的 Segment,每段配一把锁。线程 A 占着这段锁干活,线程 B 完全可以去占另一段,互不耽误。

听着挺聪明,但 1.8 觉得还不够快。

1.8 直接换了思路:加锁的粒度从"一整段"缩小到"一个头节点"。

数据结构也升级成了数组 + 链表/红黑树,链表太长就转红黑树,查询效率从 O(n) 直接干到 O(logn)。

锁更细,冲突更少,性能自然更高。

这就是"从粗放到精细"的进化,工程里这种思路到处都是。


synchronized 和 ReentrantLock,到底选哪个

这题考的不是背诵,是理解场景。

synchronized 用起来省心,自动加锁自动释放,但它是非公平锁,不能响应中断。

ReentrantLock 要自己手动加锁解锁,麻烦是麻烦,但换来的是灵活:既能公平也能不公平,还能响应中断,能主动解死锁的局。

底层实现也不一样。synchronized 靠 JVM 的监视器,ReentrantLock 靠 AQS。

简单说:省事选 synchronized,要灵活选 ReentrantLock。


JVM 内存结构,五分钟说明白

程序计数器:每个线程一份,记着当前执行到哪一行。

Java 虚拟机栈:存局部变量、操作数栈这些,方法调用一次压一次栈。

本地方法栈:跟上面差不多,专门伺候 native 方法。

Java 堆:最大的一块,对象都住这儿,年轻代老年代都在这。

方法区:类信息、常量池、静态变量的家,现在叫元空间。

背下来不难,难的是别人问一句"堆里年轻代怎么分区",你能不能接得住。


垃圾回收,几种算法你分得清吗

标记清除:先标记要回收的,再统一清掉。问题是效率不高,还留下一堆内存碎片。

复制算法:内存分两半,一半用,一半空。用满了就把活的对象搬到另一半,原来那半直接清空。碎片问题解决了,代价是白白浪费一半空间。

标记整理:标记完不直接清,而是把活的对象都挪到一头,再把剩下的一口气清掉。

分代回收:把内存分成新生代和老年代,对象活得越久,年纪越大,攒够岁数就升去老年代。

新生代具体用啥算法?

复制算法。分成一个 Eden 区和两个 Survivor 区,每次 GC 完谁活下来就挪个地方,From 和 To 来回换角色。

这套逻辑不难,难的是你得知道"为什么这么设计",而不是死记硬背。


强引用软引用弱引用虚引用,你是不是也总搞混

强引用:只要它还在,垃圾回收器不敢动。

软引用:内存够用就留着,内存告急了才清,常拿来做缓存。

弱引用:不管内存够不够,垃圾回收器扫到就清,活得比软引用还短。

虚引用:形同虚设,压根不影响对象的生死,主要配合引用队列用来监控对象啥时候被回收。

四种引用,本质上就是"垃圾回收器对这个对象有多狠"的四个等级。


Redis 持久化,三种方式说清楚

Redis 快是因为数据都在内存里,但重启一断电,内存就清零了。

所以得落到磁盘上,靠的是三种方式:

AOF:每来一条写命令,就往文件里追加一条。

RDB:某个时间点,把整个内存数据拍个快照,写进磁盘。

混合持久化:4.0 新增的,把 AOF 和 RDB 的优点捏一块。


Redis 分布式锁,面试必考中的必考

这题基本是每家公司都会问的经典款。

核心就一条命令:

SET lock_key unique_value NX PX 10000

NX 保证了"key 不存在才能设置",这就是加锁的关键。

PX 设置过期时间,防止客户端拿到锁之后挂了,锁永远释放不掉。

unique_value 是每个客户端的专属标识,避免释放的时候把别人的锁误删了。

解锁呢?可不是简单 del 一下就完事。

得先判断这把锁是不是你加的,是的话才能删,而且这个"判断+删除"必须是原子操作,不然中间一插队,照样出乱子。

所以解锁用的是 Lua 脚本:

if redis.call("get",KEYS[1]) == ARGV[1] then
    return redis.call("del",KEYS[1])
else
    return 0
end

一条 SET,一段 Lua,单节点分布式锁就这么搞定了。


面试全程都是硬核 Java,气氛挺正经的。

结果最后一题,画风突变:

"你觉得小米汽车的发布,对整个车圈影响怎么样?"

我这读者当场愣住,心想这是要转行聊车了?

这题答得怎么样我也不知道,反正 offer 是拿到了。

你说,这种"临门一脚"的开放题,到底是考察你的知识面,还是就单纯想看看你紧张起来是啥反应?

评论区聊聊呗,你面试的时候有没有遇到过这种"神来一笔"的收尾题?

posted on 2026-07-14 16:08  cqrocky  阅读(11)  评论(0)    收藏  举报