Skip to content
掌握度

题面整理 17 题:实习与 AI 项目(1–7)+ Java 基础八股(8–17)。参考答案基于项目材料与八股库准备;有题库托底的标注链接。

一面 ​

1. Canal 实现原理 (题库 Q2 已备) ​

伪装成 MySQL 的 slave,走 dump 协议(带上文件名 + 位点)让主库持续推 Binlog 事件流;解析成结构化变更对象(库名、表名、事件类型、字段列表、前后镜像)投递 MQ。不丢靠位点管理(记录 filename + position,重启续传),HA 靠 ZooKeeper 抢占锁单实例消费。完整口径见 Binlog2Hive链路设计 Q2。

2. 如何做到不丢不重的 (题库 Q3/Q4/Q5 已备) ​

口径一句话:at-least-once 投递 + 下游主键幂等 = 业务上不丢不重。锚点管不丢(增量从 wholeLoadBatchTime 位点之后消费),幂等管不重(Spark 按主键保留位点最大末态);重复的三类来源(MQ 重放、任务重跑、全量重叠窗口)分别对应三种修复手段。完整口径见 Binlog2Hive链路设计 Q3–Q5、Q7。

3. Kafka 中的 position 支持多个消费者吗 新题 ​

参考答案:分两种情况,核心是位点按消费者组隔离。

  • 跨消费者组:支持,互不影响。committed offset 按「组 + topic + 分区」维度存放在 __consumer_offsets 里,两个组各自维护自己的位点、各自独立消费同一批数据——这就是发布订阅语义,也是 Binlog 多订阅的基础(实时数仓、索引构建各是一个组)。
  • 同一消费者组内:一个分区同一时刻只分配给组内一个消费者(队列语义),组内消费者分摊分区,不是都从头读。
  • 概念上区分两个位点:position 是消费者客户端内存里「下一条要拉取的位置」;committed offset 是定期提交到 broker 的位点——rebalance 或重启后从 committed 恢复,两者之间的差就是「已拉取未提交」的窗口,也是重复消费的来源。

4. 有做多 Agent 吗 (未现场考过,材料已备) ​

参考答案:

有,/team 模式。AgentOrchestrator 固定三个角色:planner 拆计划、worker 池按依赖执行、reviewer 审查质量——reviewer 不通过会带着 issues 打回重试,上限两次。和 Plan-and-Execute 的本质区别在节点粒度:Plan 的节点是「任务」,Multi-Agent 的节点是「子 Agent」,每个子 Agent 有自己的上下文和职责。三个模式共享同一套 ToolRegistry、Memory 和快照能力,不为某个模式造孤立功能。选择逻辑:单步任务 ReAct,可枚举的多步任务 Plan,需要角色分工和质量审查的上 Multi-Agent。

5. 工具调用是怎么实现的 (题库 Q4 已备) ​

模型不执行任何东西——它产出「调哪个工具、带什么参数」的结构化意图(按 JSON Schema 约束的 tool_call),应用侧校验后执行真正的函数,结果作为 tool_result 回注,模型再决策下一步,这个循环就是 ReAct 骨架。工具选择靠描述即路由,MCP 工具运行时发现后注册进同一张表。完整口径见 CodeAgent-专项题库 第 4 题。

6. 「如果库里有非常多的工具,如何选择、避免幻觉?」 新题 ​

参考答案(分层降选择空间):

工具一多,prompt 膨胀加上候选空间变大,幻觉(选不存在的工具、编参数)必然上升。我的方案分四层:

  1. 分组两级路由:先给模型一个工具组目录(每组一句话描述),模型先选组、再在组内选工具——两跳把每次的选择空间从 N 降到「组数 + 组内数」;
  2. RAG 式工具召回:把工具描述向量化,按当前查询语义召回 top-k 相关工具注入 prompt——工具量大时的标准做法,等价于「动态组装工具清单」;
  3. schema 硬校验:结构化输出约束加应用侧校验,选了不存在的工具名直接返回错误列表让模型重选,编造的参数打回修正——幻觉最多打回一轮,不会执行出去;
  4. 描述纪律加评测:每个描述写清「做什么、什么时候用、什么时候不用」,避免语义重叠;用黄金集定期测工具选择准确率。

结合我的项目:当前 16 个工具在单 prompt 内是可控规模,靠描述纪律就够;这套四层方案是我明确知道该往哪长的扩展路径。

7. Skill 实现原理 新题 ​

参考答案:

Skill 是「按需注入的领域知识包」。机制三步:索引先行——每个 Skill 的名字和一句话描述放进 system prompt 的索引里,成本极低;按需加载——模型判断当前任务匹配某个 Skill 时,调 load_skill 工具把正文(提示词流程、脚本、资源)加载进上下文,之后按内容执行;一次加载——同一会话同一 Skill 通常只加载一次,正文留在上下文里持续生效,避免每轮重复注入。注册表统一管理 Skill 的发现与加载。价值是把「要不要带某领域知识」的决定权交给模型,按需付 token 成本,而不是把所有领域知识无差别塞进 system prompt——和工具调用的「描述即路由」是同一个设计思想在知识层面的复用。

8. 封装、继承、多态 八股 ​

参考答案(带项目例子是加分项):

封装:隐藏内部状态,只通过方法暴露行为——比如我的 ToolRegistry 内部的工具 Map 是私有的,对外只暴露 executeTool 一个入口。继承:is-a 的复用与扩展,Java 单继承——项目里 HitlToolRegistry 继承 ToolRegistry,覆写 executeToolOutput 插入审批逻辑,其余行为零改动。多态:父类引用指向子类对象、运行时动态绑定——代码里持有的都是父类 ToolRegistry 引用,HITL 开没开取决于注入的是哪个子类实现,调用方完全无感。一句话总结:封装管边界、继承管复用、多态管扩展——我的审批能力就是靠「继承 + 多态」以装饰器方式插进去的,没有在原类里塞 if。

9. HashMap 实现原理 八股 ​

参考答案:

JDK 8 用「数组 + 链表 + 红黑树」。put 时对 key 的 hashCode 做扰动——高 16 位异或低 16 位,让高位也参与寻址,减少碰撞——再与 (n-1) 按位与定位桶。碰撞用链表尾插;链表长度到 8 且数组长度 ≥ 64 时树化为红黑树(查询 O(logn)),缩回 6 退化回链表。默认容量 16、负载因子 0.75,元素数超过容量 × 0.75 触发翻倍扩容,JDK 8 的 rehash 按hash 新增位是 0 还是 1 把链表拆成高低两条,不需要重算 hash。允许一个 null 键;线程不安全——JDK 7 头插并发扩容会成环,JDK 8 改尾插解决了环,但并发 put 仍有数据覆盖丢失,并发场景用 ConcurrentHashMap。

10. ConcurrentHashMap 实现原理 八股 ​

参考答案:

JDK 8 抛弃了分段锁,改为 CAS + synchronized 锁单个桶头节点:桶为空用 CAS 插入新节点,有冲突才 synchronized 锁住头节点——锁粒度从「段」细化到「桶」,并发度约等于数组长度。get 全程无锁,靠 Node 的 volatile val 和 next 保证可见性。扩容支持多线程协助迁移:迁移中的桶放 ForwardingNode,读它就去迁移后的新表找,写它的线程帮忙一起搬。size 不用全局计数器,用 baseCount 加 CounterCell 数组分散计数(LongAdder 思路),避免计数成为热点。不允许 null 键值——并发下 get 返回 null 无法区分「不存在」和「值就是 null」。迭代器是弱一致的,不抛 ConcurrentModificationException。

11. 「hashCode 恒为 1,放一堆进 HashMap,会被覆盖吗?」 八股计算题 ​

参考答案:

不会覆盖,全部共存。HashMap 判断是否同一个 key 的条件是 hash 相同且 equals 为 true——hashCode 相同只是让它们落进同一个桶,触发哈希冲突,用链表(或红黑树)挂在一起;只要 equals 不相等(默认 Object.equals 比较引用,新 new 的对象各不相等),就是不同的 key,各自存各自的价值。

后果是退化:所有元素挤在同一个桶,链表越来越长,get 从 O(1) 退化成 O(n);JDK 8 里链表长到 8 且数组长度 ≥ 64 会树化成红黑树,查询 O(logn)——数组长度不够 64 就先扩容。反向说:如果把这些对象的 equals 也重写成永远相等,那才会互相覆盖、只留最后一个。

考点一句话:覆盖的判断权在 equals,hashCode 只决定住哪个桶——这也是为什么重写 equals 必须重写 hashCode。

12. 「Redis 的 RENAME 会重置到期时间吗?」 新题 ​

参考答案:

不会重置——RENAME 后新 key 继承原 key 的剩余 TTL。Redis 的过期时间存在专门的 expires 字典里(key → 过期时间戳),RENAME 的语义是把原 key 连数据带 TTL 映射一起迁移到新名字下:原 key 剩 50 秒,改名后新 key 就剩 50 秒,不会变回永久。另外如果 newkey 本来已存在,会被原样覆盖(连同它自己的 TTL 一起丢弃)。

工程上的坑:用「SET 临时 key 加 EX,再 RENAME 转正」这种模式时,要清楚转正后的 key 带的是临时 key 的剩余 TTL——如果转正后希望它是永久 key,必须显式 PERSIST;反过来想让它到期自动清理,转正前就要算好剩余时间。相关命令还有 RENAMENX:newkey 已存在时拒绝执行。

13. ThreadLocal 实现原理 八股 ​

参考答案:

每个 Thread 对象内部有一个 ThreadLocalMap(threadLocals 字段),线程隔离的本质是数据挂在线程身上:threadLocal.set 时以当前线程的 map.put(threadLocal 对象, value) 存入,get 时取当前线程的 map 按 threadLocal 对象查——不同线程操作同一个 ThreadLocal,拿到的是各自 map 里的各自值。

两个必考点。一是内存泄漏:map 的 key 是 ThreadLocal 对象的弱引用(防止 ThreadLocal 外部回收后 entry 永远不可达),但 value 是强引用——key 被回收后 entry 变成 (null, value),线程不死就清不掉;线程池里线程长期存活,泄漏会累积,所以用完必须 remove()。二是线程池复用污染:线程复用时会读到上个任务的残留值,提交任务前后要 set / remove 成对。延伸:父子线程传递用 InheritableThreadLocal,线程池场景要用 TransmittableThreadLocal(拿到任务提交时的上下文副本)。

14. 「core=10, max=100, 队列=30,50 个任务同时到达,高峰会有多少个线程?」 八股计算题(招人比喻实战) ​

参考答案:

高峰 20 个线程。按提交流程走:① 前 10 个任务开满 10 个核心线程;② 第 11–40 个任务入队,队列容量 30 正好放满;③ 第 41 个任务发现队列已满、线程数还没到 max,开始创建非核心线程——剩余 10 个任务需要 10 个非核心线程,20 个线程(10 核心 + 10 非核心)就全部领完了。50 个任务 = 在执行的 20 个 + 队列里排着的 30 个,全部安置,不会触发拒绝。

考点一句话:队列满了才扩非核心线程,不是先扩满再排队——所以高峰线程数是 20 而不是 100。追问变体也要会:如果队列是无界的(比如 newFixedThreadPool 的默认队列),第②步永远「不满」,非核心线程永远不会创建,高峰就只有 10 个线程——这正是无界队列的风险。

15. AtomicInteger 实现原理 八股 ​

参考答案:

基于 CAS + volatile。value 用 volatile 修饰保证可见性;incrementAndGet 底层通过 Unsafe 调用 CPU 的原子指令(x86 上是 LOCK 前缀的 CMPXCHG)做比较并交换——「当前值是 expected 就改成 next,返回是否成功」,失败就自旋重试直到成功。无锁:没有挂起和唤醒的开销,低中并发下性能优于 synchronized。

两个延伸。一是 ABA 问题:值从 A 改回 A,CAS 感知不到中间过程——需要感知时用 AtomicStampedReference 加版本号;二是高并发下的自旋失败率:大量线程挤同一个变量,失败重试浪费 CPU,这时用 LongAdder——把计数分散到 Cell 数组、读时汇总,用空间换吞吐(和 ConcurrentHashMap 的 CounterCell 同一思想)。

16. ArrayList 和 LinkedList 的区别 八股 ​

参考答案:

四个维度对比。底层结构:ArrayList 是动态数组 Object[],连续内存;LinkedList 是双向链表,每个 Node 带 item、next、prev 两个引用。随机访问:ArrayList 按下标 O(1) 直接寻址;LinkedList 要从头或尾遍历,O(n)——但 JDK 有折半优化,见下一题。插入删除:ArrayList 尾部均摊 O(1)(扩容 1.5 倍时才整体复制),中间插删要 System.arraycopy 移动后续元素,O(n);LinkedList 头尾操作 O(1),指定位置的插删同样是 O(n)——必须先遍历找到那个节点,这是常见误区:不是链表插删就一定快,只有已经拿到节点引用时,摘链才是 O(1)。内存:ArrayList 连续内存、缓存友好,代价是扩容和预留空间的浪费;LinkedList 每个节点多两个引用、内存开销大、指针跳跃对缓存不友好。两者都线程不安全,并发用 CopyOnWriteArrayList 或 Collections.synchronizedList。

17. 「LinkedList 查找第 90 位,需要查找多少次?」 八股计算题 ​

参考答案:

直接答 90 次就中了陷阱——JDK 的 node(int index) 有折半优化:index < size >> 1 时从 first 沿 next 正着走,否则从 last 沿 prev 倒着走。所以要分情况:

  • 链表长度 size > 180(index 90 落在前半段):从头正着走,90 次 next 移动、访问 91 个节点;
  • 链表长度 90 < size ≤ 180(index 90 落在后半段):从尾倒着走,只需 size − 91 次——比如 size = 100 时只走 9 次;
  • 无论哪种,最坏不超过 size / 2。

考点一句话:面试官想听的不是背「链表查找 O(n)」,而是知不知道源码里这个二分方向优化——顺带引申到选型结论:正因为随机访问是 O(n/2),频繁按索引读的场景选 ArrayList;LinkedList 的价值在头尾增删(如队列、双端栈),用 ArrayDeque 替代更好。

复盘提示 ​

面试官反馈:对项目技术的使用要熟悉,不要停留在表层,要深层次使用——具体到怎么用、为什么这么用、和其他方案进行对比。

对应补强方法:每个技术点按三连自问——「怎么用的(执行层细节)→ 为什么这么用(当时的约束和取舍)→ 其他方案是什么、为什么不选(对比论证)」。题库里所有完整答案都是按这个结构写的,复习时遮住答案先自答三连。

  • 第 3、7、11、12、14 题是本轮新增内容,重点过;第 14 题正是后端岗行动项「招人比喻」的实战应用
  • 第 1、2、5 题全部有题库托底,扫一眼即可
  • 本场结构(实习 + AI 项目 + 大量 Java 基础)说明:大厂一面八股占比不低——八股库的后端通用部分要按题库的弱点清单系统过

本站仅供个人学习使用,请勿外传