小米面试官问到最后,画风突然崩了
昨晚十一点半,我一个读者突然甩给我一张截图。
小米 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 是拿到了。
你说,这种"临门一脚"的开放题,到底是考察你的知识面,还是就单纯想看看你紧张起来是啥反应?
评论区聊聊呗,你面试的时候有没有遇到过这种"神来一笔"的收尾题?
浙公网安备 33010602011771号