面灵AI→

顺丰 Java 后端一面面经(无手撕)

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

《面试题目》

  1. 你平常用多线程做了哪些任务?
  2. 多线程的核心参数有哪些?
  3. 多线程的任务加入流程是怎样的?
  4. 多线程的无界队列和有界队列怎么选择?
  5. 有哪些拒绝策略?
  6. 使用无界队列的时候发生 OOM 了怎么办?
  7. OOM 的排查过程是怎样的?
  8. 有哪些专业软件可以用来分析 OOM?
  9. Arthas 的使用方法是怎样的?
  10. Redis 为什么快?
  11. Redis 的分布式锁是怎么实现的?
  12. Redisson 是怎么进行锁续期的?
  13. 应用内存和 Redis 内存有什么区别?
  14. 如果让你设计,你怎么设计应用内存、Redis 和数据库之间的交互架构?
  15. 你的设计有什么缺点?
  16. MySQL 的 B 树和 B+ 树有什么区别?
  17. B+ 树相对于 B 树的缺点是什么?
  18. Kafka 的高水位线是什么意思?
  19. 介绍一下简历里的 Agent 项目。
  20. 怎么设计记忆模块?长期和短期记忆是怎么设计的?
  21. 上下文压缩是怎么设计的?
  22. LangGraph 中编排的流程是动态的还是线性的?
  23. 反问。

《参考解析》

线程池:参数、入队流程与队列选择。七个参数是 corePoolSize、maximumPoolSize、keepAliveTime 与 unit、workQueue、threadFactory、handler。任务提交顺序必须背准:核心线程未满就新建核心线程执行;核心满了入队;队列满了才扩到最大线程数;再满交给拒绝策略。因此队列类型直接决定行为:有界队列(ArrayBlockingQueue 或指定容量的 LinkedBlockingQueue)让扩容与拒绝生效;默认构造的无界 LinkedBlockingQueue 会让 maximumPoolSize 形同虚设,任务无限堆积直到 OOM,表现为「服务没报错但请求全部超时」。选型判据是「上游能不能等、下游能不能扛」:核心业务用有界队列加 CallerRunsPolicy 形成反压,可丢弃的旁路任务(埋点、日志)用有界队列加 DiscardPolicy 并打日志,不允许排队的用 SynchronousQueue 配较大的最大线程数。线程数按 CPU 密集型取「核数加一」、IO 密集型按「核数乘 (1 加等待比)」估算,最终以压测为准。

OOM 排查与 Arthas 的使用。先判断区域:Java heap space 是堆,GC overhead limit exceeded 多为泄漏,Metaspace 是类元数据过多,unable to create new native thread 是线程触顶,Direct buffer memory 是堆外内存。准备动作比事后补救更重要:加 -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath,容器里让 -Xmx 与内存 limit 对齐,避免被 OOMKiller 直接杀掉而没有现场。分析链路是 jstat -gcutil 看各代增长与 GC 频率 → jmap -histo:live 看对象分布 → dump 后用 MAT 看支配树与 GC Roots 引用链,判断是「一次性加载太多」(集合未分页、批量查询、缓存无上限)还是「缓慢泄漏」(监听器未注销、ThreadLocal 未 remove、连接或流未关闭)。工具按场景选:MAT 做离线 dump 分析,async-profiler 出低开销火焰图,jstack 看线程栈。Arthas 常用命令要能报出来:dashboard 看整体、thread -n 3 找最忙线程、thread -b 定位阻塞其他线程的锁、jad 反编译确认线上代码版本、watch 与 trace 看入参与耗时、stack 看调用来源、heapdump 导堆、vmtool 查对象、redefine 热替换类。风险是 trace 这类命令在大流量方法上要加条件与次数限制,否则会把线上打崩。

Redis 为什么快、分布式锁与 Redisson 续期。快是多层叠加的结果:数据在内存里、避免磁盘 IO;单线程处理命令,没有锁竞争与线程切换,也保证命令原子性(6.0 后 IO 多线程只做读写与协议解析);epoll 事件循环让单线程能扛大量连接;数据结构与编码做了针对性优化(listpack、intset、跳表、渐进式 rehash);协议简单并支持 pipeline 与 Lua 减少往返。分布式锁的标准实现是 SET key value NX PX ttl,value 用 UUID 加线程标识,释放时用 Lua 先比对再删除以避免误删。Redisson 在此基础上封装了可重入、阻塞等待与看门狗:加锁成功后启动后台任务,默认每 10 秒(TTL 的三分之一)检查业务是否仍在执行,是就把过期时间续回 30 秒;客户端宕机则续期停止、锁到期自动释放。两个坑必须说:只有不显式指定 leaseTime 时才启用看门狗,指定固定租期就没有自动续期;主从切换或网络分区时锁仍可能失效,所以业务必须叠加幂等,锁只能降低冲突概率。

应用内存与 Redis 内存的区别,以及三层缓存架构。区别在归属、可见性、容量与失效方式:应用内存属于 JVM 堆,随实例扩缩、实例间不共享、受 GC 与 -Xmx 限制,本地命中零网络开销但一致性最难;Redis 是独立进程,按 maxmemory 与淘汰策略(LRU、LFU、TTL、随机)管理容量,所有实例共享同一份数据,代价是网络往返与序列化。设计原则是「数据库是唯一真相源,缓存只做加速」:读走「本地缓存(可选,秒级 TTL)→ Redis → 数据库」依次回源并回填,回填加随机过期时间避免雪崩,热点用互斥重建或逻辑过期防击穿,不存在的键用空值或布隆过滤器防穿透;写走 Cache-Aside,先更新数据库再删缓存,必要时延迟双删或订阅 binlog 异步失效。缺点要主动讲:删缓存失败与主从延迟造成短期不一致;多级缓存让排查变复杂;Redis 成为强依赖,挂了会打穿数据库,需要限流、熔断与本地兜底;无界缓存与不当淘汰会吃内存,要监控命中率、大 key 与内存水位。

B 树与 B+ 树、Kafka 高水位线。B 树每个节点既存键也存数据,查找可能在中间节点命中,但单节点容纳的键更少、树更高、路径长度不一致,范围查询要在层间跳跃;B+ 树把数据全放叶子层、非叶子层只存索引键,扇出更大、树更矮(三到四层撑起千万级数据),查询路径等长、性能稳定,叶子用双向链表串联让范围扫描变成顺序遍历。B+ 树的缺点要如实说:等值查询可能多走一层到叶子;非叶子冗余存键使索引略大;分裂合并逻辑更复杂;非覆盖索引查询仍要回表。Kafka 的高水位线(HW)是「已被 ISR 中所有副本复制成功、因而可被消费者读取」的偏移上界:Leader 维护 LEO(下一条写入位置),HW 取 ISR 中各副本 LEO 的最小值,消费者只能读到 HW 之前,这保证读到的消息不会因 Leader 切换而丢失,代价是消费者进度滞后。副本落后超过 replica.lag.time.max.ms 会被移出 ISR,HW 由剩余副本决定;Leader Epoch 解决重启后按 HW 截断导致的不一致,unclean.leader.election.enable 则决定是否让非 ISR 副本当选(用一致性换可用性,会丢消息)。

Agent 项目的记忆、上下文压缩与 LangGraph 编排。记忆通常分三层:短期记忆是当前会话历史,按 token 预算保留原文并做摘要;长期记忆是跨会话的用户画像与事实,抽取成结构化条目存库或向量库,带来源与时间戳以便更新与溯源。检索时机是关键,不能每轮全量召回,而靠意图或实体触发(命中相关场景或工具即将调用时才查对应记忆),召回后按相关度与时效重排,并标注记忆来自何时,避免把过期偏好当事实。上下文压缩的做法有滑动窗口丢弃最旧消息、分层摘要(留结论与决策,丢寒暄与冗余工具输出)、把大块工具返回裁成结构化字段、用向量检索按需回溯原文;压缩必须保留不可丢项——用户明确约束、任务目标、已完成与失败的动作,否则模型会重复劳动或违背要求。LangGraph 是显式状态图,节点是函数、边是流转条件,所以流程既有线性部分(预定义处理链)也有动态部分(条件边、循环与路由,由模型或规则决定下一步),是否动态取决于你把决策放在边还是节点内部;工程上要注意状态用 reducer 合并、checkpoint 与业务库的单一真相源(业务库为准,checkpoint 只存控制流)、以及给循环设步数与超时上限防止跑飞。