# 阿里淘天 Agent 研发岗面经:从“会调模型”追问到代码怎么提交
面经时间:2026年8月18日
前面那篇字节番茄小说 Agent 应用开发实习一面,重点是混合检索、多 Agent 编排和项目效果怎么衡量。
这次看阿里淘天 Agent 研发岗。面试官抓住一个代码 Agent 项目,沿着“需求怎么进来、代码在哪读、模型返回什么、谁负责执行、改完怎么提交”连续往下问,后半场再切到 Java 基础。
光说“我接了 MCP,模型能自动写代码”,这场面试很难答下去。
下面按题目顺序整理参考回答。原始材料只有问题,没有候选人的完整简历和作答,所以文中的第一人称是口述示例,统一以“面向本地 Java 仓库的代码 Agent”为背景,不代表原候选人实际做过这些功能。录友使用时,要换成自己的实现,没做过的能力就说是后续方案。
本次面经问题:
- 一、自我介绍,并介绍简历上的内容
- 二、这个 Agent 主要实现了什么功能?
- 三、Agent 用来做什么?执行链路怎样?
- 四、支持写代码,那代码库在哪读、怎么读?
- 五、要改某个项目的代码,怎么做?
- 六、读到代码、改完之后,代码怎么管理?
- 七、写到本地之后,Git 怎么提交?
- 八、小需求如何分析、拆解并逐步实现?
- 九、三个线程交替打印 abcabc,在 Agent 里怎么实现?
- 十、需求交给模型后,模型返回什么?
- 十一、结合上下文和已有仓库新增实现,有什么不同?
- 十二、已有接口和策略 A/B,新增策略 C,第一步返回什么,第二步调用什么?
- 十三、用这个 Agent 处理最多的是什么问题?
- 十四、“获取知识”是联网查询还是已有知识库?
- 十五、本地代码库构建成了知识库吗?
- 十六、RAG 的分片逻辑是什么?
- 十七、召回的分片不完整怎么办?
- 十八、还有哪些索引方式?
- 十九、除了文件分片,还有哪些方式组织 RAG 数据?
- 二十、AI 方面还有哪些探索和学习?
- 二十一、介绍一下学到的 Harness
- 二十二、Harness 相比 MCP、ReAct 多了什么?
- 二十三、哪些核心要素让 Harness 更稳定、更安全?
- 二十四、Java 类从字节码到内存,经历了什么?
- 二十五、类加载后,各部分存在哪些内存区域?
- 二十六、对象的属性和方法分别存在哪?
- 二十七、类卸载的条件有哪些?
- 二十八、什么是反双亲委派?
- 二十九、线程池的核心线程、队列、最大线程和拒绝策略
- 三十、最大线程数 30,第二个请求复用线程还是新建?
- 三十一、Java 中有哪些锁?
- 三十二、读写锁怎么用?
- 三十三、Java 8 的 HashMap 是什么结构?
- 三十四、红黑树是什么结构?
- 三十五、本地缓存和 CPU L1/L2/L3 有什么关系?
# 一、自我介绍,并介绍简历上的内容
我主要做 Java 后端,也在做大模型应用方向的实践。简历里最相关的是一个代码 Agent:用户给出小范围需求后,它能在指定仓库里定位实现、修改代码、运行验证,并输出 Diff 和执行结果。
我主要负责工具执行层和任务循环,包括文件检索、补丁应用、命令执行、任务状态记录,以及工作区的权限限制。模型负责选择下一步动作,真正的文件和进程操作由执行器完成。
这个项目里,我最关注两个问题:一是模型怎么拿到足够且准确的代码上下文,二是怎么证明它已经完成任务。前者靠分层检索和实时读取,后者靠测试、Diff 检查和明确的验收条件。
这里要按真实情况补充自己的技术栈和完成度。自我介绍里提到的每个能力,后面都有可能被追问到代码。
# 二、这个 Agent 主要实现了什么功能?
它主要有三类功能:解释代码、辅助排查问题,以及完成边界明确的小需求。
比如用户说“给现有策略接口增加一个实现”,它先找接口、已有实现和注册入口,再补代码和测试。遇到报错,则先读日志和相关实现,提出可验证的假设,再通过工具检查。
我会把交付物定义为“修改后的文件、Diff、验证结果和遗留问题”。模型回复一句“已经完成”,不能算功能实现成功。涉及业务规则不明确的需求,会先补齐关键条件,不能让模型自己编规则。
# 三、Agent 用来做什么?执行链路怎样?
它服务于开发者,把原来需要手动搜索、阅读、改代码和验证的流程串起来。
一次任务大致是:接收需求 → 读取仓库约定 → 定位相关代码 → 形成计划 → 调用工具修改 → 执行检查 → 根据结果修正 → 返回交付物。
这不是一次模型请求就结束。每轮把目标、必要上下文、工具定义和已有结果交给模型;如果模型提出工具调用,执行器先校验,再执行,把结果作为工具消息放回下一轮。
测试失败就读报错、继续定位;发现目标有歧义就澄清;达到轮数、时间或费用上限就停止并交代进展。停止条件是执行层控制的,不能只靠 Prompt 提醒模型别无限循环。

图里的循环是基本骨架,真正做成代码 Agent,还要补工作区、权限和验收这些工程能力。
# 四、支持写代码,那代码库在哪读、怎么读?
代码在用户指定的本地工作区,或者事先检出的隔离工作区里。模型本身读不到磁盘,是执行器把工具结果传给模型。
我会先看目录、README、构建文件和仓库约定,确认语言、模块和测试入口,再用文件名匹配和关键词搜索定位。比如先查 *Strategy*,再搜索接口名,最后按路径和行范围读取完整方法及调用点。
仓库大时可以接符号索引或语义检索,但命中以后仍然回到当前工作区读原文件。索引负责指路,当前文件负责确认事实。 搜索无结果和输出被截断也要告诉模型,不能把“没看到”直接当成“不存在”。

更完整的取舍,可以看代码 Agent 为什么需要 grep 和 RAG。
# 五、要改某个项目的代码,怎么做?
我先确认仓库、分支和当前工作区状态,避免把别人的未提交改动当成自己的产物。然后读目标接口、已有实现、调用方和测试,判断真正需要改哪些位置。
模型基于这些上下文生成补丁请求,执行器校验路径是否在允许范围、原内容是否仍匹配,再应用修改。应用失败会返回冲突信息,重新读文件,而不是强行覆盖。
改完先跑相关测试和编译检查,再检查 Diff 有没有无关改动。已有失败要与本次引入的失败区分开;依赖缺失导致没跑成,也不能汇报为“测试通过”。
# 六、读到代码、改完之后,代码怎么管理?
仓库文件由 Git 管理,任务过程另外记录任务 ID、基线 commit、修改文件和执行结果。
简单的单人本地任务可以在当前分支操作;需要隔离时,为任务创建分支或 Git worktree。Worktree 有独立的检出目录和索引,但共享仓库对象数据库,适合减少多个任务对同一工作区的干扰。
在任务开始时记录已有改动,结束时只整理本次新增的 Diff。索引也要带仓库、版本等元数据,改文件后及时更新或标记过期。
回退时只回退本次任务的改动。如果文件里混有用户修改,先保留现场再处理,不能用整仓库硬重置来图省事。
# 七、写到本地之后,Git 怎么提交?
写文件、暂存、提交、推送和合并是几件不同的事,Agent 要分别表达状态。
如果任务授权包含本地提交,执行器会先检查 git status 和 Diff,只暂存本任务相关文件,再检查暂存区内容,生成准确的提交说明,执行 git commit,最后读取 commit ID 并确认工作区状态。
不能直接 git add . 把无关文件一起带上。提交钩子失败,就把失败原因返回给模型处理,不能绕过后宣称成功。
如果只要求“帮我改代码”,就交付 Diff 和验证结果。是否 push、提 PR、合并,按用户授权和仓库流程单独处理。Git 能追踪修改,但不能替代测试和审查。
# 八、小需求如何分析、拆解并逐步实现?
先把自然语言需求变成可以验证的条件,再定位代码和拆步骤。
比如“新增一种优惠策略”,我需要确认输入、输出、适用条件、错误处理和兼容要求。计划可以是“读接口与已有策略 → 找选择入口 → 新增实现 → 注册 → 补边界测试 → 验证旧策略不受影响”。
每一步都应有完成标志:定位阶段给路径和符号,修改阶段给 Diff,验证阶段给命令、退出码和结果。任务很小就用简单步骤列表,不一定要引入多 Agent 或 DAG。
执行中发现策略选择写死在别处,就更新计划和影响范围。计划是随着证据修正的工作状态,不是模型一开始写完就不能变的清单。
# 九、三个线程交替打印 abcabc,在 Agent 里怎么实现?
我先把验收条件讲清楚:三个线程分别打印 a、b、c,顺序必须严格交替;这里约定重复两轮,输出 abcabc,三个线程都要正常退出。
Agent 如果在已有项目里工作,会先确认 JDK、源码目录和测试方式;如果是独立题,就创建最小 Java 示例。模型提出实现和补丁请求,执行器落盘,然后调用编译、运行工具,把输出和退出状态交回模型检查。
实现可以用共享状态、synchronized 和 wait/notifyAll:
public class AlternateABC {
private final Object lock = new Object();
private int turn = 0;
private boolean cancelled = false;
private void print(int id, char ch, int rounds) {
synchronized (lock) {
for (int i = 0; i < rounds; i++) {
while (!cancelled && turn != id) {
try {
lock.wait();
} catch (InterruptedException e) {
cancelled = true;
lock.notifyAll();
Thread.currentThread().interrupt();
return;
}
}
if (cancelled) {
return;
}
System.out.print(ch);
turn = (turn + 1) % 3;
lock.notifyAll();
}
}
}
public static void main(String[] args) throws InterruptedException {
AlternateABC printer = new AlternateABC();
Thread a = new Thread(() -> printer.print(0, 'a', 2));
Thread b = new Thread(() -> printer.print(1, 'b', 2));
Thread c = new Thread(() -> printer.print(2, 'c', 2));
a.start();
b.start();
c.start();
a.join();
b.join();
c.join();
System.out.println();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
while 用来重新检查条件,处理虚假唤醒;notifyAll 避免只唤醒一个暂时不能执行的线程。状态判断、打印和更新都在同一把锁内,wait 会释放锁,唤醒后重新竞争。
中断时设置共享取消标志并唤醒其他线程,避免有一个线程退出后,其余线程永远等不到自己的前驱。验证时还要加运行超时,多次执行检查顺序和结束状态,不能只看一次输出正确。
面试官问的是“在 Agent 里怎么实现”,所以除了并发代码,还要讲清楚需求约束、工具调用、执行反馈和验收。
# 十、需求交给模型后,模型返回什么?
不一定直接返回完整代码,取决于当前阶段和接口约定。可能是最终回答、结构化计划,也可能是一组工具调用请求。
比如在缺少代码上下文时,它可能返回这样的逻辑结构。下面是示意,不绑定某家接口的字段格式:
{
"tool_calls": [
{
"id": "call_001",
"name": "search_code",
"arguments": {
"query": "interface PricingStrategy",
"path": "src"
}
}
]
}
2
3
4
5
6
7
8
9
10
11
12
这只是“请求搜索”。执行器检查参数、权限和返回大小后才执行,工具结果要关联调用 ID,再送回模型。下一轮模型可能读文件,也可能在证据足够后申请补丁操作。
模型生成工具调用,不等于工具已经执行;模型说改好了,也不等于磁盘真的变了。 系统要分别记录请求、执行结果和最终交付状态。这里依赖的是接口返回的可观察结果,不需要把模型内部思维过程当成执行计划。
# 十一、结合上下文和已有仓库新增实现,有什么不同?
独立题只要满足题意;已有仓库里还要满足接口契约、命名、依赖、异常约定和业务兼容性。
比如独立实现策略 C,写一个类就可能够了。但在现有项目里,要确认接口参数、金额单位、策略选择方式、配置项、依赖注入和测试框架。项目可能已经有工具类,不能重复实现。
所以我先拿到“接口 + 代表性实现 + 调用入口 + 相关测试”这组最小上下文,再生成修改。验收也从“新代码能运行”变成“新逻辑生效,而且原来的 A/B 行为不被破坏”。
# 十二、已有接口和策略 A/B,新增策略 C,第一步返回什么,第二步调用什么?
如果上下文只有“有接口和 A/B”这句话,我期待第一步返回定位代码的工具请求,比如搜索 PricingStrategy 和它的实现;第二步由执行器真正调用搜索工具,再把结果交回模型。模型下一轮才根据路径请求读文件。
如果完整接口、A/B、注册入口和需求规则都已经给到模型,它就可能直接返回补丁请求。执行器校验后应用补丁,不必为了固定流程再搜一遍。
一个典型过程是:
| 轮次 | 模型提出的动作 | 执行器返回的证据 |
|---|---|---|
| 定位 | 搜接口、A/B 和选择入口 | 文件路径、命中位置 |
| 阅读 | 读接口、实现、注册代码和测试 | 当前代码、行号 |
| 修改 | 新增 C,接入选择入口,补测试 | 补丁是否成功、修改摘要 |
| 验证 | 跑相关测试和编译 | 退出码、日志、测试结果 |
| 交付 | 汇报实现与限制 | 已核验的 Diff 和结果 |
如果项目用 Spring 按 Bean 名称装配,就沿用现有注册方式;如果是枚举或工厂映射,就补映射。新增一个类,不一定意味着调用链已经能选到它。
# 十三、用这个 Agent 处理最多的是什么问题?
按这个示例项目的定位,我会回答:主要是代码定位、小范围修改和测试修复,比如找某个接口的实现、补参数校验、增加一个已有扩展点的实现。
这类任务边界清楚、反馈快,比较适合验证 Agent 是否真的有帮助。跨多个服务的大改动,会涉及更多业务信息和联调依赖,不能用“本地编译通过”就宣布完成。
如果有使用记录,我会按任务类型统计数量、人工介入和验收情况,没有数据就说明目前主要是个人验证。不要为了让项目显得成熟,临时编成功率或用户规模。
# 十四、“获取知识”是联网查询还是已有知识库?
我会分清三类来源:当前仓库、团队文档,以及外部资料。
代码事实优先查当前工作区;团队规范和业务说明可以走已有知识库;第三方 API、版本行为等需要时效性的信息,再查官方文档。模型预训练知识用来解释和推理,不能替代对项目现状的读取。
不是所有任务都会联网。工具权限和数据范围要跟任务一致,私有代码也不能默认发到外部搜索服务。最终回答最好带路径、文档版本或来源链接,方便核验。
# 十五、本地代码库构建成了知识库吗?
在这个示例方案里,本地代码库首先是实时工作区,不要求先把整个仓库向量化才能使用。文件匹配、关键词搜索和按行读取就能解决很多问题。
代码量大、跨模块语义定位困难时,可以另建检索索引。索引保存代码片段或符号、向量、文件路径和版本信息,但它不是代码的唯一真相,也不接管文件修改。
未提交改动尤其容易与索引不一致。因此召回后还要读当前文件;索引有增量更新和删除机制,分支也要隔离。能读本地代码,不等于一定做了 RAG;用了 RAG,也不等于每一步都走向量检索。
# 十六、RAG 的分片逻辑是什么?
分片要兼顾两个目标:检索时能精确命中,生成时又有足够上下文。
普通文档先解析标题、段落、表格等结构,再按语义边界切分;超长章节才继续按长度拆。代码更适合结合 AST,围绕类、方法和声明切分,超长函数还得二次拆分,并保留所属符号。
每块带文档或文件 ID、路径、标题层级、行范围、父块 ID、版本和访问范围等元数据。必要时做少量重叠,但重叠只能缓解边界问题,不能保证函数依赖完整。
块大小和重叠比例没有通用答案,要看语料和查询,用检索评测与任务效果调。嵌入时可以加入路径、标题等上下文,但别把一堆无关元数据都塞进正文。

分块和召回的联动问题,可以继续看RAG 落地最难的地方在哪。
# 十七、召回的分片不完整怎么办?
我先判断是块被切断、相关依赖没召回,还是原文就没有答案,处理方式不一样。
边界被切断,可以用父子块:小块负责匹配,命中后补回所属章节或完整方法;也可以按块序号补相邻片段。代码依赖缺失,就沿符号定义、调用关系和测试继续查,邻近几行不一定能覆盖另一个文件里的实现。
补出来的内容要去重、控制长度,并再次检查相关性。代码任务优先按路径和行号读当前文件,避免旧索引把已经删除的逻辑补回来。
如果补查后证据仍不足,就缩小结论范围或继续检索,不能让模型顺着残缺片段补出一个看起来完整的答案。单纯把 Top-K 调大,可能只是增加噪声。
# 十八、还有哪些索引方式?
除了向量索引,我还会考虑下面几类,它们擅长的问题不同:
| 索引或定位方式 | 适合解决的问题 |
|---|---|
| 倒排索引、BM25 | 函数名、配置键、错误码等精确词匹配 |
| 文件路径索引 | 按模块、目录、后缀缩小范围 |
| AST、符号索引 | 找类、方法、定义和引用 |
| 调用或依赖图索引 | 查上下游关系、影响范围 |
| 元数据过滤索引 | 限定仓库、版本、租户和权限 |
| 混合检索 | 同时覆盖自然语言语义和精确标识符 |
这些方式可以组合。比如先按仓库和版本过滤,再做关键词与向量召回,融合重排后读取原文。
grep 本身通常是直接扫描,不应因为它能搜索就把它说成“建好了索引”。另外,索引解决的是“怎么找到”,不是“返回多少上下文”或“怎么验证答案”,这几层要分开讲。
# 十九、除了文件分片,还有哪些方式组织 RAG 数据?
上一题讲检索入口,这一题我会理解为“检索单元一定得是文件块吗”。答案是不一定。
业务问答可以按问答对组织,先匹配问题再取审核过的答案;结构化业务数据可以按记录和字段检索,或通过受控 SQL 查询;代码可以按符号、定义引用关系组织;关系密集的材料可以抽取实体和边,结合图检索定位关联证据。
长文档还可以构建章节摘要和层级索引,先定位主题再下钻原文。摘要适合路由,具体结论仍要核对原文,避免摘要遗漏限定条件。
这些方法并不都排斥分片,图节点也可能关联原文块。RAG 的关键是先取回任务相关的外部证据,再辅助生成,不是必须把文件按固定长度切成向量。 图检索的适用范围可以看GraphRAG 面试详解。
# 二十、AI 方面还有哪些探索和学习?
我会围绕这个项目延伸,而不是列一串没实际碰过的名词。比如读过 ReAct 和工具调用的基本机制,比较过代码检索与文档 RAG,也在学习 Harness 怎么管理执行状态、权限和验证。
如果做过实验,就给一个具体观察:同一批代码任务,只有“生成补丁”的版本经常漏注册和漏测试;加上读取调用入口、运行相关测试和检查 Diff 后,能发现这些问题。没有完整评测就说是观察,不能把几次体验包装成量化结论。
学习和实现要区分。我可以说某个恢复机制研究过、尚未接入,接着讲设计和局限,这比声称已经具备完整生产能力更经得起追问。
# 二十一、介绍一下学到的 Harness
我理解的 Harness,是围绕模型构建的任务执行与管理系统。它没有唯一固定的组件清单,核心是让模型在明确的环境、状态和约束里完成任务。
对代码 Agent 来说,它负责准备工作区、装配工具、维护上下文和任务状态、执行工具请求、处理超时失败,并收集验证证据。
比如模型申请运行测试,Harness 要决定这个命令能否执行、在哪执行、多久超时、日志返回多少;进程失败后,是重试、回到定位阶段,还是报告阻塞。模型负责提出动作,执行层控制动作如何落地。
概念和常见追问可以对照Harness Engineering 面试题。
# 二十二、Harness 相比 MCP、ReAct 多了什么?
它们处在不同层次,可以一起使用,不能简单理解为谁替代谁。
| 概念 | 主要回答什么问题 | 在代码 Agent 里的例子 |
|---|---|---|
| ReAct | 如何结合推理、行动和观察推进任务 | 搜代码、看结果、决定下一步 |
| MCP | 如何标准化接入工具和资源 | 连接代码搜索或文档服务 |
| Harness | 如何管理一次任务的完整执行 | 权限、工作区、状态、预算、恢复和验收 |
所以“调通 MCP”只能说明工具接入链路跑通了。还要看调用者有没有权限、工具是否有超时、重复请求会不会产生副作用、任务中断后能否恢复。
ReAct 也可以运行在完善的 Harness 里。它没有禁止这些工程能力,只是这个模式本身不替你实现它们。
# 二十三、哪些核心要素让 Harness 更稳定、更安全?
我会讲具体失败场景和对应机制,而不是只说“多了一层校验”。
第一是明确的任务状态和验收条件。记录当前做到哪一步、有哪些产物,测试没通过就不能进入完成状态;中断后按检查点恢复,而不是重新猜之前发生了什么。
第二是执行层的权限和隔离。限定可读写目录、工具范围和网络访问,结合进程或容器隔离;仓库文件、网页和工具结果里的指令都按外部数据处理,不能直接提升为系统规则。
第三是超时、预算、取消和重试边界。只对合适的瞬时故障做有限重试;提交、发消息等有副作用的动作,要先核对是否已经成功,不能网络断了就重复执行。
第四是上下文与证据管理。保留目标、关键约束、已验证结论和产物引用,压缩历史时不丢验收条件;读到过期代码要回源确认。
第五是可观测性和独立检查。工具请求、结果、Diff、测试输出都能追踪;格式、类型和测试等能由程序检查的部分,尽量由程序检查。再加模型审查也有帮助,但不能据此保证正确。

这些机制把一部分约束变成执行系统的硬边界,降低越权、重复执行和虚假完成的概率。更多实现细节见Multi-Agent Harness 面试详解。
# 二十四、Java 类从字节码到内存,经历了什么?
我按加载、链接、初始化来讲,链接包含验证、准备和解析。
加载时找到字节码并建立类型表示;验证检查格式和字节码约束;准备阶段为静态字段建立存储并设置默认值;解析把符号引用解析为可使用的运行时引用;初始化执行类的 <clinit>,包括静态字段显式赋值和静态代码块。
比如 static int count = 10,准备阶段默认是 0,初始化时才执行赋值为 10。带 ConstantValue 的常量字段有专门赋值规则,不要把所有 static final 都当成普通静态字段。
解析可以按需延迟,不一定一次全部做完;类初始化与对象构造的 <init> 也不是同一件事。阶段与时机可核对 JLS 第十二章 (opens new window)。

# 二十五、类加载后,各部分存在哪些内存区域?
这里我会先区分 JVM 运行时数据区和 Java 内存模型 JMM。前者讲数据存在哪,后者主要讲并发读写的可见性、有序性等规则,这道题问的是前者。
按 JVM 规范,堆主要存对象和数组,方法区保存类级别的结构与方法代码;每个线程有自己的虚拟机栈和程序计数器,方法调用时创建栈帧。本地方法还有相应的运行支持。规范并不规定所有实现的物理布局,见 JVMS 运行时数据区 (opens new window)。
如果问的是 JDK 8 及之后的 HotSpot:类元数据主要在本地内存中的元空间;Class 镜像对象在 Java 堆;静态字段存储与类镜像关联;JIT 编译后的机器码在 Code Cache。不能一句“类相关的东西都在元空间”带过。
# 二十六、对象的属性和方法分别存在哪?
实例属性随对象存放,通常在堆里;引用类型的属性保存的是引用,被引用对象有自己的存储。静态字段属于类,不会给每个实例复制一份。
方法也不会随每个对象复制。以 HotSpot 为例,方法元数据和字节码主要随类元数据管理,编译后的机器码在 Code Cache;每次调用方法,会为当前线程建立栈帧,保存参数、局部变量和操作数等。
比如 User u = new User(),u 如果是局部变量,通常作为引用保存在当前栈帧;new User() 对象通常在堆,其实例字段也在对象布局中。实际运行可能被 JIT 做标量替换等优化,面试先讲清通常模型,再补实现差异。
# 二十七、类卸载的条件有哪些?
对普通命名类,核心是定义它的类加载器能够被垃圾回收。仅仅没有该类实例还不够,外部仍可能持有 Class、类加载器或其他关联引用。
常见排查会看实例、类镜像以及类加载器是否仍被引用,尤其注意线程上下文类加载器、ThreadLocal 和全局缓存造成的保留。满足可回收条件也不意味着立刻卸载,还取决于 GC 和相关配置。规范条件见 JLS 类与接口卸载 (opens new window)。
所以插件卸载时,不能只把一个插件变量设为 null,还要停掉相关线程、清理外部引用。若进一步讨论现代 JDK 的隐藏类,还需单独说明它们的生命周期规则,不能把普通类的结论不加区分地套过去。
# 二十八、什么是反双亲委派?
默认的父优先委派,是先查是否已加载,再委托父加载器,父加载器找不到时才由当前加载器查找。“反双亲委派”通常指特定范围的子优先加载,并不是完全取消委派。
比如插件或 Web 应用为了隔离依赖,可以让业务包先在自己的路径找,找不到再交给父加载器。Java 核心类和共享 API 一般仍要遵循相应保护和共享规则,不能让插件随意替换。
实现上通常在 loadClass 中控制范围,保留已加载检查和并发加载保护;单纯重写 findClass,默认调用链仍然是父优先。默认行为可参照 ClassLoader 文档 (opens new window)。
另一个常见例子是 SPI 借线程上下文类加载器查找应用侧实现,但它和“整个加载器改成子优先”要区分。同一个类名由不同定义加载器加载,可以形成不同类型,插件边界上的接口因此要统一由共享加载器提供。
# 二十九、线程池的核心线程、队列、最大线程和拒绝策略
以标准 ThreadPoolExecutor 为例:工作线程少于核心线程数时,优先创建线程执行;达到核心数后尝试入队;入队失败再尝试扩到最大线程数;无法接收时执行拒绝处理。线程池已关闭也会拒绝任务。
常见策略是抛异常的 AbortPolicy、由提交线程执行的 CallerRunsPolicy,以及丢弃任务的 DiscardPolicy、丢弃队头后重试的 DiscardOldestPolicy。是否允许丢任务,要看业务。
此外还要结合空闲回收时间、线程工厂等参数。无界队列可能让任务持续堆积,正常情况下不会靠增大最大线程数解决;有界队列要配合并发和拒绝处理。具体语义见 ThreadPoolExecutor 官方说明 (opens new window)。

# 三十、最大线程数 30,第二个请求复用线程还是新建?
只给最大线程数 30,无法判断。 我还要知道核心线程数、当前工作线程数、队列,以及第一个线程是否仍存活。下面假设线程池正常运行、通过标准 execute 提交:
- 核心数为 5,当前只有 1 个存活工作线程,即使它已经空闲,第二个任务通常仍会触发新建线程,因为还没到核心数。
- 核心数为 1,当前已有 1 个空闲工作线程,普通队列成功接收任务后,就可以被这个线程取走执行。
- 如果原线程已因超时等原因退出,就不能复用它;使用直接移交队列等情况,还要看移交是否成功。
这里比较的是工作线程总数,不是正在忙的线程数。默认不预启动核心线程,所以即使任务串行提交,也可能逐步把线程数补到核心数。这个优先级在 ThreadPoolExecutor 文档 (opens new window)中有明确说明。
# 三十一、Java 中有哪些锁?
我先讲常用实现,再讲分类,避免把不同维度混在一起。
synchronized 是内置监视器锁,支持重入,退出同步块时自动释放;ReentrantLock 是显式锁,支持可中断获取、尝试获取、超时和多个 Condition,使用时要在 finally 中解锁。
公平与非公平、共享与独占、乐观与悲观,属于不同分类维度。CAS 是原子比较更新机制,可以用来构建乐观并发控制,但不是所有业务操作都能靠一次 CAS 完成,还要考虑重试成本和 ABA 等问题。
volatile 提供相应的可见性和排序语义,不能让 count++ 这样的复合操作自动具备原子性。代码 Agent 并发写同一个工作区时,也不能只给一个状态字段加 volatile,就认为文件修改安全了。
# 三十二、读写锁怎么用?
ReentrantReadWriteLock 允许多个线程同时持有读锁,写锁与其他读写持有者互斥,适合读多写少、临界区有一定工作量的共享数据访问。
比如一份内存配置表,读取时加读锁,更新时加写锁。读锁保护下也不能去修改共享结构;访问入口必须遵守同一套锁规则。
它支持先持有写锁,再获取读锁,最后释放写锁的降级过程;不能直接持有读锁等写锁来完成升级,否则可能一直等自己释放读锁。通常要先释放读锁,再取写锁,并重新检查条件。这两种转换的区别见 ReentrantReadWriteLock 官方说明 (opens new window)。
读多也不一定更快,锁管理本身有成本,还要考虑公平性和写线程饥饿。StampedLock 还支持乐观读,但乐观读后必须校验 stamp,并且它不具备与重入锁相同的重入语义。
# 三十三、Java 8 的 HashMap 是什么结构?
它使用数组,桶内保存链表或红黑树。先对 hash 做扰动,再用 (n - 1) & hash 定位桶;冲突时比较 hash 和 key,已有 key 则更新值。
普通 put 路径中,当桶内原有节点达到树化阈值时,新增冲突节点会触发树化检查;数组容量不足 64 时优先扩容。常量 TREEIFY_THRESHOLD 是 8,但别把它简化成“桶里刚有 8 个节点就一定变树”。扩容拆分树桶时,节点较少的部分可以退回链表。具体逻辑见 OpenJDK 8 HashMap 源码 (opens new window)。
容量通常保持为 2 的幂,扩容时原桶的节点依据 hash 的新增位拆到原位置或“原位置 + 旧容量”。它不是线程安全容器,并发读写应根据场景使用同步或 ConcurrentHashMap。
# 三十四、红黑树是什么结构?
它是带颜色约束的二叉搜索树。面试里我会先说搜索树的有序关系,再补红黑规则:节点是红色或黑色,根和空叶子按黑色处理,红节点不能连着红节点,从任意节点到其后代空叶子的路径具有相同黑节点数。
这些约束限制了最长路径和最短路径的差距,使树高保持在 O(log n)。插入和删除后通过变色、旋转恢复约束,查找、插入、删除具有对数级最坏时间界。
它不要求左右子树高度完全一样。放到 HashMap 里,还要说明桶内排序涉及 hash、可比较键和冲突处理,不能简单说成按任意 key 的大小排一棵普通搜索树。
# 三十五、本地缓存和 CPU L1/L2/L3 有什么关系?
我会先确认“本地缓存”指的是应用进程内缓存,还是 CPU 硬件缓存,因为题目把两层放到一起问了。
应用本地缓存,比如 Caffeine,把热点数据保存在当前进程里,通常能减少远程请求和重复计算。但它需要处理容量、过期、淘汰、更新一致性和多实例之间的数据差异;数据在本进程里,也仍然要付出查表、对象访问和可能的 GC 成本。
CPU 的 L1、L2、L3 则由硬件按缓存行管理。通常越靠近核心容量越小、访问越快,具体层级是否私有或共享取决于处理器。JVM 堆是虚拟地址空间中的软件分配区域,其数据访问可能命中 CPU Cache,未命中再向更低层查找,不存在“对象放进 Java 堆,就绕过 CPU 缓存”的关系。
连续访问数组通常比追逐分散对象指针更容易利用局部性。多线程频繁写同一缓存行里的不同变量,可能产生伪共享和一致性流量。这些性能现象可对照 Intel VTune 的 CPU 指标说明 (opens new window)。不能只用“在内存里”四个字判断快慢,也别背固定的纳秒数,实际延迟取决于硬件、访问模式和负载。
回到 Agent 系统,缓存检索结果可以减少重复计算,但键里要考虑仓库版本、权限范围等条件。否则缓存很快,返回的却是别的分支或用户不该看到的代码。
# 这场面试真正考什么?
如果想继续练工具规模和入口设计,可以看百度 Agent 开发岗面经,从前端与 Chat 分工追问到上千工具的路由,以及长对话中的状态保留。
前半场问“模型返回什么”,后半场问“第二个任务会不会复用线程”,其实都要求你把执行过程讲准确。
代码 Agent 里,要分清请求、执行和验收;Java 里,要说清版本、状态和触发条件。只背“工具调用”“线程复用”“方法区”,一追问就容易断。
录友可以拿自己的项目走一遍:给一个小需求,从第一次模型响应讲到最终 Diff,每一步都指出输入、动作、结果和失败处理。 能把这条链讲顺,再去谈复杂架构。
评论
验证登录状态...