去哪儿 AI 面试问题清单(AI 应用)
- 时间
- 2026-10
- 来源
- 牛客网
《面试题目》
- 操作系统中进程和线程的核心区别和联系是什么?
- 同一个进程里的两个线程同时修改同一个全局变量时会出现什么问题?
- 对外 HTTP 接口原本平均耗时 100 毫秒,突然升到 2 秒触发报警,你的排查思路是什么?
- 如果排查后确认是接口本身的逻辑问题导致耗时突增,你会从哪些具体的代码层面的点去定位根因?
- 锁竞争可能导致接口耗时突增,你会通过哪些具体的观测指标或者日志信息来确认当前接口耗时高确实是锁竞争导致的?
- 分享一个你做过的最具技术挑战、最值得深入探讨的后端开发项目,它要解决什么问题?
- 分享一段你最近半年内主动学习 AI 工具或大模型相关技术并尝试落地到实际开发场景的真实经历,包括你做了什么、最终落地效果如何。
- G1 垃圾收集器相较于 CMS 垃圾收集器做了哪些优化?它的可配置停顿时间是如何实现的?
- G1 垃圾收集器收集垃圾时会优先考虑收益更大的区域,它是怎么判断哪块区域收益更大的?
- 如果主线程开启了三个子线程调用接口,需要等三个子线程接口调用返回结果后才继续执行,你会使用哪些 Java 的工具来实现?
- 如果使用 CountDownLatch 来实现这个场景,当某一个线程出现异常终止、没有执行 countDown 操作时会出现什么情况?如何解决?
- 用 CompletableFuture 实现等待多个子线程返回,在出现异常时 allOf 方法是怎么处理的?与 CountDownLatch 的异常处理有什么区别?
- 现在如果用 Redis 做数据库的前置缓存,此时商品查询 QPS 为 5000,商品更新频率为分钟级,你怎么保持缓存和数据库一致性?
《参考解析》
共享变量并发问题
两个线程对同一个全局变量做「读—改—写」时会丢更新,因为自增这类操作不是原子的:编译后是取数、加一、写回三条指令,两条线程可能读到同一个旧值。根因不止原子性,还牵涉可见性(一个线程的写入可能停留在 CPU 缓存或寄存器,另一个线程读到旧值)和有序性(编译器与 CPU 可能重排指令)。解法按成本排序:优先把共享状态改成局部变量或线程封闭,其次用 AtomicInteger 这类 CAS 原子类,需要复合操作时用锁;同时把共享变量声明为 volatile 只能保可见性与禁止重排,不保证自增这类复合操作的原子性,这一点是高频追问。
接口耗时突增的排查思路
先分层定位再谈代码。第一步看影响面:全站还是单个接口、单机房还是多机房、什么时候开始、有没有发版或配置变更。第二步看上下游:把接口的耗时拆成自身计算、下游 RPC、数据库、缓存、外部服务几段,用链路追踪或埋点看哪一段涨了。第三步看资源与依赖:CPU、内存、GC、线程池活跃数与队列、连接池占用、下游的错误率与超时、慢查询和锁等待。区分「自身变慢」和「等待变慢」是关键——如果是等待,问题往往在依赖、线程池或锁上,而不是这段代码本身。
代码层定位与锁竞争的确认
代码层按几个方向查:新增或改动的逻辑里有循环、正则回溯、大对象序列化、一次性加载全量数据;缓存失效导致缓存击穿、批量请求打到底层;数据库出现无索引查询、大事务或者锁等待;线程池参数或队列不合理导致任务排队;同步块范围过大或锁对象被所有请求共享。确认是不是锁竞争,看四个证据:线程 dump 里大量线程 BLOCKED 或 WAITING 在同一把锁的监视器上、JFR 或 async-profiler 火焰图上有明显的 monitor 事件、锁的等待时间指标随 QPS 上升而放大、以及压测时降低并发后耗时明显回落。日志里可以打关键区段的耗时做旁证,但不能只看总耗时下结论。
G1 与 CMS 的对比
CMS 以老年代为主,标记清除会产生碎片,并发失败时会退化成串行 Full GC,停顿不可控。G1 把堆切成等大的 Region,按 Region 做增量回收,整体是「标记—整理」,能顺带压缩、避免碎片;它用并发标记(SATB)算每个 Region 的存活对象与回收收益,并维护一个按收益排序的回收计划,再结合用户设定的目标停顿时间(MaxGCPauseMillis)挑出一批 Region 组成回收集合,优先回收垃圾占比高、成本低的 Region——这正是第二个追问的答案:收益主要由 Region 的垃圾占比与预测的回收耗时决定。工程上还要知道 G1 的 remembered set 与写屏障开销、Humongous 对象对 Region 的占用,以及停顿目标设得过低会导致回收集合太小、回收跟不上分配速度。
多线程等待的工具选择与异常处理
可选的有 CountDownLatch、CyclicBarrier、CompletableFuture.allOf、以及 ExecutorService 的 invokeAll,还有阻塞队列加 Future 的组合,实际项目里最常用的是 CompletableFuture 编排加线程池。CountDownLatch 的坑正是题目问的:如果某个线程抛异常退出而没有执行 countDown,计数永远减不到零,主线程会一直阻塞甚至挂死。解决办法是每个任务都用 try/finally 保证 countDown,并给 await 设置带超时的版本;或者改用 CompletableFuture,它把异常当作结果的一部分,allOf 返回的 future 在任一子任务异常时会以 CompletionException 结束,get 时抛出,再配合 exceptionally 或 handle 做降级,不需要额外维护计数。两者最本质的区别是:CountDownLatch 只负责「数量到齐」,不传递子任务结果和异常;CompletableFuture 是带结果的编排原语,超时与异常兜底都在链路里。
QPS 5000、分钟级更新的缓存一致性
更新频率是分钟级、QPS 很高,说明读多写极少,方案要围绕「读路径不打穿数据库」设计。写路径用「先更新数据库、再删除缓存」而不是更新缓存,避免并发写导致脏数据;删缓存失败时用消息队列重试,或订阅 binlog 做补偿删除,把最终一致性兜住。读路径用 cache-aside:未命中时查库回填,同时给热点键加互斥或逻辑过期,防止缓存击穿把并发全压到数据库。再加三道防线:热点键的过期时间打散避免同时失效引发雪崩、空值与不存在的键写短 TTL 防穿透、给数据库连接池和查询加超时上限做最后保护。如果业务允许脏读窗口很短,这套组合足够;要做强一致就得引入版本号或延迟双删,但代价是复杂度明显上升。