淘宝闪购 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面试实战专栏 文章被收录于专栏

本专栏聚焦 AI-Agent 面试高频考点,内容来自真实面试与项目实践。系统覆盖大模型基础、Prompt工程、RAG、Agent架构、工具调用、多Agent协作、记忆机制、评测、安全与部署优化等核心模块。以“原理+场景+实战”为主线,提供高频题解析、标准答题思路与工程落地方法,帮助你高效查漏补缺.

全部评论

相关推荐

xdm&nbsp;早上喝奶茶差点喷出来。事情是这样的,我们班有个哥们儿,简称&nbsp;L,去年秋招拿了字节sp,专业方向是后端。我们当时都震惊:这哥们儿平时课上从来不发言,期末小组作业基本是划水的那种,刷题平台&nbsp;commit记录我点进去看过,绿格子稀稀拉拉。但他面试一路绿灯。一面二面三面&nbsp;hr&nbsp;面,全过,给的还是sp。当时班级群里恭喜他的、问他经验的、约饭的,热闹了一周。他说自己"运气好,准备充分"。我们都信了,直到三月初他入职。入职第二周开始,班里另一个进字节的同学W(在隔壁组的)开始跟我他的不对劲。一开始是写代码慢,后来写不出来,再后来是组里&nbsp;mentor&nbsp;让他fix&nbsp;一个简单&nbsp;bug&nbsp;都搞了一下午没动静。最离谱的是上周。W&nbsp;说他们大部门搞了个新人分享会,让新人讲一下自己负责模块的设计思路。L&nbsp;上去讲了&nbsp;20分钟,全程念稿子,问答环节别人随便问一个"那你这里为什么用&nbsp;Redis&nbsp;不用&nbsp;Memcached",他直接卡&nbsp;30秒说"这个我回去再确认一下"。会后他&nbsp;mentor&nbsp;直接找&nbsp;leader&nbsp;谈,leader&nbsp;找&nbsp;hr&nbsp;谈,hr调出了他面试录像,全程对比口型和回答节奏,发现他二三面有大量时长在偷偷看屏幕外(推测开了双机位&nbsp;AI&nbsp;答题)。(这段是&nbsp;W后来转述给我的,他自己也是听他组里同事八卦来的)昨天下班前,W&nbsp;告诉我L&nbsp;被辞退了,让他自己走,不走就走仲裁但会发函到学校。L&nbsp;现在已经回学校了,朋友圈仅三天可见。我说真的,我不是个心眼小的人,但是我看到这个消息的时候真的有种"嗯,挺好"的感觉。去年秋招我投字节后端,简历挂。我准备了八个月,背&nbsp;八股&nbsp;+&nbsp;刷&nbsp;500&nbsp;题&nbsp;+项目改了三版,连面试机会都没拿到。班里这哥们儿凭着一个外挂上岸,最后还是被甩出来了。不是说作弊就一定会被发现,但是当面试拿到的&nbsp;offer远远超出真实能力的时候,迟早会有这一天。试用期三个月不是给你过家家的,是真的要写代码、要在会议上回答问题、要扛需求的。我现在反而有点同情他。同情他相信"上岸就是终点"。发出来不是为了嘲笑谁,就是想说给那些正在被身边作弊上岸的同学搞得很&nbsp;emo&nbsp;的&nbsp;uu&nbsp;们听——别急,回旋镖很长,但它一定会回来。你继续刷你的题,写你的项目,背你的八股。该是你的迟早是你的,不是你的早晚还得还回去。xdm&nbsp;共勉。
牛客12588360...:我不想评论面试方式,作弊是绝对不对的,但是你八股加刷题也不过是个做题小子,他穿帮纯粹是他菜,你也没有高明到哪里去
点赞 评论 收藏
分享
不知道怎么取名字_:青花的都挂啊,这是要啥人呢
点赞 评论 收藏
分享
天乐敲代码呀:9咋了,多投投优越感就没了
点赞 评论 收藏
分享
评论
1
1
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务