面灵AI→

顺丰 Java 线下面(一面 + 二面)

时间
2026-09
来源
牛客网

《面试题目》

一面

  1. 拷打实习。
  2. 线程池核心参数。
  3. IoC、AOP。
  4. 消息队列的交换机有哪些,具体什么作用?
  5. MVCC 讲解一下。
  6. JVM 有哪些区域?
  7. 哪些区域不会 OOM?
  8. 反问。

二面

  1. 拷打实习。
  2. 做了什么优化?为什么会出现这样的情景?这么做的目的是什么?
  3. 如果一个用户查询要关联多表的情况怎么办?
  4. 怎么去理解死信队列?
  5. 讲一下死锁。
  6. 反问。

《参考解析》

  1. 线程池核心参数怎么答才算完整:七个参数要能一口气说清并且知道每个的取舍——核心线程数、最大线程数、空闲存活时间、时间单位、工作队列、线程工厂、拒绝策略。执行流程是「核心线程未满就新建线程 → 核心满了进队列 → 队列满了才扩到最大线程数 → 再满就走拒绝策略」,这个顺序反直觉的地方在于「先排队再加线程」,很多人会答反。参数怎么定才是真正被追问的:CPU 密集型按核数 + 1,IO 密集型按「核数 × (1 + 等待时间 / 计算时间)」估,但更实际的做法是压测调参。队列一定要用有界队列,Executors.newFixedThreadPool 那种无界 LinkedBlockingQueue 在任务堆积时会一直涨到 OOM,这也是阿里规约不让用 Executors 的原因。拒绝策略四种:直接抛异常、调用方线程执行(相当于反压)、丢弃最老的、静默丢弃,业务上要么让调用方感知到并降级,要么必须记日志,静默丢弃是最危险的。
  2. IoC 与 AOP:IoC 是把对象的创建和依赖装配交给容器,好处不只是「解耦」——它让对象可以统一被代理,这才是 AOP 能成立的前提。实现上容器启动时扫描 BeanDefinition,通过反射实例化,再按类型或名称注入依赖,循环依赖靠三级缓存解决(提前暴露未完成初始化的对象引用)。AOP 是运行期或编译期生成代理,Spring 默认对实现了接口的类用 JDK 动态代理、没有接口的用 CGLIB;被追问得最多的是「同类内部方法调用为什么切面失效」,因为 this.method() 走的是原对象而不是代理对象,解法是注入自己、AopContext.currentProxy() 或者拆到另一个 Bean。
  3. RabbitMQ 的四种交换机:direct 按 routing key 精确匹配;topic 按通配符匹配,* 匹配一个词、# 匹配零个或多个词,适合按业务维度做订阅;fanout 忽略 routing key 广播到所有绑定队列,适合配置刷新、缓存失效通知这类场景;headers 按消息头键值匹配,性能差、实际用得少。面试官顺着通常会问「怎么保证消息不丢」,答案要覆盖三段:生产者用 confirm 机制确认投递到 broker、broker 侧交换机和队列都设持久化、消费端关掉自动 ack 改成处理完手动 ack。三段缺一段就可能丢,另外还要防重复消费,也就是消费端做幂等。
  4. MVCC 是怎么工作的:核心是每行记录上的隐藏字段(最近修改它的事务 ID trx_id 和指向 undo log 的回滚指针 roll_pointer),加上 read view。InnoDB 在 RC 和 RR 两个隔离级别下都用 MVCC 实现快照读,区别只在 read view 的生成时机——RC 是每次 select 都生成一个新的,所以能看到别人已提交的修改;RR 是事务里第一次 select 时生成一次、之后复用,所以整个事务看到的是同一个快照,这也正是 RR 能防可重复读的原因。可见性判断规则是:trx_id 小于当前活跃事务最小 ID 的可见,大于等于下一个待分配 ID 的不可见,落在活跃区间里的不可见;不可见就顺着 roll_pointer 沿 undo log 往前找。要注意快照读(普通 select)靠 MVCC,当前读(select for update、lock in share mode、update、delete)靠的是行锁加间隙锁,别把两者混为一谈。
  5. JVM 区域和「哪些不会 OOM」:区域分五块——堆、方法区(JDK 8 之后是元空间,落在本地内存)、虚拟机栈、本地方法栈、程序计数器。会 OOM 的是堆(Java heap space)、元空间(Metaspace)和直接内存(Direct buffer memory,不在上面五块里但要提)。虚拟机栈和本地方法栈正常不会报 OOM,递归太深报的是 StackOverflowError——一个是「栈帧太多超了深度」,一个是「内存不够」,性质不同;但线程数开太多导致创建不出新线程时,报的是 unable to create new native thread,那是本机内存和线程数上限的问题。程序计数器是唯一任何情况下都不会 OOM 的区域,这通常就是这道题想要的答案。
  6. 死信队列是什么:死信是那些正常情况下没法被消费的消息,进死信的三种典型原因:消费被 nack 或 reject 且 requeue 设为 false、消息 TTL 过期、队列长度超过上限被丢弃。把这些消息路由到一个专门的队列就是死信队列,作用有三个:不让坏消息把正常队列堵死、保留现场方便排查、以及用 TTL + 死信做延时任务(下单 30 分钟未支付自动取消就是这么实现的)。要注意死信队列不是自动重试机制,如果配了重试又没设上限,消息会反复进出直到超过重试次数才落死信,上线前得确认这个阈值。
  7. 死锁的四个必要条件和排查:互斥、占有并等待、不可剥夺、循环等待,四个同时成立才死锁,破坏任何一个就能避免——工程上最常动的是「循环等待」,方法是给所有锁定义全局顺序、按顺序加锁,或者用带超时的 tryLock 破坏「不可剥夺」。排查靠 jstack <pid>,输出里搜 Found one Java-level deadlock 会直接给出互相等待的线程和它们各自持有的锁。预防上还有几条经验:锁的粒度和持锁时间要尽量小、不要在持锁时做 RPC 或查库、synchronized 嵌套时警惕锁顺序反转。数据库层面的死锁是另一回事,InnoDB 会自己检测并回滚代价小的事务,应用侧要做的是捕获死锁异常后重试。
  8. 「用户查询要关联多表怎么办」:先问清楚是不是真的需要 join——很多场景下分别查两张表在应用层拼装,比一条大 join 更好维护也更容易缓存。如果必须关联,按照表的数据量和过滤性决定驱动表,让被驱动表的关联字段上有索引,避免 join 的列上有函数或隐式类型转换导致索引失效。再往上就是读写分离和宽表:查询量大时把关联结果预计算成宽表或走搜索引擎,把在线 join 从主库挪走。这道题答「先看能不能不 join」通常比直接背 join 优化技巧更能拿到分。