去哪儿 AI 面试:AI 应用开发(Java 方向)
- 轮次
- AI面试
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 请做一下自我介绍。
- Java 的类加载器机制和双亲委派模型是什么?
- 追问:如果自定义了一个 String 类,并且破坏了双亲委派模型,那么会发生什么?
- volatile 有什么作用,怎么实现这些作用?
- 追问:volatile 怎么保证有序性?内存屏障是什么,为什么写操作没有把所有种类的内存屏障都加进去?
- MySQL 的 B+ 树比 B 树好在哪里?
- 追问:B+ 树和 B 树的 IO 效率怎么比较?B+ 树叶子节点的链表具体是怎么减少 IO 次数的?
- TCP 三次握手的过程是什么,为什么不能是两次?
- 追问:握手过程中,如果客户端的 SYN 长时间滞留直到连接关闭才消失,会有什么问题?
- 设计一个一秒一万次的抽奖系统,库存有限。
- 追问:针对权限校验、库存校验模块怎么设计?用 Redis 加 Lua 保证原子性是怎么实现的,Lua 脚本具体怎么设计?
- 介绍一下简历上的项目。
- 最有挑战性的后端项目以及解决方案是什么?
- AI 落地的实际案例。
《参考解析》
双亲委派模型,以及「自己写一个 String」会发生什么
类加载器分四层:Bootstrap 加载核心类库,Platform 加载扩展模块,Application 加载 classpath,再往下是自定义加载器。加载一个类时先委派给父加载器,父加载器加载不了才轮到自己,目的是保证同一个类不会被不同加载器重复定义,尤其保证 java.lang 这类核心类始终由 Bootstrap 加载——否则任何人都能塞一个同名的 String 冒充。它也确实会被破坏,且都是有意为之:SPI 场景(JDBC 驱动在 classpath 里、接口在核心库里)需要线程上下文类加载器反向委派;Tomcat 为隔离各个 web 应用,让 WebappClassLoader 先自己加载再委派父级;OSGi 则是网状委派。回到追问本身:真去自定义 java.lang.String 并绕过委派,JVM 在加载阶段就有包级保护——以 java. 开头的类只允许 Bootstrap 加载,其他加载器会直接抛 Prohibited package name 的 SecurityException;即便用启动类路径强行塞进去,核心类已经被加载过、同名类在同一个加载器命名空间里不会再定义一次,两个不同加载器加载的同名类也会互不兼容、赋值时抛类型转换异常。
volatile 的语义与内存屏障为什么这样加
volatile 给两个保证、不给一个:可见性(写立即对其他线程可见,实现上是写操作后强制把 store buffer 刷出去、并让其他核心的缓存副本失效)与有序性(编译器和处理器都不能把它与前后指令随意重排),但不保证原子性——i++ 这类读改写仍需 synchronized 或原子类。屏障的加法是有取舍的:volatile 写前面加 StoreStore、后面加 StoreLoad;volatile 读后面加 LoadLoad 与 LoadStore。之所以不给写操作把所有种类的屏障都加上,是因为多数屏障在 x86 这类强内存模型上本来就「免费」——硬件本身不允许 StoreStore、LoadLoad 重排,加了只是阻止编译器优化、白付代价;真正昂贵的是 StoreLoad(需要 mfence 或带锁前缀的指令,几十上百个周期),而它恰好是跨架构语义所必需的那一个,所以只在 volatile 写之后加。这也是 JMM 的设计取向:用最小的屏障集合覆盖所有架构的正确性,把性能留给硬件已经保证的部分。
B+ 树比 B 树强在哪
B 树的每个节点既存键也存数据,B+ 树的内部节点只存键、数据全在叶子,且叶子之间用双向链表串起来。好处有三:内部节点更小,同样 16KB 的页能放更多键,扇出更大、树高更低,一次点查的页 IO 次数更少(三五层就能覆盖千万级数据);叶子有序链表让范围查询、排序、分页只需定位到第一个叶子再顺序扫描,不必像 B 树那样在层间来回中序回溯;全量扫描也更友好。至于链表为什么能减少 IO:范围查询命中第一个叶子后沿后继指针顺序读页,页的物理分布相近,InnoDB 还会做线性预读与随机预读,把后续页提前拉进 buffer pool,于是后续访问基本是内存操作;而 B 树的中序遍历每次跨层都要重新定位页面、命中率低,页 IO 次数随范围长度线性增长。B+ 树的代价是键在内部节点冗余一份、空间略多,单点查询必须走到叶子——但因为 B 树非叶节点能放的键更少、树更高,整体 IO 并不占优。
三次握手为什么不能是两次,SYN 滞留又会怎样
三次握手是:客户端发 SYN 与自己的初始序列号;服务端回 SYN+ACK,确认对方序列号并给出自己的;客户端再回 ACK。它的作用是双向确认可达并交换、确认双方的初始序列号,两次只能让一端确认往返、且服务端在收到第二个包后就无法确认客户端是否收到了自己的 SYN。如果只握手两次就建连,服务端会为大量伪造源地址的 SYN 分配资源,形成半开连接堆积(SYN flood),所以第三次的意义就是让服务端在分配完整资源前确认「你确实收到了我的确认、你确实在线」。关于 SYN 长时间滞留:客户端重传 SYN 直到放弃、或该 SYN 在网络里延迟很久才到达,服务端收到后会当成新连接建立 SYN_RECV 条目并回 SYN-ACK;若这类滞留或重传的 SYN 持续到达,服务端的半开队列会被无效连接占满、accept 队列涨、内存与端口被消耗,正常的连接反而被丢弃;更坏的情况是同一四元组被复用,旧 SYN 建立的连接与后续真实连接语义混淆。防护手段包括开启 syncookies(不预先分配状态,把序列号编码进返回的 SYN-ACK)、调小 SYN-ACK 重试次数、增大 SYN 队列并配合应用尽快 accept。
一秒一万次的抽奖系统怎么设计
按「挡在越前面越便宜」分层。接入层限流削峰(令牌桶),超出部分直接排队或拒绝,别让流量直接打到底层。库存预热到 Redis,扣减用 Lua 脚本在服务端原子完成「查库存、判断、扣减、记参与」四步:Redis 单线程执行命令,EVAL 会把整个脚本当作一个命令跑完,期间不穿插其他客户端请求,所以脚本内的检查与扣减不会被并发打断;注意它不做回滚,脚本要短、只做 O(1) 操作,否则会阻塞所有请求。热点 key 是主要风险,一万 QPS 打同一个 key 会给单分片造成压力,解法是把库存分成若干桶(比如 1000 个库存拆成 20 个桶),按用户哈希路由,桶内扣减,用总量对账。库存校验与权限校验分开:资格类判断(活动时间、用户等级、实名)放在业务层或网关,用本地缓存加布隆过滤器挡掉绝大部分无效请求;幂等与配额放 Redis,用集合的原子添加结果决定是否放行。扣减成功后发消息队列异步落库,数据库用唯一索引做最终幂等并定期对账,避免超卖与少卖;Redis 不可用时要有明确降级——要么直接暂停活动、要么切到数据库严格扣减,绝不能静默放行。
AI 落地案例怎么讲
面试官想听的是「你解决了什么业务问题、为什么非要用模型、效果怎么衡量、成本多少」,而不是模型名与技术栈罗列。可以按这个顺序讲:业务背景与原来的做法(基线是什么)、为什么规则或传统方案不行、方案怎么选(模型选型、提示词、检索增强、工具链,以及有没有评测集)、效果指标(准确率或人工替代率、端到端耗时、单次调用成本,最好有线上灰度或 A/B 数据)、最后是踩过的坑与兜底(幻觉、脏数据、评测缺失、成本失控,以及人工复核、置信度阈值、灰度回滚这些手段)。追问通常集中在「怎么保证输出可靠」「为什么不直接用规则」「成本能不能再降」「怎么证明效果」这几处,提前把基线数字与失败案例准备好,比讲模型细节更得分。