小米 Java 软件开发工程师一面面经
1. 你的项目中使用了消息队列(MQ),如何保证消息的可靠性投递(不丢失)?
- 回答:保证可靠性需要从三个阶段入手: 生产者阶段:开启确认机制(如 RabbitMQ 的 Confirm 模式或 RocketMQ 的同步发送)。发送失败时进行重试,或者记录日志入库。MQ 节点阶段:开启持久化(交换机、队列、消息全部持久化到磁盘)。如果是集群环境,使用镜像队列或多副本同步,确保 Leader 宕机后数据不丢。消费者阶段:关闭自动 ACK,改为手动 ACK。只有在业务逻辑处理成功后才返回确认,防止消息在处理中途由于异常丢失。
2. 谈谈 Netty 的高性能架构,为什么它比传统的 Tomcat 连接器更快?
- 回答:Netty 采用了主从多线程 Reactor 模型。 非阻塞 IO:利用 Linux 的 epoll 机制,一个线程可以监控成千上万个连接。零拷贝(Zero-Copy):通过 DirectBuffer 直接操作堆外内存,减少内核态与用户态的数据拷贝;使用 CompositeByteBuf 组合数据,避免内存搬移。无锁串行化:消息处理在特定的 EventLoop 线程内完成,避免了多线程竞争锁的开销。
3. Java 中的 String 为什么要设计成不可变的(final)?
- 回答: 安全性:String 经常作为 HashMap 的 Key 或网络连接参数,不可变性保证了哈希值的稳定性及代码安全性。常量池优化:由于不可变,多个相同的字符串可以共享同一个物理地址,节省内存。线程安全:不可变对象天生线程安全,无需同步。
4. 详细描述 JVM 的垃圾回收过程,特别是 G1 收集器的 Region 设计。
- 回答:G1 取代了物理上的年轻代和老年代划分,改为大小相等的 Region。 可预测停顿:G1 会根据用户设定的最大停顿时间,优先回收价值(存活对象少)最大的 Region。回收过程:初始标记(STW)-> 并发标记 -> 最终标记(STW)-> 筛选回收(STW)。优势:由于采用复制算法,Region 级别的整理避免了内存碎片。
5. 什么是“虚假唤醒”(Spurious Wakeup)?在编写 wait/notify 时如何避免?
- 回答:虚假唤醒是指线程在没有收到
notify信号的情况下从wait状态中醒来。 避免方法:必须将 wait() 方法放在 while 循环中检查条件,而不是 if 语句中。
6. HashMap 在 JDK 1.8 之后做了哪些优化?为什么 1.7 的版本会产生死循环?
- 回答: 1.8 优化:引入红黑树(链表长度 > 8 且数组长度 > 64 时转换),优化查找效率从 O(n) 到 O(log n);扩容时从头插法改为尾插法。死循环原因(1.7):1.7 扩容采用头插法,多线程环境下会导致链表节点的顺序反转,形成环形链表。1.8 尾插法保持了顺序,从而避免了环的产生。
7. 谈谈 CAS 的原理及其带来的 ABA 问题如何解决?
- 回答:CAS(Compare And Swap)利用 CPU 的原子指令完成读-改-写。 ABA 问题:一个值从 A 变到 B 再变回 A,CAS 会认为它没变过。解决方法:引入版本号或时间戳。Java 中提供了 AtomicStampedReference 类,每次更新时对比值和版本号。
8. 线程池的 allowCoreThreadTimeOut 参数有什么作用?
- 回答:默认情况下,核心线程(CorePoolSize)即使空闲也不会被销毁。开启该参数后,核心线程在空闲时间超过
keepAliveTime时也会被回收。这在业务波动较大、夜间流量极低的场景下有助于节省服务器资源。
9. volatile 关键字如何保证可见性?它能保证原子性吗?
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
Java面试圣经 文章被收录于专栏
Java面试圣经,带你练透java圣经