面灵AI→

超聚变 软件开发一面面经:锁、线程池参数推演与 OOM 排查

轮次
一面
时间
2026-09
来源
牛客网

《面试题目》

  1. 自我介绍
  2. 介绍一下你的 Agent 项目
  3. 这个业务中有没有什么技术难点?
  4. Java 中的锁有哪些?
  5. synchronized 是轻量级锁还是重量级锁?
  6. synchronized 一定是线程安全的吗?
  7. 线程会有几种状态?
  8. 有一个四核 8G 的机器,运行一段时间之后经常会出现 OOM 的问题,能帮我排查一下这个问题吗?
  9. 线程池的核心参数有哪些?
  10. 现在设置了最大线程 20 个、核心线程 15 个,写了 18 个线程,那整个线程的状态是什么样子的?
  11. 那如果现在写了 22 个线程呢?
  12. Redis 的过期策略有哪些?
  13. MySQL 的 MVCC 的原理是什么?
  14. MQ 有用过吗?怎么保证 MQ 不丢消息?
  15. 一些软性问题(为什么想来郑州、与同事沟通等)

《参考解析》

  1. synchronized 的锁升级要按版本讲:早期 JDK 里 synchronized 直接就是重量级锁(依赖操作系统的互斥量,涉及用户态与内核态切换);JDK 6 之后引入了偏向锁、轻量级锁、重量级锁的升级路径,锁状态记录在对象头的 Mark Word 里。所以正确答案是「现在它既可能是轻量级也可能是重量级,取决于竞争程度,会随竞争升级」。再往深一层要知道偏向锁在 JDK 15 起默认关闭、JDK 18 之后已废弃,别把过时结论当标准答案。

  2. synchronized 不等于线程安全:它只保证同一把锁下临界区的互斥,锁对象不同、锁粒度不对、或者在锁外读写共享变量,都照样出问题。常见反例是 synchronized 加在方法上但锁的是不同实例(等价于没锁)、包装类型被重新赋值导致锁对象变化、以及复合操作里只锁了其中一半。答这题最好带一句「真正保证可见性与有序性还要配合 volatile 或 final,原子性由锁或 CAS 提供」。

  3. 线程池参数推演是这场的重点:核心线程 15、最大线程 20、队列容量记为 Q。提交 18 个任务时,15 个先占核心线程,剩下 3 个进队列,此时线程数还是 15、没有触发扩容;只有当队列满了才会继续创建线程直到 20,再多才走拒绝策略。所以「18 个线程时的状态」取决于队列容量——如果队列是无界的(LinkedBlockingQueue 默认),线程数永远停在核心线程数,最大线程数形同虚设。22 个任务同理:先看队列是否已满。把「核心 → 队列 → 最大 → 拒绝」这个顺序讲清楚,再点出无界队列会让 maximumPoolSize 失效这个坑,这题就答满了。

  4. OOM 排查要给出可执行的步骤:先确认是堆内还是堆外——看异常信息(Java heap space / Metaspace / unable to create new native thread / Direct buffer memory)就能大致定位。堆内问题加 -XX:+HeapDumpOnOutOfMemoryError 拿到 dump,用 MAT 或 JProfiler 看支配树找大对象;同时看 GC 日志确认是内存泄漏(老年代持续增长、Full GC 后不降)还是单纯内存不足。四核 8G 的机器还要考虑容器场景:JVM 默认堆上限按物理内存比例算,容器里没设 -XX:MaxRAMPercentage 或 -Xmx 时容易和系统其他进程抢内存,容器内存限制偏小也会被 OOM Killer 干掉。常见根因是大量对象堆积、缓存无上限、连接或流没关。

  5. Redis 过期策略是「惰性删除 + 定期删除」组合:惰性删除是访问 key 时才检查是否过期,省 CPU 但会留下垃圾;定期删除是每 100ms 随机抽查一部分设置了过期时间的 key,把过期的清掉,避免全量扫描卡住主线程。内存吃紧时还要接上淘汰策略(maxmemory-policy),allkeys-lru / volatile-lru / allkeys-lfu 各自适用面不同;如果业务要求过期即不可见,必须靠惰性删除兜底 + 主动删除关键 key,不能假设到点就立刻消失。

  6. MVCC 讲四个要素:隐藏字段(trx_id、roll_pointer)、undo log 版本链、ReadView(活跃事务列表)以及快照读与当前读的区别。RC 与 RR 的差别就在 ReadView 的生成时机——RC 每次查询都重新生成,RR 只在事务第一次快照读时生成一次,因此 RR 下同一事务内多次读结果一致。要注意 select ... for update、update、delete 都是当前读,读的是最新版本并加锁,MVCC 不负责这部分。

  7. MQ 不丢消息要分段回答:生产端开确认机制(RabbitMQ 的 publisher confirm、Kafka 的 acks=all 加重试)并保证发送与业务落库的一致性(本地消息表或事务消息);Broker 端靠副本与刷盘策略(Kafka 的 min.insync.replicas 与 unclean.leader.election 关闭);消费端把自动提交改为手动 ack,处理成功才提交,失败进重试队列或死信队列,同时业务侧做幂等(唯一键、去重表)应对重复投递。只答「手动 ack」会被继续追问重复消费怎么办。