面灵AI→

BIGO 客户端秋招一二三面:JVM 线程池、项目深挖与算法手撕

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

《面试题目》

一面

  1. 请做自我介绍:教育背景、C++ 自学经历、个人项目、快手客户端实习和博客。
  2. 实习项目深挖:K 歌数字人 / MV 生成、音频拼接、AEC、异步任务与轮询分别是怎么做的?
  3. 技术栈确认:C++ 是怎么学的?Java/Android 实习做了什么?团队里 Android 和 iOS 怎么分工?
  4. JVM 的运行时内存区域有哪些?StackOverflowError 和 OutOfMemoryError 分别在什么情况下出现?
  5. HashMap 和 TreeMap 有什么区别?各自的时间复杂度、哈希冲突怎么处理、负载因子为什么是 0.75、什么场景该选哪个?
  6. 进程和线程有什么区别?多线程共享一个计数器会出什么问题?atomic 和 CAS 是怎么保证原子性的?ABA 问题怎么解决?
  7. 线程池的工作流程是怎样的?线程数怎么定?1000 个网络请求该怎么配?I/O 密集型和 CPU 密集型有什么区别?
  8. 一次网络请求的完整链路是怎样的?HTTPS/TLS 握手做了什么、证书和密钥分别起什么作用、协议怎么分层、DNS 是怎么解析的?
  9. Android 列表快速滚动时头像错位是怎么产生的?View 的复用和缓存机制是怎样的?
  10. AI Coding 你是怎么用的:PRD 拆分、Plan/Design、代码生成、Review、测试、Skill、Harness Engineering 各扮演什么角色?
  11. 手撕:判断单链表是否有环,要求 O(1) 额外空间;再求环的长度。
  12. 手撕:合并已经按起点有序的区间,样例 [[1,3],[2,6],[8,10],[15,18]] 输出 [[1,6],[8,10],[15,18]]。
  13. 反问:部门业务方向是什么?音视频产品的难点在哪?新人会遇到什么挑战?面试官对本次面试有什么反馈?

二面

  1. 请做自我介绍。
  2. 深挖快手项目:数字人 MV、分段录音、上传、AEC、异常状态、断点续传是怎么做的?IM 消息排序与卡片展示呢?
  3. RAG 项目:切片、向量化、多路召回、重排、置信度、流式输出分别怎么做的?你个人负责哪一部分?
  4. 经历与方向确认:你更熟悉哪套技术栈?实习时长多久?有音频相关的经验吗?
  5. 如果让你设计一个线程池,要考虑哪些组成部分?固定线程数和动态线程数怎么选?任务积压和资源上限怎么控制?
  6. C++ 里内存泄漏怎么排查?shared_ptr 和 weak_ptr 有什么区别?循环引用怎么解决?
  7. 消息队列由哪些部分组成?Producer、Broker、交换机、队列、Consumer、ACK 和持久化分别起什么作用?
  8. 有依赖的任务怎么编排?哪些该串行、哪些该并行?怎么保证线程安全、怎么检测循环依赖?(追问:录音/转录中段不完整怎么处理?)
  9. 互斥锁、读写锁、自旋锁、递归锁、条件变量分别适合什么场景?
  10. Activity 和 Fragment 的生命周期是怎样的?MVP、MVVM 这些设计模式有什么区别?
  11. 你怎么看 AI 辅助开发和 Vibe Coding?测试流程怎么走?双端、成熟产品与孵化项目、AI 协作平台你会怎么选?

三面

  1. 背景与语言:你的专业是什么?怎么自学 C++、C++ 熟悉到什么程度?快手 Android 实习里用过 Java/Kotlin 吗?
  2. 数字人 MV 的异步链路是怎么设计的?客户端、服务端和算法各负责什么?轮询间隔、异常/取消、任务状态怎么处理?
  3. 为什么需要状态机?多个订单之间怎么隔离?加锁和每个订单一个独立实例有什么区别?
  4. RAG 知识库怎么搭:数据源有哪些?PDF 怎么入库?嵌入怎么做?检索怎么融合?LLM 输出怎么控制?
  5. 你的职业方向是客户端、后端还是全栈?长期想做什么角色?
  6. 手撕:n 位员工各有去北京和广州的费用,必须一半去北京、一半去广州,求最低总费用。
  7. 聊聊你对 AI 的看法。

《参考解析》

一面:Java 与 Android 八股的主线和卡点

JVM 运行时内存分五块:堆、方法区(JDK 8 之后是元空间,移到本地内存)、虚拟机栈、本地方法栈、程序计数器。StackOverflowError 是栈帧太深——递归没有收敛路径,或者单个栈帧太大;OOM 多数出在堆(对象太多、或者真的泄漏),也可能出在元空间(动态生成类过多)、直接内存(NIO/Netty)以及线程数过多(每个线程都要占一份栈,线程创建失败也是 OOM)。答这题把「哪个区域 → 什么条件触发 → 怎么定位(jstat 看 GC、jmap/MAT 找支配树)」串成一条线,比背区域名有用。

HashMap 是数组 + 链表 + 红黑树,平均 O(1):链表长度到 8 且容量足够时转红黑树,退回阈值 6,负载因子 0.75 是「空间浪费」与「冲突概率」的折中,扩容翻倍并重新散列。TreeMap 是红黑树,O(log n),要 Comparable 或 Comparator,优势是有序遍历和范围查询(subMap、ceilingKey),选型就看要不要顺序。并发场景下 HashMap 会丢更新甚至死循环,得换 ConcurrentHashMap;既要并发又要有序则是 ConcurrentSkipListMap。

进程是资源分配单位、线程是调度单位,线程共享地址空间、各有独立栈和寄存器。i++ 是读—改—写三步,多线程下会丢更新,靠锁或 AtomicInteger 解决;atomic 的底座是 CAS(比较并交换,落到 CPU 的 cmpxchg)加 volatile 的可见性,失败就自旋重试,高竞争下空转很费 CPU,所以还有 LongAdder 这类把热点拆开的做法。ABA 是值从 A 变成 B 又变回 A,CAS 只看值察觉不到,用版本号(AtomicStampedReference)或干脆换成不可变结构。

线程池的流程是:核心线程没满就开核心线程 → 进队列 → 队列满再开非核心线程 → 到最大线程数就走拒绝策略。1000 个网络请求是 I/O 密集型(线程大部分时间在等),正确答案不是把线程开到 1000,而是有界队列 + 合理的最大线程数 + 下游连接池上限 + 超时,再监控队列长度与拒绝次数,超了就降级。CPU 密集型经验值是核数 + 1,I/O 密集型可以用「核数 × (1 + 等待时间/计算时间)」估算,最终都以压测为准;别用 Executors 的默认工厂(无界队列会堆到 OOM),手写 ThreadPoolExecutor 把每个参数讲明白。

HTTPS 里非对称加密只用于握手,数据走对称加密。顺序是 ClientHello(版本、密码套件、随机数)→ ServerHello + 证书链 → 客户端用内置 CA 公钥验签、校验证书链、域名与有效期 → ECDHE 协商出会话密钥 → Finished,之后对称加密传数据。证书里装的是服务端公钥、域名和 CA 签名,私钥永远不出服务端。前面还接着 DNS 解析(浏览器/系统/hosts 缓存 → 递归解析器 → 根 → 顶级域 → 权威)和 TCP 三次握手,答完补一句 HTTP/2 多路复用与 CDN 的影响,链路才算完整。

列表头像错位的根因是 ViewHolder 复用叠加图片异步加载:图片回来时这个 item 已经滚出去被复用成别人,回调却还往同一个 ImageView 上贴。解法是让「请求」和「当前绑定的数据」对得上——加载前先放占位图,给 ImageView 打上数据标识(tag 或 url),回调里比对不一致就丢弃;更省事的是直接用 Glide/Coil 这类自带 tag 绑定和生命周期取消的库,再配 DiffUtil 与稳定 id 减少无谓重绑。AI Coding 那题面试官想听的是「你负责验收」:需求拆到一次能验证的大小,先让模型出 plan/design 再生成代码,自己重点 review 边界与安全,补测试跑通才算完,重复出现的流程固化成 skill。

二面:项目深挖与并发设计的答法

项目深挖的追问路径基本固定:为什么选这个方案、原理是什么、落地效果如何、你具体做了哪部分。每个技术选择都准备三样东西——当时的约束(时延、成本、数据量)、对比过的备选、最后取舍掉的代价。数字人 MV 那条链路要讲清客户端做了什么(录音、拼接、上传、状态展示)、服务端和算法做了什么(合成与推理)、异常与断点续传怎么兜底;作者两轮都被要求划清「个人负责边界」,说明这轮最忌讳夸大贡献,把接口和数据流讲清楚比覆盖面大更稳。IM 消息排序与卡片展示那类问题,落点是排序键怎么定、乱序与重复怎么处理。

线程池设计题先列组成:任务队列、工作线程集合、线程工厂、拒绝策略、状态与统计。固定 vs 动态的取舍是——固定便于容量规划与压测,动态(核心 0 + 缓存线程)能扛突发但资源上限难预估。积压要靠有界队列暴露出来,满了走拒绝或降级,而不是无限堆积;上限同时受内存(每线程栈约 1MB)和下游承受能力约束,线程再多万一全打到数据库也没用,所以限流要放在下游之前。C++ 智能指针部分,shared_ptr 用引用计数决定释放时机,两个对象互持 shared_ptr 就形成循环引用、计数永不归零,weak_ptr 不增加计数、用 lock() 临时提升为 shared_ptr 来判断对象是否还活着,打破循环只需一方改持 weak_ptr;补一句「计数操作是原子的,但指向的对象本身不因此线程安全」会很加分。

消息队列的基本盘是 Producer → Exchange 按 binding 与 routing key 路由 → Queue → Consumer 拉取,ACK 之后才删除,持久化要交换机、队列、消息三处都声明,再配手动 ACK 与幂等消费,才能两头不丢不重。任务编排本质是 DAG:先拓扑排序检测循环依赖,就绪节点交给线程池并发跑、有依赖的串行,共享状态要么加锁要么收敛到单点,部分失败要能重试且重试必须幂等——录音或转录中段不完整这类问题,靠分片加偏移续传,而不是整段重来。锁的选型是:互斥锁最通用,读写锁适合读多写少,自旋锁适合临界区极短、不想付线程切换代价的场景,递归锁允许同线程重入但出现它往往说明设计有问题,条件变量是配互斥锁做等待与通知(生产者—消费者)。Java 里对应 synchronized、ReentrantLock、ReentrantReadWriteLock、StampedLock,收尾落一句「先想能不能消除共享,不能才加锁」比罗列 API 有说服力。

Activity/Fragment 生命周期要讲清可见性与可交互性的差异(onCreate/onStart/onResume 到 onPause/onStop/onDestroy),以及配置变更重建、Fragment 与宿主绑定、状态保存。MVP 把 View 抽象成接口由 Presenter 驱动,MVVM 用可观察数据(DataBinding、LiveData、StateFlow)让 View 自动响应变化,解耦更彻底、更容易测,代价是数据流更绕、调试更麻烦。二面全程没有手撕,问题集中在项目与并发设计上,说明这一轮的取舍是「项目真实度」优先。

三面:状态机、RAG 链路与差旅费用贪心

状态机的价值是把散落的 if-else 收敛成「有限状态 + 明确迁移」,让异常、取消、重试都有定义,任务走到哪一步是显式可查的,而不是靠一堆布尔标志位拼。多订单隔离有两条路:每个订单一个独立的状态机实例(状态随实例走,天然无共享),或者共享一套状态机、把状态和订单 id 一起落库或落 Redis。加锁与独立实例的区别正在这里——加锁是在共享可变状态上做互斥,独立实例是直接消除共享,后者没有锁竞争,能选就该选它。

RAG 的链路是数据源 → 解析清洗(PDF 要处理页眉页脚、分栏、表格,并保留页码便于溯源)→ 按结构切片并留重叠 → 嵌入入库 → 多路召回(向量 + 关键词)→ 融合(如 RRF)→ 重排 → 置信度不够就兜底或澄清 → 拼 prompt 流式输出。最容易被追问的是「切片大小怎么定」和「你负责哪一段」:切片没有普适最优值,要拿自己的评测集调,还要看 embedding 模型的输入上限;个人边界照实说,讲清接口与数据流比虚报覆盖面稳得多。

差旅费用这题的标准贪心是:以「全员去广州」为基准,总费用是所有人广州费用之和;第 i 个人改去北京的增量是 B_i - G_i,要恰好一半人去北京,就挑增量最小的 n/2 个人,把他们的增量加到总和上。正确性用交换论证——如果最优解没取最小的 n/2 个增量,把其中一个换成更小的那个总费用只会更低,矛盾。复杂度 O(n log n)(瓶颈在排序,用快选能到 O(n)),空间 O(n)。写之前先确认 n 是偶数、费用能否为负、增量相等时怎么取,这几点问清楚比抢着写代码得分高。

手撕与流程复盘

单链表判环用快慢指针:慢指针一次一步、快指针一次两步,有环必相遇,额外空间 O(1)。追问环长时,从相遇点继续绕行计数、回到原点即得环长;再进一步求入口,是「头节点与相遇点同速前进的相遇点」,这三问常常连着出。合并有序区间这题因为已经保证按起点有序,省掉排序,一趟线性扫描即可:维护当前区间 cur,遇到 next.start <= cur.end 就 cur.end = max(cur.end, next.end),否则把 cur 收进答案并以 next 开新区间,时间 O(n)、额外空间 O(1)。边界是空数组、单区间、包含关系,以及端点相接算不算重叠,动手前先和面试官对齐。

流程上有两点值得注意:三面进行中出现了设备卡顿和通话中断,恢复后面试官直接让继续,遇到这种事故别反复道歉,把思路接着讲完就行;整场节奏是一面铺基础、二面挖项目、三面验证设计与编码,均衡准备比押注某一轮更划算。