超聚变 软件开发一面面经:锁、线程池参数推演与 OOM 排查
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- 自我介绍
- 介绍一下你的 Agent 项目
- 这个业务中有没有什么技术难点?
- Java 中的锁有哪些?
- synchronized 是轻量级锁还是重量级锁?
- synchronized 一定是线程安全的吗?
- 线程会有几种状态?
- 有一个四核 8G 的机器,运行一段时间之后经常会出现 OOM 的问题,能帮我排查一下这个问题吗?
- 线程池的核心参数有哪些?
- 现在设置了最大线程 20 个、核心线程 15 个,写了 18 个线程,那整个线程的状态是什么样子的?
- 那如果现在写了 22 个线程呢?
- Redis 的过期策略有哪些?
- MySQL 的 MVCC 的原理是什么?
- MQ 有用过吗?怎么保证 MQ 不丢消息?
- 一些软性问题(为什么想来郑州、与同事沟通等)
《参考解析》
-
synchronized 的锁升级要按版本讲:早期 JDK 里 synchronized 直接就是重量级锁(依赖操作系统的互斥量,涉及用户态与内核态切换);JDK 6 之后引入了偏向锁、轻量级锁、重量级锁的升级路径,锁状态记录在对象头的 Mark Word 里。所以正确答案是「现在它既可能是轻量级也可能是重量级,取决于竞争程度,会随竞争升级」。再往深一层要知道偏向锁在 JDK 15 起默认关闭、JDK 18 之后已废弃,别把过时结论当标准答案。
-
synchronized 不等于线程安全:它只保证同一把锁下临界区的互斥,锁对象不同、锁粒度不对、或者在锁外读写共享变量,都照样出问题。常见反例是
synchronized加在方法上但锁的是不同实例(等价于没锁)、包装类型被重新赋值导致锁对象变化、以及复合操作里只锁了其中一半。答这题最好带一句「真正保证可见性与有序性还要配合 volatile 或 final,原子性由锁或 CAS 提供」。 -
线程池参数推演是这场的重点:核心线程 15、最大线程 20、队列容量记为 Q。提交 18 个任务时,15 个先占核心线程,剩下 3 个进队列,此时线程数还是 15、没有触发扩容;只有当队列满了才会继续创建线程直到 20,再多才走拒绝策略。所以「18 个线程时的状态」取决于队列容量——如果队列是无界的(
LinkedBlockingQueue默认),线程数永远停在核心线程数,最大线程数形同虚设。22 个任务同理:先看队列是否已满。把「核心 → 队列 → 最大 → 拒绝」这个顺序讲清楚,再点出无界队列会让maximumPoolSize失效这个坑,这题就答满了。 -
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 干掉。常见根因是大量对象堆积、缓存无上限、连接或流没关。 -
Redis 过期策略是「惰性删除 + 定期删除」组合:惰性删除是访问 key 时才检查是否过期,省 CPU 但会留下垃圾;定期删除是每 100ms 随机抽查一部分设置了过期时间的 key,把过期的清掉,避免全量扫描卡住主线程。内存吃紧时还要接上淘汰策略(
maxmemory-policy),allkeys-lru/volatile-lru/allkeys-lfu各自适用面不同;如果业务要求过期即不可见,必须靠惰性删除兜底 + 主动删除关键 key,不能假设到点就立刻消失。 -
MVCC 讲四个要素:隐藏字段(
trx_id、roll_pointer)、undo log 版本链、ReadView(活跃事务列表)以及快照读与当前读的区别。RC 与 RR 的差别就在 ReadView 的生成时机——RC 每次查询都重新生成,RR 只在事务第一次快照读时生成一次,因此 RR 下同一事务内多次读结果一致。要注意select ... for update、update、delete都是当前读,读的是最新版本并加锁,MVCC 不负责这部分。 -
MQ 不丢消息要分段回答:生产端开确认机制(RabbitMQ 的 publisher confirm、Kafka 的
acks=all加重试)并保证发送与业务落库的一致性(本地消息表或事务消息);Broker 端靠副本与刷盘策略(Kafka 的min.insync.replicas与unclean.leader.election关闭);消费端把自动提交改为手动 ack,处理成功才提交,失败进重试队列或死信队列,同时业务侧做幂等(唯一键、去重表)应对重复投递。只答「手动 ack」会被继续追问重复消费怎么办。