淘宝闪购 AI应用开发 一面
估计后面2 3个月只有社招的面经了, 粉丝群的春招和暑期的小伙伴分享的差不多了
1. 自我介绍
2. Java 中 final、finally、finalize 的区别
答案:
final 是关键字,可以修饰类、方法和变量。修饰类表示不能被继承,修饰方法表示不能被重写,修饰变量表示引用或值不能再次赋值。需要注意的是,final 修饰对象引用时,只是引用不能变,对象内部状态仍然可能改变。
finally 是异常处理结构的一部分,通常用于释放资源。无论 try 中是否发生异常,只要 JVM 没有直接退出,finally 通常都会执行。比如关闭连接、释放锁、清理临时文件。
finalize 是 Object 类中的方法,在对象被 GC 回收前可能被调用,但它不可靠,也不推荐使用。因为调用时机不确定,甚至可能不执行,现代 Java 里已经不建议依赖它做资源释放。
final class ContractRule {
private final List<String> rules = new ArrayList<>();
public void addRule(String rule) {
rules.add(rule); // final 修饰的是引用,集合内容仍然可以变
}
}
资源释放应该用 try-with-resources,而不是 finalize:
try (InputStream in = new FileInputStream("contract.pdf")) {
// 读取文件
} catch (IOException e) {
throw new RuntimeException(e);
}
3. try-catch-finally 的具体执行过程是什么
答案:
try 中的代码先执行,如果没有异常,会跳过 catch,然后执行 finally。如果 try 中发生异常,会匹配对应的 catch,catch 执行完后再执行 finally。如果 try 或 catch 里有 return,finally 仍然会在真正返回前执行。
需要注意的是,如果 finally 里也写了 return,会覆盖 try 或 catch 里的返回值,这种写法线上应该避免,因为非常容易隐藏异常或改变结果。
public int test() {
try {
return 1;
} catch (Exception e) {
return 2;
} finally {
System.out.println("finally execute");
}
}
如果 finally 修改的是局部变量,返回值是否变化要看返回的是基本类型值还是对象引用:
public int value() {
int x = 1;
try {
return x;
} finally {
x = 2;
}
}
这里返回的是 1,因为 return 时已经把返回值暂存了。但如果返回的是对象,finally 修改对象内部字段,调用方能看到修改后的对象状态。
4. 线程怎么创建,生产里为什么不建议直接 new Thread
答案:
Java 创建线程可以继承 Thread、实现 Runnable、实现 Callable 配合 FutureTask,也可以使用线程池。生产环境里不建议大量直接 new Thread,因为线程创建和销毁成本高,而且没有统一的队列、限流、监控和拒绝策略。请求量一上来,很容易创建过多线程导致 CPU 切换严重、内存上涨甚至 OOM。
更推荐使用线程池。线程池可以复用线程,并通过核心线程数、最大线程数、队列长度、拒绝策略控制系统负载。
ExecutorService executor = Executors.newFixedThreadPool(8);
executor.submit(() -> {
System.out.println("process contract audit task");
});
但也不建议直接用 Executors 的快捷方法创建线上线程池,因为很多默认队列是无界的。更建议显式使用 ThreadPoolExecutor。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(1000),
new ThreadFactoryBuilder().setNameFormat("contract-audit-%d").build(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
5. 怎么停止一个线程,为什么不推荐 stop
答案:
不推荐使用 Thread.stop(),因为它会强制终止线程,可能导致锁释放时对象处于不一致状态。比如线程正在更新合同状态,刚写了一半就被 stop,内存状态和数据库状态都可能异常。
更安全的方式是协作式停止。常见方式有:使用中断标记 interrupt、使用 volatile 标志位、使用线程池的 shutdown。线程内部要定期检查停止信号,然后自己结束。
class AuditWorker implements Runnable {
private volatile boolean running = true;
public void shutdown() {
running = false;
}
@Override
public void run() {
while (running && !Thread.currentThread().isInterrupted()) {
processOneTask();
}
}
private void processOneTask() {
// 处理任务
}
}
如果线程阻塞在 sleep、wait、queue.take 这类方法上,可以用 interrupt 唤醒:
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
});
worker.start();
worker.interrupt();
6. 线程池有哪些核心参数,怎么设置才不容易出问题
答案:
线程池核心参数包括核心线程数、最大线程数、空闲线程存活时间、任务队列、线程工厂和拒绝策略。核心线程数决定常驻线程数量,最大线程数决定突发流量下最多能扩到多少,队列决定能缓存多少任务,拒绝策略决定超过承载能力后怎么处理。
参数不能拍脑袋设置,要看任务类型。如果是 CPU 密集型,线程数一般接近 CPU 核数;如果是 IO 密集型,比如调用模型、查数据库、请求外部接口,线程数可以适当大一些,但一定要看下游承载能力。
ThreadPoolExecutor executor = new ThreadPoolExecutor(
12,
24,
30,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(500),
r -> {
Thread t = new Thread(r);
t.setName("risk-check-worker-" + t.getId());
return t;
},
new ThreadPoolExecutor.AbortPolicy()
);
线上还要监控线程池状态,比如活跃线程数、队列长度、拒绝次数、任务耗时。如果队列长期接近满,说明系统已经接近瓶颈,不应该只盲目加线程。
int active = executor.getActiveCount(); int queueSize = executor.getQueue().size(); long completed = executor.getCompletedTaskCount();
7. 给一个合同异步审查场景,说一下线程池执行过程
答案:
比如用户上传合同后,接口不直接同步审查,而是提交一个异步审查任务。主线程先保存合同记录和任务记录,然后把任务提交到线程池。线程池接收到任务后,如果当前运行线程数小于核心线程数,就创建核心线程执行;如果核心线程已满,就进入阻塞队列;如果队列也满了,并且线程数没到最大线程数,就创建非核心线程;如果线程数也到最大值,就触发拒绝策略。
这里最重要的是不能让用户请求线程无限等待,也不能让线程池无界堆积。合同审查任务通常涉及 OCR、条款抽取、规则匹配和模型调用,耗时不可控,所以要设置超时和任务状态。
public void submitAudit(String contractId) {
auditTaskRepo.create(contractId, "PENDING");
try {
executor.execute(() -> audit(contractId));
} catch (RejectedExecutionException e) {
auditTaskRepo.updateStatus(contractId, "REJECTED");
throw new BizException("AUDIT_SYSTEM_BUSY");
}
}
private void audit(String contractId) {
auditTaskRepo.updateStatus(contractId, "RUNNING");
try {
riskCheckService.check(contractId);
auditTaskRepo.updateStatus(contractId, "SUCCESS");
} catch (Exception e) {
auditTaskRepo.updateStatus(contractId, "FAILED");
}
}
如果任务很重,线程池只适合做本机调度,真正生产里更推荐用 MQ 或分布式任务系统削峰和解耦。
8. SQL 中 group by、having 和子查询怎么口述清楚
答案:
group by 用来分组,通常配合聚合函数使用,比如 count、sum、avg。where 是分组前过滤明细行,having 是分组后过滤聚合结果。子查询是把一个查询结果作为另一个查询的条件或数据来源,可以出现在 select、from、where 里。
比如合同系统里,要查询每个供应商近一年合同总金额超过 100 万,并且存在高风险合同的供应商,可以这样写:
SELECT supplier_id,
COUNT(*) AS contract_count,
SUM(amount) AS total_amount
FROM contract
WHERE sign_time >= '2025-01-01'
GROUP BY supplier_id
HAVING SUM(amount) > 1000000
AND supplier_id IN (
SELECT DISTINCT supplier_id
FROM contract_risk
WHERE risk_level = 'HIGH'
);
这里 where sign_time >= '2025-01-01' 是先过滤合同明细,group by supplier_id 是按供应商聚合,having SUM(amount) > 1000000 是对聚合后的供应商过滤,子查询负责筛出有高风险合同的供应商。
9. 数据库事务一致性怎么保障
答案:
事务一致性不是只靠数据库自动保证,业务也要设计正确。数据库提供 ACID,其中原子性保证要么都成功要么都失败,隔离性保证并发事务之间互不干扰到一定程度,持久性保证提交后数据不会丢。一致性最终要靠约束、事务边界和业务逻辑共同保证。
比如合同审批通过后,需要同时更新合同状态、生成履约计划、写审批日志。如果这三个操作有一个失败,就不能只更新一部分,所以要放在一个事务里。
@Transactional(rollbackFor = Exception.class)
public void approveContract(String contractId, String approver) {
Contract contract = contractRepo.selectForUpdate(contractId);
if (!"PENDING".equals(contract.getStatus())) {
throw new BizException("INVALID_CONTRACT_STATUS");
}
contractRepo.updateStatus(contractId, "APPROVED");
performancePlanRepo.createByContract(contractId);
auditLogRepo.insert(contractId, approver, "APPROVE");
}
同时,数据库层要加唯一约束、外键或业务唯一索引,防止重复生成履约计划。
ALTER TABLE performance_plan ADD UNIQUE KEY uk_contract_node(contract_id, node_type, due_date);
事务不是
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
本专栏聚焦 AI-Agent 面试高频考点,内容来自真实面试与项目实践。系统覆盖大模型基础、Prompt工程、RAG、Agent架构、工具调用、多Agent协作、记忆机制、评测、安全与部署优化等核心模块。以“原理+场景+实战”为主线,提供高频题解析、标准答题思路与工程落地方法,帮助你高效查漏补缺.
