面灵AI→

腾讯后端开发岗面经合集:带锁 Map 与 AI 协同开发

轮次
多轮面试合集
时间
2026-09
来源
牛客网

《面试题目》

  1. (面经 01)介绍项目的业务场景
  2. (面经 01)常见的排序算法有哪些?
  3. (面经 01)快速排序的原理、实现方法和复杂度是什么?
  4. (面经 01)请介绍 TCP 三次握手和四次挥手
  5. (面经 01)算法:实现 LRU,并自行构造输入输出
  6. (面经 02)HashMap 的底层原理是什么,如何保证线程安全?
  7. (面经 02)算法:使用两个队列实现一个栈
  8. (面经 02)堆排序的原理是什么?
  9. (面经 02)排序算法的稳定性是什么意思?
  10. (面经 02)开发中如何与 AI 协作?
  11. (面经 02)是否阅读过 Skill 的具体内容?
  12. (面经 02)实习中遇到过什么难题,如何保障系统可靠性?
  13. (面经 03)在大型项目中,AI 根据文档生成代码后,如何验收代码是否符合需求且功能正常?
  14. (面经 03)既然代码由 AI 生成,为什么仍需要人工测试?
  15. (面经 03)主链路验证通过后,如何确认代码可以安全合入线上?
  16. (面经 03)从需求分析、方案设计到代码实现,是否有标准的 AI 协同开发模板、框架或工具?
  17. (面经 03)没有标准 AI 协同开发流程时,如何保证开发质量和工程效果?
  18. (面经 03)了解哪些成熟的 AI 协同开发实践?
  19. (面经 03)拿到 AI 输出后,如何判断内容是否正确?
  20. (面经 03)请介绍对 Go 的掌握程度
  21. (面经 04)系统设计:实现一个带锁的 Map,说明核心数据结构
  22. (面经 04)带锁 Map 的 Read Map 和 Dirty Map 如何设计,各自承担什么职责?
  23. (面经 04)读取 Read Map 的同时并发写入 Dirty Map 应如何处理?
  24. (面经 04)在 CP 或 AP 取舍下,读写锁应采用什么粒度?
  25. (面经 04)如何降低多锁设计带来的开销?
  26. (面经 04)该设计与 Redis Cluster、MIT 6.824 中相关方案相比有什么优缺点?
  27. (面经 04)系统设计:单线程接收十万个计算型请求,如何设计任务管理和执行函数?
  28. (面经 04)该计算任务如何使用 MapReduce 思路处理?
  29. (面经 04)任务调度应使用什么数据结构,为什么使用队列而不是 Map?
  30. (面经 04)十万个请求同时到达时如何处理?
  31. (面经 04)应创建多少个 Goroutine,每个任务传递哪些参数?
  32. (面经 04)执行具体任务前是否需要加锁?
  33. (面经 04)算法:一个文件包含 20 亿个 32 位整数……(原帖在此截断)

《参考解析》

1. HashMap 的底层原理与线程安全。JDK 1.8 的 HashMap 是「数组 + 链表 + 红黑树」:用 (n − 1) & hash 定位桶,hash 先经过扰动(h ^ (h >>> 16))让高位参与运算,减少冲突;链表长度达到 8 且数组长度不小于 64 时树化(否则先扩容),退化阈值是 6;默认负载因子 0.75,扩容是 2 倍,1.8 改用尾插(1.7 是头插)。并发问题要说清版本差异:1.7 头插在多线程扩容时会形成环导致死循环、CPU 打满;1.8 改尾插后不会死循环了,但仍会丢数据(两个线程同时 put 到同一个空桶,后写的覆盖先写的),size 计数也不准。所以并发场景必须用 ConcurrentHashMap:1.8 放弃了分段锁,改成「CAS 插入空桶 + synchronized 锁桶头节点」,size 用 baseCount 加 CounterCell[] 分散热点(类似 LongAdder);它的读操作不加锁(Node 的 val 和 next 都是 volatile),并用 volatile 的 tabAt 保证桶数组可见性。追问一般会落到「为什么不允许 null 键值」——并发下 get 返回 null 无法区分「键不存在」和「值就是 null」,所以干脆禁用。

2. 排序算法与稳定性。快排是选基准分区、递归处理两侧:平均 O(n log n)、最坏 O(n²)(已排序数组加固定取首元素)、原地、不稳定;工程实现要随机化或三数取中选 pivot,小区间切插入排序,并用尾递归优化控制栈深。堆排序建堆是 O(n),每次取堆顶后 O(log n) 调整,总 O(n log n)、原地、不稳定、常数比快排大,但对最坏情况有保证——这也是 introsort 在快排递归过深时切堆排的原因。稳定性的定义是「相等的元素排序后相对次序不变」:冒泡、插入、归并、计数、基数稳定,选择、快排、堆排不稳定。为什么工程上在意:多字段排序要能「先按 B 排、再按 A 排」并保留 B 的顺序,Java 的 Collections.sort 用 TimSort 就是为了稳定。再补一句复杂度边界:比较排序的下界是 Ω(n log n),要突破必须用非比较排序(计数、基数、桶),代价是对数据范围或位数有约束。

3. 带锁 Map 的 Read Map 与 Dirty Map。这是 sync.Map 的思路,适合「读多写少、key 基本不变」的场景。结构上是两个 map:read 是只读的,用 atomic.Pointer[readOnly] 存,里面有 m map[any]*entry 和 amended bool 标记;dirty 是普通 map,由 mutex 保护。读路径先查 read,未命中且 amended 为 true 时加锁查 dirty 并对 misses 计数;misses 累加到 dirty 的长度时,把 dirty 整体提升为 read、dirty 置空——这是为了不让「read 未命中就查 dirty」变成常态。写路径:key 已存在于 read 时只原子地改 entry 指针,不必加锁;不在 read 里才加锁写 dirty,并把 read 中对应的 entry 标成 expunged(表示这个 key 在 dirty 提升时不需要带过来)。entry 用指针包一层,是为了把「删除」变成一次原子的指针置 nil,而不需要真的删 map。取舍要讲透:它偏向读性能(读几乎无锁),代价是写和删除更贵、read 与 dirty 并存时内存有冗余;读写比不高时直接用 RWMutex + map 更省内存、语义也更简单;另一个常见折中是分片锁——把 key 哈希到 N 个「Mutex + map」上,既降低锁竞争又保留普通 map 的语义。和 Redis Cluster、MIT 6.824 的方案对比时,抓住「都是为了降低争用,区别在一致性级别和开销来源」这个主线:分片是把争用按 key 拆开,共识协议换的是强一致。

4. 十万计算型请求的任务管理。单线程接收加异步计算的标准解法是「有界队列 + worker 池 + 背压」。①接收线程只做反序列化和入队,队列用环形缓冲。为什么用队列不用 Map:任务有到达顺序和公平性要求,FIFO 天然表达「先到先服务」,而 map 按键寻址、遍历无序,要额外维护序号才能表达顺序;只有需要「按 id 取消某个任务」「按请求去重」「按 id 查状态」时才用 map 作为辅助索引。②队列必须有界,满了要有明确策略:阻塞接收(自然背压,但会顶到调用方)、返回 429 让上游重试、或按优先级丢弃低优任务。③worker 数量按任务类型定:CPU 密集取 GOMAXPROCS(核数),IO 密集可以更多;每个任务只传必要参数,传切片时注意共享底层数组的坑,要么传副本要么明确约定只读。④「执行前要不要加锁」——如果每个任务只操作自己的数据就不需要;要更新共享计数器或统计时才用 atomic 或分片锁,绝不能在整个任务执行期间持锁,那等于把并行变串行。⑤MapReduce 化:把十万个请求按 key 或按输入分片切成 map 阶段并行处理,每个 worker 产出局部结果,再用 reduce 阶段(或分层归并、sync.WaitGroup 加结果 channel)汇总,好处是分片内部无锁、只在归并时同步一次。⑥别忘了超时与取消(context)、失败重试与死信队列,以及用 pprof 确认瓶颈在调度还是在计算。

5. AI 生成代码的验收与安全合入。这题要答成一套可执行流程,而不是表态。①生成前:把需求写成可验证的规格——接口签名、输入输出、边界条件、错误码,并给足上下文(相关文件、现有约定、已有测试),这样「验收标准」在写代码之前就定好了。②生成后:先看 diff(改动要小,不接受顺手重构无关文件),再跑单测与集成测试;对关键逻辑要自己补边界用例(空值、超长输入、并发、幂等)。③为什么仍要人工测试:AI 输出是概率性的,会出现「看起来对的错」——幻觉 API、漏掉错误分支、把测试写成永远通过;它也不知道线上流量形态和历史包袱,测试与 review 是把「生成」变成「工程」的那道关。④安全合入的判断标准是「可灰度、可回滚、有监控、可对账」:用配置开关或流量比例灰度,准备好回滚路径,把错误率、延迟、日志关键字纳入告警,评估数据变更(Schema 改动)是否可逆。⑤没有标准协同流程时,就固化三条原则:改动小、可回滚、有测试覆盖。⑥判断 AI 输出对不对的通用手法:要它给出结论依据(引用哪个文件、文档或测试结果),数字和 API 一律以本地实测为准,跨模型的第二意见用来发现盲点而不是当成结论。

6. LRU 与两个队列实现栈。LRU 要 O(1) 的 get/put,结构就是「哈希表 + 双向链表」:哈希表存 key 到链表节点,链表头部是最近使用、尾部是最久未用;get 命中后把节点移到头部,put 时 key 存在则更新并移到头部,不存在则插到头部并在超容量时删掉尾节点。Go 里可以用 container/list 或手写带两个哨兵节点的链表,哨兵能省掉大量边界判断。追问方向有:并发安全(按 key 分片加锁,或直接用带锁 LRU)、按过期时间淘汰(用最小堆或时间轮,和 LRU 组合)、以及 LRU 的固有缺陷——一次全表扫描会冲垮缓存,这时要用 LRU-K 或 LFU(Redis 4.0 的 LFU 用计数加时间衰减),而 Redis 的近似 LRU 是采样淘汰,因为精确 LRU 要维护额外指针、内存开销大。用两个队列实现栈:push 时入 q1;pop 时把 q1 里前 n−1 个元素移到 q2,剩下那个出队即栈顶,然后交换两个队列的角色——push O(1)、pop O(n)。反向题(两个栈实现队列)是「入栈 O(1)、出栈摊还 O(1)」:输入栈倒到输出栈,摊还分析要能说出来。