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