面试知识

07:线上排障、项目话术与综合题库

10-Java并发与锁体系 面试知识整理。

07:线上排障、项目话术与综合题库

你学完本章后,能够把并发事故从“接口慢”还原成可验证的证据链:先保护业务,再用指标、线程栈、运行时记录和业务流水定位根因;最后把修复说成带边界、可恢复、可复盘的项目方案。

1. 并发故障总决策树:先证据,后结论

1.1 症状到根因的闭环

并发事故先按症状分流,但绝不按现象直接改代码。CPU(中央处理器)飙高要分清计算热点、CAS(比较并交换)自旋和垃圾回收;接口卡顿要分清队列等待、锁等待、下游等待和线程池耗尽;BLOCKED(阻塞)暴增要找同一把监视器和持有者;任务丢失则先核对接收、落库、提交、消费与确认五个事实点。每个结论都应能回答“哪份证据排除了其他解释”。

flowchart TD
    A[告警:延迟、错误或积压] --> B{优先保护核心交易?}
    B --> C[限流、摘流、暂停非核心任务]
    C --> D{主要症状}
    D -->|CPU 高| E[top -H + 连续 jstack]
    D -->|接口慢| F[线程池、队列、P99 与下游指标]
    D -->|BLOCKED 多| G[监视器地址、持有线程、锁内耗时]
    D -->|任务丢失| H[任务表、消息状态、幂等流水]
    D -->|内存涨| I[jcmd 与堆转储]
    E --> J{重复热点栈?}
    F --> K{活跃满且队列增长?}
    G --> L{等待关系成环?}
    H --> M{事实链在哪一跳断开?}
    I --> N{对象为何仍被持有?}
    J --> O[计算热点 / CAS 自旋 / GC]
    K --> P[线程池耗尽 / 下游慢 / 重试风暴]
    L --> Q[死锁 / 长临界区]
    M --> R[提交语义、幂等或恢复缺口]
    N --> S[上下文泄漏、无界队列或缓存]
    O --> T[止血、根治、压测与复盘]
    P --> T
    Q --> T
    R --> T
    S --> T

图解:正常路径先把流量和业务优先级稳定住,再采集互相印证的指标、栈和流水;失败路径是看到 CPU(中央处理器)高就扩容,或看到 BLOCKED(阻塞)就重启,证据随之消失。结论是:止血动作可以快,根因结论必须慢且可复现。

症状首看指标首条命令或证据常见根因立即止血根治与复盘
CPU(中央处理器)持续高核数占用、GC(垃圾回收)时间、吞吐top -H、连续 jstack(线程栈工具)热循环、CAS(比较并交换)自旋、序列化或 GC(垃圾回收)限流、关闭高代价功能降低竞争、分片、容量基线
接口卡顿P99(99 分位响应时间)、活跃线程、队列深度线程池指标、调用链下游慢、排队、锁内 I/O(输入输出)降级、超时、隔离有界队列、背压、依赖预算
BLOCKED(阻塞)暴增阻塞比例、锁等待时间jstack(线程栈工具)监视器文本热点锁、长临界区、死锁摘流、暂停热点操作缩短临界区、统一顺序、分片
任务丢失接收数、落库数、完成数、失败数任务表和消息状态先确认后落库、进程内状态丢失停止确认、重放状态机、幂等键、对账
内存增长堆曲线、晋升、队列长度jcmd(JVM 诊断命令)直方图ThreadLocal(线程本地变量)残留、无界队列限制入口、暂停导出生命周期清理、容量上限

**数据演绎一:线程池正反馈。**32 个工作线程、队列容量 500。下游从 80ms(毫秒)变为 2s(秒)后,理论完成能力由约 400 次/秒降到 16 次/秒;入口仍为 120 次/秒,队列每秒净增约 104 个,5 秒后积压 520 个并触发拒绝。若调用方把超时请求重试一次,入口变为约 224 次/秒,积压不再是单纯“线程不够”,而是超时—重试—积压的放大环。

热门面试题

  1. 问题(基础题):并发事故为什么不能只看一个告警指标?

    • 考点:相关性、因果、证据链。
    • 回答思路:说明单指标歧义,再给出指标、栈和业务事实的交叉方法。
    • 详细答案:同一个接口超时可能来自 CPU(中央处理器)饱和、队列排队、监视器竞争、数据库慢查询或网络超时。单看错误率只能知道结果,不能证明等待发生在哪里。我会把时间线对齐:先看流量、P99(99 分位响应时间)、线程池活跃数和队列深度,再采集至少三份间隔数秒的 jstack(线程栈工具),最后抽样关联订单号或任务号的状态流水。若队列持续增长且工作线程栈都在同一远程调用,才说明消费能力下降;若大量线程等待同一监视器,还要继续找持锁线程和锁内代码。这样修复动作对应明确因果,避免把扩容当成诊断。
    • 进阶追问:采集证据会不会影响线上?
    • 进阶回答:短时间线程转储通常可接受,但堆转储和高频探针可能有成本;应先做低侵入采样,并在业务低谷或隔离实例上完成重型采集。
  2. 问题(原理题):如何区分锁竞争和死锁?

    • 考点:等待关系、可推进性、监视器证据。
    • 回答思路:先给定义差异,再说明如何用线程栈构造有向图。
    • 详细答案:锁竞争表示多个线程等待同一资源,但持有者仍可能继续执行并最终释放;死锁要求等待关系形成闭环,环上每个线程都持有别人需要的资源且无法推进。线上我不会因为 BLOCKED(阻塞)数量高就宣布死锁,而是从线程栈的 lockedwaiting to lock 文本提取监视器地址,画出“线程持有资源、等待资源”的有向关系。只有甲持有 A 等 B、乙持有 B 等 A,或形成更长环,并且多份转储稳定存在,才下死锁结论。长临界区、锁内远程调用也会制造大量阻塞,但它们没有环,治理手段是缩短边界和隔离慢操作。
    • 进阶追问:重启能否解决死锁?
    • 进阶回答:重启只释放当前进程中的锁,属于可用性止血;代码中的反向加锁路径、重试副作用和数据恢复仍须修复并演练。
  3. 问题(项目题):WMS(仓储管理系统)库存接口卡顿时如何排查?

    • 考点:热点 SKU(库存单位)、库存事实、止血顺序。
    • 回答思路:从流量分布、线程池、锁栈、条件更新和流水依次回答。
    • 详细答案:我先按仓库和 SKU(库存单位)统计热点,比较 P99(99 分位响应时间)与库存条件更新失败率;热点集中时不先扩大整个集群,而先对该分片限流,保护其他仓库。随后看扣减线程池:若队列增长且线程栈在数据库调用,检查连接池、慢 SQL(结构化查询语言)和事务耗时;若大量 BLOCKED(阻塞)在同一库存锁,读取持锁栈,确认是否把远程通知放进锁内。最终库存事实必须以数据库条件更新、预占流水和幂等键为准,本地锁只减少单实例重复竞争。止血后把热点请求转为排队预占或提示稍后重试,并用流水对账确认没有超卖或漏释放。
    • 进阶追问:为什么不能只用本地锁保护库存?
    • 进阶回答:多实例各有自己的锁对象,本地互斥不能协调跨实例请求;共享数据库条件更新或共享原子存储才拥有最终裁决权。

2. 取证顺序:命令产生事实,不产生猜测

2.1 从轻到重的诊断工具链

取证顺序遵循“先低侵入、先可重复、再高成本”。先保存告警时间窗、发布版本、流量和线程池快照;再用 jstack(线程栈工具)连续取样定位线程状态与调用点;用 jcmd(JVM 诊断命令)确认 JVM(Java 虚拟机)进程、堆、类直方图和运行参数;JFR(Java 飞行记录器)用于把 CPU(中央处理器)、锁、分配和 I/O(输入输出)放入同一时间轴;Arthas(Java 诊断工具)只在权限和开销可控时观察运行方法;火焰图用于确认采样热点比例而不是单条栈;最后才导出堆转储。

顺序工具主要回答的问题关键产物不能单独证明什么
1指标与日志何时开始、影响多大时间窗、版本、流量、错误率线程卡在哪里
2jstack(线程栈工具)线程在等什么或跑什么多份线程转储内存为何增长
3jcmd(JVM 诊断命令)JVM(Java 虚拟机)状态与对象趋势信息、直方图、参数单请求的业务因果
4JFR(Java 飞行记录器)一段时间内的 CPU(中央处理器)、锁、分配事件时间线持久化事实是否正确
5Arthas(Java 诊断工具)线上方法参数与耗时受控观察结果全局性能比例
6火焰图哪条调用链累计消耗最多热点宽度业务状态机是否遗漏
flowchart LR
    A[告警时间窗] --> B[指标与版本快照]
    B --> C[三份 jstack]
    C --> D[jcmd 进程与堆信息]
    D --> E[JFR 时间窗录制]
    E --> F[Arthas 定点观察]
    F --> G[火焰图确认比例]
    G --> H[业务流水交叉验证]
    H --> I[根因与复盘证据包]

图解:正常路径是每一步缩小假设并保留可复查产物;失败路径是直接用 Arthas(Java 诊断工具)大范围跟踪高频方法,反而加重故障。结论是:工具顺序由侵入性和证据价值决定,不是由工具“强大”程度决定。

示例线程栈一:热点锁。

"http-8080-exec-17" #218 prio=5 tid=0x... state=BLOCKED
    at com.wms.StockService.reserve(StockService.java:86)
    - waiting to lock <0x000000076ab1> (a java.lang.Object)
    at com.wms.OrderService.create(OrderService.java:142)
"http-8080-exec-03" #204 prio=5 tid=0x... state=RUNNABLE
    at java.net.SocketInputStream.read0(Native Method)
    at com.wms.StockService.reserve(StockService.java:94)
    - locked <0x000000076ab1> (a java.lang.Object)
证据解读能下的结论仍需验证
state=BLOCKED17 号线程在等待进入监视器是监视器竞争,不是队列等待是否存在循环等待
waiting to lock等待对象地址为 0x...6ab1可与持有者按地址关联该对象对应哪段业务锁
03 号 locked03 号持有同一地址找到当前持有者持有者是否可推进
SocketInputStream.read0持有锁时在网络读高概率为锁内慢 I/O(输入输出)下游耗时与超时设置

逐行结论:这不是死锁证据,因为只有“17 等 03”,还没有“03 等 17”的闭环;但已经足够支持先把远程调用移出锁、设置下游超时并隔离线程池。多份栈若持续相同,说明竞争不是瞬时抖动。

示例线程栈二:死锁。

"reserve-1" state=BLOCKED
    - waiting to lock <0xA> (a java.lang.Object)
    - locked <0xB> (a java.lang.Object)
"cancel-1" state=BLOCKED
    - waiting to lock <0xB> (a java.lang.Object)
    - locked <0xA> (a java.lang.Object)
Found one Java-level deadlock:

逐行结论:reserve-1 持有 B 等 A,cancel-1 持有 A 等 B;两条边闭合后,工具摘要进一步确认 Java(编程语言)级死锁。根因不是“锁太多”这个泛化描述,而是预占和取消走了相反的资源排序。止血前先保存全文线程栈和请求流水,再摘流或重启单实例;根治为统一排序、缩短多资源临界区,或把最终裁决收敛为单条数据库条件更新。

flowchart LR
    T1[预占线程] -->|持有| B[库存锁 B]
    T1 -->|等待| A[订单锁 A]
    T2[取消线程] -->|持有| A
    T2 -->|等待| B
    A --> X[等待环成立]
    B --> X
    X --> Y[保存 jstack 与流水]
    Y --> Z[统一锁顺序或状态机]

图解:正常路径中预占和取消都先锁订单再锁库存;失败路径是两条路径排序相反,等待环一旦成立不会自行恢复。结论是:死锁的证据是资源等待环,不是“系统看起来不动”。

热门面试题

  1. 问题(基础题):为什么要连续采集多份 jstack(线程栈工具)?

    • 考点:瞬时状态、稳定性、采样证据。
    • 回答思路:解释单份栈的局限,说明间隔采样如何区分短暂与持续问题。
    • 详细答案:单份 jstack(线程栈工具)只是某一时刻的切片:一个线程可能正好在抢锁、发生一次短暂停顿或完成一次垃圾回收。连续采集三份以上、间隔数秒的转储,可以观察同一线程或同类线程是否稳定停在相同栈帧、同一监视器或同一下游调用。如果栈持续不变且队列深度同步增长,说明它是吞吐瓶颈;如果栈快速变化而 CPU(中央处理器)高,可能是计算热点或自旋;如果两个线程的等待—持有关系持续闭环,才是死锁。采样还应保留时间、实例、版本和流量,避免把已回滚或已迁移的实例混入结论。
    • 进阶追问:RUNNABLE(可运行)线程怎样进一步定位?
    • 进阶回答:用操作系统线程 CPU(中央处理器)占用与线程标识关联,再看连续栈是否重复;RUNNABLE(可运行)本身不等于正在耗尽 CPU(中央处理器)。
  2. 问题(原理题):JFR(Java 飞行记录器)和火焰图各解决什么问题?

    • 考点:时间线、采样比例、互补性。
    • 回答思路:比较记录范围、时间维度与使用边界。
    • 详细答案:JFR(Java 飞行记录器)记录一段时间内的运行事件,可把线程调度、锁等待、对象分配、垃圾回收和 I/O(输入输出)放在时间线上,适合回答“从发布后第几分钟开始、哪类事件同时变坏”。火焰图把采样栈按累计时间聚合,宽度代表比例,适合回答“CPU(中央处理器)究竟花在哪条调用链”。前者擅长因果时间窗,后者擅长热点结构。两者都不能替代业务流水:看到 JSON(数据交换格式)序列化热点,只能说明性能消耗,仍需确认它是否造成订单状态漏写或仅是非核心导出变慢。
    • 进阶追问:火焰图上宽的函数一定要优化吗?
    • 进阶回答:不一定;它可能是必要工作或被高流量正常调用。应结合吞吐、服务目标和可替代成本判断,而不是按宽度机械优化。
  3. 问题(项目题):如何用 jcmd(JVM 诊断命令)排查内存增长?

    • 考点:趋势、对象直方图、持有链、止血。
    • 回答思路:先确认增长形态,再从对象种类走到引用原因和业务入口。
    • 详细答案:我先保存堆使用曲线、GC(垃圾回收)后基线和队列长度,区分短期峰值与每轮回收后仍上升的泄漏趋势;然后用 jcmd(JVM 诊断命令)采集类直方图,比较两次间隔采样的对象数量和字节增量。若导出行对象、字节数组或任务包装对象持续增加,就关联导出批次、队列深度和完成率;若 ThreadLocal(线程本地变量)相关对象增长,则检查线程池工作线程是否在 finally(最终清理)清理上下文。只有确认对象为何被引用后才导出堆转储,避免把大文件采集放在最脆弱时刻。止血是限制并发、缩小批次和暂停非核心导出,根治是有界队列、流式写出和明确清理责任。
    • 进阶追问:对象直方图能直接证明泄漏根因吗?
    • 进阶回答:不能,它只显示种类和数量;根因还要看引用链、对象字段和业务生命周期。

3. 线程池与并发设计:把故障限制在故障域内

3.1 耗尽、背压与本地正确性的边界

线程池不是“线程越多越快”的开关,而是并发预算、排队策略和故障域。每类工作必须有独立的线程数、队列上限、超时、拒绝策略和业务降级:扣库存、支付回调、异步导出、通知发送不能共用一个无界池。上下文也不能凭 ThreadLocal(线程本地变量)自然传播;线程复用会让上一个请求的租户、追踪标识或安全身份串到下一个请求,必须在提交时显式捕获,在执行后 finally(最终清理)清理。

flowchart TD
    A[下游变慢] --> B[工作线程占满]
    B --> C[队列持续增长]
    C --> D[排队时间超过超时]
    D --> E[调用方重试]
    E --> F[入口流量更大]
    F --> B
    C --> G[内存增长]
    G --> H[GC 停顿扩大]
    H --> D
    I[有界队列 + 超时 + 拒绝] --> J[背压或降级]
    J --> K[保护核心线程池]

图解:正常路径在容量接近上限时把压力反馈给上游或降级;失败路径由无界队列隐藏过载,直到超时重试和内存增长共同击穿服务。结论是:排队不是免费缓存,队列长度就是延迟和内存承诺。

指标健康解释危险组合对应动作
活跃线程数接近核心数但可回落长时间等于最大线程数查栈、降级下游
队列深度短峰后回落持续单调增长限流、拒绝、扩容消费者
任务等待时间小于业务预算等待时间大于执行超时缩短队列或分池
拒绝数偶发且可解释突增并伴随重试返回可识别降级结果
完成率接近接收率接收远大于完成查确认与任务状态机

**数据演绎二:队列内存预算。**导出任务平均携带 200KiB(千字节)参数和结果引用,若队列没有上限而积压 10,000 个任务,仅任务对象就约 1.95GiB(吉字节),还未包含中间行数据。把队列设为 200、每任务只保留任务标识,超过时返回“稍后生成”,内存上限可从数 GiB(吉字节)缩到数十 MiB(兆字节),并把等待转成可观测的业务状态。

热门面试题

  1. 问题(基础题):线程池为什么要使用有界队列?

    • 考点:背压、内存预算、服务目标。
    • 回答思路:说明无界队列隐藏过载的机制,再说明有界后的业务处理。
    • 详细答案:无界队列会让提交方误以为任务被“接受”,实际只是把延迟和对象堆积从调用方转移到服务内存。下游变慢时,工作线程先满,随后任务无限排队;排队时间超过业务超时后调用方重试,队列继续变长,最终引发 OOM(内存溢出)或长时间 GC(垃圾回收)。有界队列把容量预算显式化:达到上限时执行拒绝策略,核心交易可同步返回繁忙并由客户端退避,非核心通知可合并或丢弃可重建副本,异步导出则落任务表等待后续调度。这样过载从不可控的进程故障变成可观察、可恢复的业务状态。
    • 进阶追问:有界队列容量如何定?
    • 进阶回答:按可接受排队时间乘稳定吞吐估算,并受单任务内存、堆预算和下游容量约束;不是直接使用一个通用默认值。
  2. 问题(原理题):为什么本地并发正确不等于分布式一致?

    • 考点:进程边界、共享事实、故障恢复。
    • 回答思路:区分进程内互斥与跨实例最终裁决,再给库存例子。
    • 详细答案:本地锁、原子变量和线程安全容器只协调一个 JVM(Java 虚拟机)地址空间内的线程;多实例部署后,每个进程都有独立内存和独立锁,两个实例可以同时认为自己“独占”某个 SKU(库存单位)。即使单实例代码没有数据竞争,也无法抵抗进程崩溃、网络分区、重复消息和重试。分布式正确性必须把最终事实放在可共享、可持久化、可恢复的位置,例如数据库条件更新、唯一约束、幂等流水和状态机;本地并发控制只能降低同实例竞争、保护短暂缓存或减少无意义重复。面试中要主动讲清这条边界,避免把 synchronized(同步锁)包装成库存一致性方案。
    • 进阶追问:分布式锁能完全替代数据库条件更新吗?
    • 进阶回答:不能。锁可能失效、续约失败或客户端停顿;最终写入仍要由版本、条件或唯一约束拒绝非法状态。
  3. 问题(项目题):如何防止 ThreadLocal(线程本地变量)上下文串请求?

    • 考点:线程复用、上下文所有权、清理责任。
    • 回答思路:说明串请求原因,给出提交、执行、清理三段式协议。
    • 详细答案:线程池复用工作线程,ThreadLocal(线程本地变量)值绑定的是线程而非一次请求;若请求甲写入租户、用户或追踪标识后未清理,请求乙复用该线程就可能继承旧值,造成日志串号、权限误判甚至数据越权。我的协议是:入口只在明确边界创建不可变上下文;提交异步任务时显式复制必要字段,不让任务隐式读取提交线程的上下文;工作线程在 try(尝试)/finally(最终清理)中设置并无条件 remove(移除)所有本次值。对第三方库的线程本地状态还会在任务装饰器统一清理。排障时从错误请求的线程名、追踪标识、租户字段和任务提交时间交叉验证,不能只因日志相同就下结论。
    • 进阶追问:为什么只调用 set(设置)空值不够?
    • 进阶回答:空值可能仍保留条目或不能清理其他库的值;明确 remove(移除)才表达生命周期结束。

4. 项目话术:从设计到失败恢复

4.1 WMS(仓储管理系统)库存防超卖

**背景与量级。**WMS(仓储管理系统)高峰每秒 1,200 个预占请求,热点仓库的单个 SKU(库存单位)可占 18%;约束是可售量绝不能为负,重复请求和跨实例并发必须可恢复。错误方案是只用本地锁扣减,或先查库存再普通更新:多实例会各自放行,读写之间也会被插入。

**设计与时序。**本地按“仓库—SKU(库存单位)”做短时分片锁,只保护同实例的组装和去重;数据库以 available >= quantity 的条件更新作为最终裁决,并写入预占流水唯一键。正常路径是验参—幂等查流水—条件更新—写预占状态—异步通知;失败路径中,条件更新 0 行表示库存不足而不是重试风暴,写库成功后通知失败则由流水扫描补发。监控包括热点分片、条件更新失败率、预占超时释放数和流水差异;降级是热点 SKU(库存单位)排队或限购,而不是放宽条件。

sequenceDiagram
    participant C as 客户端
    participant A as WMS 应用
    participant D as 库存数据库
    participant N as 通知任务
    C->>A: 预占请求与幂等键
    A->>A: 本地分片去重
    A->>D: 条件更新 available >= qty
    alt 更新成功
        D-->>A: 影响 1 行
        A->>D: 写预占流水
        A-->>C: 预占成功
        A->>N: 异步通知
    else 库存不足或重复
        D-->>A: 0 行或唯一冲突
        A-->>C: 返回既有结果或不足
    end

图解:正常路径中数据库条件更新先决定可售事实,通知是可恢复副作用;失败路径中重复请求命中流水、通知失败可重放,任何本地锁失效都不会绕过数据库。结论是:本地锁用于减压,条件更新和流水用于正确性。

维度错误方案目标方案可观察证据
跨实例互斥本地锁条件更新与唯一流水影响行数、唯一冲突
热点全局锁仓库—SKU(库存单位)分片热点分片等待
重复重复扣减幂等键返回既有结果重复率、命中率
崩溃恢复内存标记丢失预占流水与超时释放超时释放、对账差异

**数据演绎三。**可售 100 件,实例甲、乙各收到 70 件请求。普通“先查再写”都读到 100,最终可能写成 30 并实际承诺 140;条件更新先由甲扣至 30,乙的 available >= 70 影响 0 行,承诺仍为 70。若甲在扣减后宕机,预占流水的过期扫描释放未支付数量,恢复不依赖甲内存。

热门面试题

  1. 问题(基础题):库存条件更新为什么能防超卖?

    • 考点:原子条件、共享裁决、影响行数。
    • 回答思路:把判断和扣减放到同一原子写入中解释。
    • 详细答案:条件更新把“库存是否足够”和“扣减数量”放进数据库的一次原子执行,例如仅在可售量不少于请求量时更新。并发请求不再依赖应用层先读到的旧值,而是竞争同一个共享事实;只有一个请求能使条件成立,另一个看到影响行数为 0 后返回库存不足或既有幂等结果。它防的是跨实例的超卖,不等于完成所有业务:仍要有预占流水记录来源、过期释放处理未完成订单、唯一键处理重复请求,并用对账发现异常人工操作。应用内的分片锁可以减少热点冲突,但不是最终保证。
    • 进阶追问:影响 0 行能否都当库存不足?
    • 进阶回答:不能;还要区分版本过期、记录不存在、状态已关闭等条件,并返回准确业务结果。
  2. 问题(原理题):库存为什么需要所有权和分片?

    • 考点:共享面、热点、吞吐与不变量。
    • 回答思路:说明分片如何减少无关竞争,以及不能拆开的不变量。
    • 详细答案:库存不变式通常以仓库和 SKU(库存单位)为边界:同一组合的可售、冻结和预占需要协调,不同组合不应被同一把全局锁串行。按稳定业务键分片让无关商品并行,同时每个分片有明确的更新所有权和监控维度。分片不是随意把字段拆开;若一次操作必须同时改变可售与冻结,就必须在同一事务或同一状态转换中保持原子。热点 SKU(库存单位)仍会串行,这是业务事实而非实现缺陷,治理是限购、排队、合并请求或预分配,而不是让多把锁突破同一库存上限。
    • 进阶追问:分片越多越好吗?
    • 进阶回答:不是;过细会增加路由、跨分片协调和数据倾斜成本,应按热点、容量和不变量选择。
  3. 问题(项目题):库存事故后的复盘要写什么?

    • 考点:事实范围、恢复、预防验证。
    • 回答思路:按影响、时间线、根因、止血、数据修复和预防组织。
    • 详细答案:复盘先用订单、预占流水和库存变更记录圈定影响范围,明确是超卖、少卖、重复预占还是通知延迟;随后按时间线记录流量、版本、告警、止血人与决策。根因必须落到可验证机制,例如某路径绕过条件更新或预占过期任务被错误关闭,而不是“并发太高”。修复数据时以流水为准生成补偿清单,人工确认不可自动逆转的订单;预防措施包括覆盖双实例并发、重复回调、宕机恢复和热点压测的自动化用例,以及条件更新失败率、流水差异和超时释放的告警阈值。最后写清哪些动作已验证、哪些依赖后续演练,避免复盘停在口号。
    • 进阶追问:如何验证修复不是偶然通过?
    • 进阶回答:在多实例和故障注入下重复压测,并对每轮结果按流水校验库存守恒与幂等性。

4.2 异步导出 OOM(内存溢出)与线程池隔离

**背景与量级。**运营一次导出可达 300 万行,历史实现把查询结果、格式化对象和压缩字节全部放入内存,并与订单线程池共用 64 个工作线程。错误方案是加大堆或把队列改为无界:峰值暂时延后,却把核心接口拖入 GC(垃圾回收)停顿。约束是导出可延后、订单不可受影响,文件必须可追踪、可重试、可取消。

**设计、正常与失败时序。**导出请求只创建任务表,导出专属线程池从任务表领取;游标或分页每次读取 2,000 行,边读取边写临时文件,上传后原子标记成功。失败时不确认任务,记录断点或清理临时文件后重试;取消信号在批次边界检查。监控任务年龄、队列深度、单批内存、写入速率和失败原因;当积压超过 200 个任务时返回稍后生成,核心池始终隔离。

flowchart LR
    A[导出请求] --> B[任务表]
    B --> C[导出专属有界线程池]
    C --> D[每批读取 2000 行]
    D --> E[流式写临时文件]
    E --> F[上传对象存储]
    F --> G[标记成功并通知]
    D --> H{内存或下游异常?}
    H -->|是| I[记录失败、清理或断点重试]
    H -->|否| E
    J[订单线程池] -.隔离.-> C

图解:正常路径只在内存中保留一个批次;失败路径把导出和订单混池、或一次性收集全量数据,队列和对象存活时间一起膨胀。结论是:异步不等于无限接收,隔离必须同时隔离线程、队列和内存预算。

项目项旧实现新实现故障时表现
数据承载全量列表批次流式写出堆基线稳定
执行资源与订单混池导出专属池核心接口可用
队列无界200 个有界明确背压
恢复内存状态任务表和文件状态重启可继续

**数据演绎四。**单行对象、格式化字符串和临时数组按 1.5KiB(千字节)估算,300 万行约需 4.29GiB(吉字节),超过 4GiB(吉字节)堆后必然频繁 GC(垃圾回收);改为 2,000 行批次,即使放大到每行 3KiB(千字节),工作集约 5.86MiB(兆字节),再加 8 个并发任务仍在可预算范围。

热门面试题

  1. 问题(基础题):异步导出为什么仍会导致 OOM(内存溢出)?

    • 考点:排队对象、批量读取、生命周期。
    • 回答思路:说明异步把执行延后但不自动减少对象数量。
    • 详细答案:异步只改变调用线程是否等待,不会限制提交速度、队列长度和任务携带的数据量。若每个任务闭包持有完整查询条件、结果列表或用户上下文,无界队列会让这些对象在等待期间长期存活;执行时再一次性读取全量数据,峰值叠加更明显。正确做法是任务只持久化最小标识和参数,执行端以小批次流式读取、写出和释放,队列、并发数和单批大小都根据堆预算设上限。导出属于可延后工作,超过预算应返回任务排队或提示稍后,不该挤占支付、库存等核心资源。
    • 进阶追问:增大堆能否作为根治?
    • 进阶回答:只能争取诊断时间;若对象增长没有上界,任何有限堆都会耗尽,且更长的 GC(垃圾回收)暂停可能扩大影响。
  2. 问题(原理题):线程池隔离的核心价值是什么?

    • 考点:故障域、资源预算、服务等级。
    • 回答思路:从资源被慢任务占满的传播路径回答。
    • 详细答案:隔离不是为了配置更多线程,而是防止一种工作负载把另一种工作负载的执行机会、队列空间和内存预算全部占掉。导出、通知和报表的延迟通常可接受秒级或分钟级,订单创建和支付回调却有严格服务目标;共用池时导出下游慢会让核心请求排在导出后面,超时重试又反向扩大压力。独立池使每类任务有独立并发数、队列、超时、拒绝和监控,故障只在本域内排队或降级。隔离后仍要防止数据库、网络和堆等共享依赖被同时打满,因此它是基础边界,不是唯一手段。
    • 进阶追问:线程池越细越好吗?
    • 进阶回答:不是;过多线程池增加调度、配置和容量管理成本,应按服务等级、依赖类型和失败传播关系划分。
  3. 问题(项目题):导出失败如何做到可恢复?

    • 考点:任务状态机、幂等、文件边界。
    • 回答思路:说明状态、断点、临时文件和重试条件。
    • 详细答案:导出任务在持久化表中记录创建、运行、成功、失败和取消状态,并记录参数版本、开始位置、尝试次数和文件标识。工作者领取任务时通过租约或条件更新取得执行权,流式写入临时文件;只有上传完成并校验成功后才把任务标记成功并暴露下载地址。进程崩溃、网络失败或磁盘异常时,过期租约允许其他工作者接管;临时文件按任务标识清理或从可验证断点继续。重试必须使用相同任务标识,避免产生多份对外可见文件;对不可重放的数据则固定查询快照或导出版本,保证用户重试不会读到混合数据。
    • 进阶追问:取消任务要杀线程吗?
    • 进阶回答:不应强杀;在批次边界检查取消标志,安全关闭流和临时资源,再把状态写为取消。

4.3 Runner(执行器)调度、租约、停止与恢复

**背景与约束。**Runner(执行器)执行跨境物流同步、对账和重试任务;任务可运行数分钟,部署会滚动重启,多个实例不能重复长期执行同一任务。错误方案是把“正在运行”只放在内存 Map(映射接口)或只依赖线程中断:重启后所有权消失,外部副作用可能重复。

**设计。**任务表保存状态、版本、租约持有者、租约过期时间、心跳和幂等业务键。领取时用条件更新把 READY(就绪)变为 RUNNING(运行中)并写入租约;正常路径周期性续租、检查停止标记、执行可重试步骤并最终提交结果。失败路径是实例失联后租约过期,其他实例接管;旧实例恢复时因续租失败必须停止,不得继续提交。停止分为停止接新任务、请求协作取消、等待期限、超时转人工处置;任何副作用以业务幂等键和状态机兜底。

stateDiagram-v2
    [*] --> READY: 创建任务
    READY --> RUNNING: 条件领取 + 租约
    RUNNING --> RUNNING: 心跳续租
    RUNNING --> SUCCESS: 幂等提交完成
    RUNNING --> RETRY: 可恢复失败
    RETRY --> READY: 到达重试时间
    RUNNING --> STOPPING: 收到停止请求
    STOPPING --> READY: 安全点释放租约
    RUNNING --> EXPIRED: 实例失联且租约过期
    EXPIRED --> READY: 其他实例接管

图解:正常路径由租约界定短期执行权,完成由持久化状态确认;失败路径中旧实例失联、租约到期、接管实例继续,旧实例恢复后不能凭本地线程继续写。结论是:中断是协作信号,租约和幂等才是跨实例恢复协议。

情况判定证据安全动作最终保障
正常执行租约未过期、心跳递增续租并推进步骤状态机提交
发布停止停止标志不再领新任务、协作退出任务回到可领取
实例宕机心跳停止、租约过期其他实例接管幂等业务键
旧实例恢复续租失败放弃本地执行条件更新拒绝旧写

**数据演绎五。**租约为 60s(秒)、每 20s(秒)续租。实例在第 25s(秒)失联,最迟第 60s(秒)后可接管;为了防止网络抖动造成双执行,接管者提交外部动作仍携带任务幂等键。若执行平均 8 分钟,不能只依赖 60s(秒)租约本身,而要把步骤拆成可重试的小单元并持续证明当前所有权。

热门面试题

  1. 问题(基础题):租约和分布式锁有什么区别?

    • 考点:所有权期限、失联恢复、提交边界。
    • 回答思路:说明租约的过期语义和为什么仍需幂等。
    • 详细答案:租约把执行权表达为“某实例在某个过期时间前可以推进任务”,核心价值是持有者失联后系统无需人工清理就能恢复;它通常通过任务表字段和条件更新实现,不一定需要单独的锁服务。分布式锁也可表达排他访问,但若没有可靠过期、续约和失效后的写入保护,仍会发生旧持有者恢复后继续执行。无论名称是什么,外部副作用都必须带幂等键或版本检查:租约只能减少同时执行窗口,不能撤销已经发出的网络请求。对 Runner(执行器)而言,任务状态、租约、步骤结果和补偿记录共同构成可恢复协议。
    • 进阶追问:租约过短有什么风险?
    • 进阶回答:正常长停顿可能导致误接管;应结合最长暂停、心跳和步骤耗时设置,并让旧持有者在续租失败后停止提交。
  2. 问题(原理题):为什么停止 Runner(执行器)不能直接杀线程?

    • 考点:中断协作、不变量、副作用恢复。
    • 回答思路:说明任意点终止如何破坏状态,再给分阶段停止协议。
    • 详细答案:强制终止可能发生在任务已经领取、外部请求已发送、结果尚未落库的任意指令点,导致租约、状态和副作用无法对应;如果同时释放本地锁,其他线程还可能看到半更新内存状态。安全停止先关闭领取入口,使新任务不再开始;对运行任务写停止请求并通过中断或轮询让它在批次、网络调用返回或事务边界退出;在 finally(最终清理)清理上下文并更新任务状态。超过期限的不可中断任务保留证据并人工处置,租约到期后由恢复流程接管。关键是把“停止进程”与“业务任务已安全结束”分开。
    • 进阶追问:收到中断异常后应怎样处理?
    • 进阶回答:若本层不能完整处理取消,应恢复中断标记并向上返回;不能吞掉异常后继续无限循环。
  3. 问题(项目题):Runner(执行器)如何避免重复执行物流推送?

    • 考点:幂等键、步骤状态、外部确认。
    • 回答思路:说明重复来源和每层的拒重责任。
    • 详细答案:重复可能来自消息至少一次投递、租约接管、网络超时后不知道对方是否收到、或发布重启。任务领取用条件更新限制同一时刻的执行者,但不能保证旧实例绝不发出请求;因此每次物流推送使用稳定业务键,例如运单号加事件版本,外部接口若支持则传入该键,本地也记录请求、响应和最终状态。超时后不盲目重发,而是先按幂等键查询或等待对账;确需重试时沿用同一键。这样重复执行被转化为同一事实的重复投递,最终只有一次状态跃迁可见,审计也能还原每次尝试。
    • 进阶追问:外部系统不支持幂等键怎么办?
    • 进阶回答:尽量查询对方可识别状态;若仍无法确认,只能把不确定结果显式进入人工或对账流程,不能假装做到精确一次。

4.4 支付回调幂等与锁边界

**背景与约束。**支付渠道会重复、乱序回调,峰值可达每秒 800 次;支付状态、账务和通知必须可追溯,回调处理不能因同一订单锁竞争拖垮整个接口。错误方案是拿订单本地锁后验签、查库、调用外部服务,或只用内存 Set(集合接口)去重。

**设计。**验签和基础格式校验在锁外;以渠道事件号或支付流水号为幂等键,通过数据库唯一约束争夺首处理权;状态转换、账务记录与 Outbox(发件箱)同事务提交。正常重复回调返回已接收,不重复记账;乱序回调按状态机拒绝非法倒退;事务提交后通知失败由 Outbox(发件箱)重试。局部订单锁只保护极短的内存聚合,绝不包住远程调用或数据库慢操作。

sequenceDiagram
    participant P as 支付渠道
    participant A as 回调服务
    participant D as 支付数据库
    participant O as Outbox
    P->>A: 回调事件与流水号
    A->>A: 验签、格式校验
    A->>D: 唯一键 + 状态事务
    alt 首次且状态合法
        D->>O: 同事务写事件
        D-->>A: 成功
        A-->>P: 已接收
    else 重复或乱序
        D-->>A: 已有结果或拒绝跃迁
        A-->>P: 幂等响应
    end
    O-->>A: 异步可靠通知

图解:正常路径先用共享事务决定首次状态,再异步传播副作用;失败路径中重复、乱序和通知失败都有持久化落点。结论是:锁边界保护短暂本地不变量,幂等与事务边界保护支付事实。

风险错误处理正确边界证据
重复回调内存 Set(集合接口)唯一键与状态机唯一冲突、重复率
乱序事件覆盖状态单向状态转换非法跃迁数
通知失败回滚已支付Outbox(发件箱)重试事件待发数
锁竞争锁内远程调用锁外验签与通知BLOCKED(阻塞)栈

**数据演绎六。**同一支付流水在 30 秒内收到 4 次成功回调和 1 次超时重投。唯一键使 1 次事务写入支付成功、账务和 Outbox(发件箱),其余 4 次读取既有结果并返回成功;若通知消费者失败 3 次,Outbox(发件箱)仍保留一条事件,重试不影响已经提交的账务。

热门面试题

  1. 问题(基础题):支付回调幂等键怎样选择?

    • 考点:稳定性、唯一性、渠道语义。
    • 回答思路:优先使用渠道事件标识,说明订单号不足的场景。
    • 详细答案:幂等键必须代表“同一个外部事实”,优先选渠道提供且可重复携带的事件标识或支付流水号;订单号往往不足,因为一个订单可能有多次支付尝试、退款或补单。键写入带唯一约束的持久化记录,并与业务类型、渠道和版本组合,避免不同事件误冲突。处理时先验签,再尝试创建该事实;冲突则读取既有处理结果,不再重复记账。若渠道只提供不稳定字段,应与金额、商户号、时间窗口等建立可审计映射,并把不确定匹配交给对账,不应依赖本地缓存猜测。
    • 进阶追问:幂等响应为什么也要返回成功?
    • 进阶回答:对已经成功处理的重复事件返回可识别成功可停止渠道重投;但非法签名或状态冲突应返回与协议匹配的可诊断结果。
  2. 问题(原理题):为什么 Outbox(发件箱)适合支付通知?

    • 考点:事务边界、提交后传播、恢复。
    • 回答思路:说明双写问题和把业务状态、待发事件同事务持久化的价值。
    • 详细答案:支付状态已提交而通知发送失败、或通知先发而事务回滚,都是典型双写不一致。Outbox(发件箱)把“支付状态和账务已变更”与“需要发送的事件”写在同一个本地事务中:提交成功后事件必可被扫描到,发送失败只影响传播时效而不影响支付事实;消费者以事件标识幂等处理,重试不会重复产生副作用。它不承诺所有外部系统瞬时一致,而是把不可避免的提交后失败转为可观察、可重试、可对账的状态。线上要监控待发事件年龄、发送失败和消费滞后。
    • 进阶追问:Outbox(发件箱)能消除所有重复吗?
    • 进阶回答:不能;发送和消费通常仍是至少一次,需要生产者、消费者和业务写入都具备幂等性。
  3. 问题(项目题):支付接口出现 BLOCKED(阻塞)暴增怎么处理?

    • 考点:锁边界、证据、交易保护。
    • 回答思路:先固证和摘流,再找锁对象与慢调用。
    • 详细答案:我先按实例采集连续 jstack(线程栈工具),确认 BLOCKED(阻塞)线程是否等待同一订单锁,以及持有者栈是否停在验签外部依赖、数据库或通知调用;同时把重复回调限流并保留渠道事件号,避免重试加剧拥塞。若证据显示锁内有远程调用,先在灰度中把通知切换为 Outbox(发件箱)异步发送,并收紧锁内为读取局部状态和短暂状态转换;若是热点订单异常重放,则对该幂等键串行或拒绝重复,而不是扩大全局线程池。恢复后核对唯一键、账务和通知事件,确保止血没有造成漏记。根治测试覆盖同订单重复、下游慢和多实例回调。
    • 进阶追问:是否应该对所有订单加公平锁?
    • 进阶回答:不应默认如此;公平锁增加调度成本,支付最终正确性应由事务和状态机保证,局部锁只在测得的短临界区使用。

4.5 IoT(物联网)报警风暴:分片、背压与原始事实

**背景与约束。**设备网络抖动时,报警量可能从每分钟 2 万条跳到 200 万条。错误方案是每条都同步通知、在全局 Map(映射接口)累积所有设备状态,或在共享池无限重试;结果是通知压垮、内存增长,原始报警反而丢失。

**设计。**先可靠保存原始事件,再按租户—设备—报警码稳定分片;同一键在窗口内聚合、去重和计数,不同分片并行。通知链路有独立有界队列和背压:队列接近上限时降级为摘要或延迟通知,绝不丢原始事实。窗口状态设置条目、时间和样本上限;多实例通过消息分区或共享窗口存储协调。监控接入速率、分片倾斜、聚合命中率、通知滞后、丢弃的派生通知和原始落盘成功率。

flowchart LR
    A[设备报警] --> B[原始事件持久化]
    B --> C[按租户-设备-报警码分片]
    C --> D[窗口聚合与去重]
    D --> E{通知池队列低于阈值?}
    E -->|是| F[发送详细通知]
    E -->|否| G[背压:摘要或延迟]
    G --> H[保留原始事件]
    F --> I[通知结果与重试]
    H --> I

图解:正常路径让原始事实和派生通知分开,分片后的窗口聚合削峰;失败路径把所有报警压入同一内存集合并同步通知,队列、内存和重试同时增长。结论是:可降级的是通知,不可丢的是原始事实。

目标机制超限行为复盘指标
原始事实不丢先持久化暂停派生通知落盘成功率
防热点串行稳定键分片热点单独限速分片倾斜度
控制内存窗口三上限淘汰派生状态条目数、字节数
保护通知有界队列背压摘要、延迟通知滞后、拒绝数

热门面试题

  1. 问题(基础题):报警风暴为什么先存原始事件?

    • 考点:事实与派生、恢复、降级边界。
    • 回答思路:区分不可重建输入和可重算通知。
    • 详细答案:原始报警是设备在某时刻产生的业务事实,后续聚合规则、通知渠道和阈值都可能变化;若先在内存中聚合再落盘,进程宕机、内存上限或规则缺陷会让事实永久丢失。先持久化原始事件后,窗口聚合、去重和通知都成为可重算的派生过程:通知池过载时可以发摘要、延迟或暂停,而恢复后仍能按原始事件重放。这样系统在极端流量下明确牺牲的是通知时效,不是审计和安全事实。持久化链路也要有容量与失败告警,不能把“先存”理解为无限写入。
    • 进阶追问:原始事件重复怎么办?
    • 进阶回答:保留稳定事件标识或可审计去重键,允许至少一次接入,再由下游幂等聚合和查询处理重复。
  2. 问题(原理题):分片与背压怎样配合?

    • 考点:局部顺序、并行度、压力反馈。
    • 回答思路:先解释稳定键分片,再说明背压避免慢分片拖垮全局。
    • 详细答案:分片按租户、设备和报警码等稳定键把同一实体的事件路由到同一处理序列,保留局部顺序并让不同实体并行;它减少共享锁竞争,但热点设备仍可能让某一分片积压。背压则在该分片或通知池接近容量时限制继续接收、合并窗口或降级摘要,把压力尽早反馈给入口而不是无限占用内存。二者结合后,局部热点被局部限制,其他分片仍可推进;没有分片的背压会让全局一起降级,没有背压的分片只是把无界积压分散到多个队列。
    • 进阶追问:分片键能随意改变吗?
    • 进阶回答:不能;变更会影响同键事件的顺序和状态归属,需要版本化路由并在迁移期保证旧新规则可解释。
  3. 问题(项目题):报警风暴发生时如何止血与复盘?

    • 考点:分级降级、容量、数据完整性。
    • 回答思路:先保护原始链路,再逐级缩减派生工作,最后量化缺口。
    • 详细答案:止血顺序是先确认原始事件落盘成功率和存储余量,再把通知切换为按设备或分钟的摘要,暂停低优先级规则计算,必要时对异常租户限速;不直接删除队列或清空内存状态,因为那会丢失复盘依据。期间持续记录入口速率、热点分片、队列年龄、摘要覆盖量和落盘失败,恢复后按时间窗重算聚合并补发关键通知。复盘要解释触发源是设备批量重连、规则变更还是攻击流量,量化原始事实是否完整、哪些派生通知延迟、背压何时生效,并把容量预算、演练和阈值写入改进项。
    • 进阶追问:为什么不直接扩容通知消费者?
    • 进阶回答:若下游渠道本身限速,扩容只会制造更多失败和重试;应先做合并、限速与优先级控制。

4.6 项目案例的共用边界

五个案例的共用结论是:进程内锁、线程池和容器负责局部效率与隔离;条件更新、唯一键、租约、状态机、持久化流水和对账负责跨实例正确性与恢复。该小节用于结束上一处知识标记,综合题库不重复章节型六字段题。

5. 设计思想、复盘模板与章节索引

设计思想要减少什么典型落地反例
减少共享多线程改同一状态不可变快照、局部变量全局可变 Map(映射接口)
分片无关键相互等待仓库—SKU(库存单位)、设备键一把全局锁
所有权谁能改不清晰租约、状态机、版本重启后内存标记丢失
不可变快照半完成对象发布配置一次替换原地修改共享配置
排队与背压无限接收压力有界队列、拒绝、摘要无界队列掩盖过载
分治与故障域非核心拖垮核心专属线程池、独立限额导出与支付混池

**事故复盘模板。**影响范围与服务目标;精确时间线和版本;症状、指标、命令、线程栈和流水证据;直接根因与系统性原因;止血动作、风险和数据校验;根治改动、压测与故障演练;监控阈值、负责人和验证日期。每条结论都标明证据来源,未知项写成待验证假设而不是事实。

章节先复习什么何时回到本章
01-线程生命周期与 Java(编程语言)内存模型状态、中断、安全发布线程状态和上下文异常
02-volatile(可见性关键字)内存屏障与原子性可见性、CAS(比较并交换)自旋、停止标志、快照
03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现监视器、死锁BLOCKED(阻塞)和持锁栈
04-CAS(比较并交换)、AQS(抽象队列同步器)与 Lock(锁接口)体系自旋、队列同步器CAS(比较并交换)热点与可中断锁
05-ThreadLocal(线程本地变量)与并发容器上下文与容器边界串请求、缓存与泄漏
06-线程池与异步编排队列、拒绝、编排耗尽、积压和背压

6. 模块级综合题库:并发、项目与线上排障

  1. 问题:线上 CPU(中央处理器)飙高时,你如何在 5 分钟内组织排查?

    • 口述答案:我先把 CPU(中央处理器)高当成症状而非结论。第一分钟确认影响实例、发布时间、流量变化、错误率、GC(垃圾回收)时间和是否有批任务;同时按业务优先级限流或暂停非核心导出,避免采集期间继续放大。第二分钟用 top -H 找持续高占用的操作系统线程,把线程标识与连续三份 jstack(线程栈工具)关联,观察相同栈是否重复。若栈集中在业务循环、序列化或正则处理,优先看火焰图确认累计比例;若大量栈在原子更新重试,检查 CAS(比较并交换)热点和共享计数;若 CPU(中央处理器)高同时 GC(垃圾回收)占比高,则用 jcmd(JVM 诊断命令)比较对象直方图与队列增长。第三分钟把技术证据与业务量对齐:是某租户、热点 SKU(库存单位)还是异常重放触发。止血可以是限流、关闭高代价功能或把热点转为排队,但根治必须是缩短计算、减少共享、分片或修正容量,而不是盲目加机器。最后保留命令输出、版本和时间线,压测验证 CPU(中央处理器)基线、P99(99 分位响应时间)和吞吐同时恢复。 补充口述时,我会明确先后顺序不是教条:在支付或库存已经受损时,先冻结有风险入口、保存流水和转储;在纯性能抖动时,则优先用低侵入采样。每一步都记录操作者、时间和结果,这样后续能把技术结论与业务影响精确对齐,而不是凭一次截图解释整个事故。
    • 追问与直接回答:
      • 为什么要连续取栈?答:排除瞬时状态,确认热点是否稳定。
      • RUNNABLE(可运行)就是耗 CPU(中央处理器)吗?答:不是,还要关联操作系统线程占用和重复栈。
      • 先扩容行不行?答:可作为短期容量止血,但不能替代根因定位。
    • 详细章节01-线程生命周期与 Java(编程语言)内存模型
  2. 问题:怎样从 jstack(线程栈工具)区分热点锁、长临界区和死锁?

    • 口述答案:我先从 jstack(线程栈工具)的线程状态和监视器文本建立等待关系。大量 BLOCKED(阻塞)只说明线程没有拿到 synchronized(同步锁)监视器,不能直接等同死锁;我会按 waiting to lock 的对象地址聚类,再找同地址的 locked 持有者。如果许多线程等同一对象,而持有者的栈停在数据库、网络或大批量循环,结论是热点锁或长临界区,修复重点是移出慢操作、缩短锁内状态转换、按业务键分片。死锁则需要更强证据:线程甲持有 A 等 B,线程乙持有 B 等 A,或更长的等待环,并且多份转储持续出现;工具的死锁摘要是辅助,逐行持有和等待关系才是可复核事实。处理顺序是先保存线程栈和相关订单、任务流水,再摘流、停非核心入口或重启单实例止血;重启不解决反向加锁。根治要统一资源排序、把多对象协作转成状态机或数据库条件更新,并用反向并发测试证明等待环不会重现。 补充口述时,我会强调锁对象地址只是证据索引,必须回到部署版本中的业务方法确认其语义;不同类加载器或包装对象不能只靠名称猜测。修复后还要压测两条原来相反的调用路径,并监控锁等待分位数,证明环被消除而非恰好没有触发。复盘中还要记录持锁期间的下游超时与请求量,区分“统一顺序缺失”的根因和“锁内工作过长”的放大因素,分别制定代码修复与容量验证。
    • 追问与直接回答:
      • 锁竞争能靠加线程解决吗?答:不能,更多线程通常只增加等待者。
      • tryLock(尝试加锁)能彻底避免问题吗?答:只能让失败可退避,仍需定义补偿和一致性边界。
      • 为什么先保存流水?答:避免重启后无法判断哪些请求处在半完成状态。
    • 详细章节03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现
  3. 问题:线程池耗尽的正反馈是什么,如何阻断?

    • 口述答案:线程池耗尽常从一个慢依赖开始:工作线程被下游调用占住,完成率下降;新请求进入队列,排队时间超过调用方超时;调用方以为失败再次提交,入口速率升高,队列更长;若队列无界,大量任务对象又推高堆和 GC(垃圾回收)暂停,进一步拉长超时。这是比“线程不够”更关键的正反馈。我的治理先分故障域:支付、库存、导出、通知使用独立有界线程池,分别有最大并发、等待预算、超时和拒绝策略。到达上限时,核心接口返回可识别的繁忙结果并让客户端退避,导出写任务表等待,通知合并摘要;不会让无界排队吞掉内存。排查时同时看活跃线程、队列深度、等待时间、拒绝数、下游耗时和重试率,再用线程栈确认线程真正卡在哪。根治还包括连接池与下游容量的联动预算,以及压测模拟慢依赖,验证降级后核心线程池仍有余量。 补充口述时,我会把拒绝后的用户体验说清:核心请求返回可退避的繁忙码,后台任务落入可查询的等待状态,通知按优先级合并。这样背压不是静默失败,而是把系统真实能力反馈给调用方,并让运营能看到恢复进度和积压边界。容量评审时还会把最大排队时间、单任务内存和下游并发许可写成联动约束,防止某次只调大队列而把问题转化为更晚发生的内存与超时事故。演练还应覆盖调用方错误重试,确认拒绝结果不会被立即放大成新的入口洪峰。
    • 追问与直接回答:
      • 队列越大越安全吗?答:不是,队列越大意味着更长延迟和更多内存承诺。
      • 拒绝任务是不是故障?答:对可延后业务是受控背压,优于进程失效。
      • 如何设置超时?答:按端到端预算分配,并让排队等待也计入预算。
    • 详细章节06-线程池与异步编排
  4. 问题:你如何讲清 WMS(仓储管理系统)库存防超卖的完整方案?

    • 口述答案:我会先说明目标不是“让一个线程扣库存”,而是在多实例、重复请求、宕机恢复下始终不承诺超过可售量。请求以仓库、SKU(库存单位)、数量和稳定幂等键进入;应用内可用按仓库—SKU(库存单位)分片的短锁做去重和聚合,降低同实例热点,但我会明确它不跨 JVM(Java 虚拟机)。最终裁决放到数据库条件更新:只有可售量不少于请求量时才扣减,影响一行才创建预占流水;影响零行要区分库存不足、版本不匹配和重复流水。预占、冻结、支付确认和超时释放构成状态机,所有外部通知在事务后异步发送,失败由流水扫描补发。热点商品不企图用更多锁突破物理库存,而是限购、排队、合并或预分配。排障时看热点分片、条件更新失败率、预占年龄、释放数和库存流水差异;事故后以流水对账而不是内存计数恢复。这样本地并发优化、共享最终事实和提交后恢复各自有明确责任。 补充口述时,我会说明数据库条件更新必须与预占状态、释放任务和人工调账共同设计;否则虽然某次扣减原子,支付超时、取消和重放仍可能留下冻结量。验收用库存守恒、预占年龄和订单流水三组数字交叉校验,而不是只看接口返回成功。
    • 追问与直接回答:
      • 为什么不能先查再扣?答:读写之间会被其他实例插入。
      • 条件更新成功后宕机怎么办?答:预占流水与过期释放任务负责恢复。
      • 分布式锁还需要吗?答:可用于减压,但条件更新仍是最终防线。
    • 详细章节04-CAS(比较并交换)、AQS(抽象队列同步器)与 Lock(锁接口)体系
  5. 问题:异步导出为什么要做线程池隔离和流式处理?

    • 口述答案:异步导出可延后,但它的查询、格式化、压缩和上传都可能消耗 CPU(中央处理器)、内存、连接和网络;若与订单接口共用线程池,单个大导出就能把核心请求排在后面。我的方案是请求只写入任务表,导出专属线程池以有界队列领取任务;执行时按固定页或游标读取,每批只保留少量行,立即写入临时文件或对象存储,不把全量结果列表放在堆中。任务状态记录参数版本、进度、尝试次数、文件标识和失败原因,成功上传后才暴露结果;失败可清理临时文件或从可验证断点重试。队列超过容量返回“稍后生成”,不是继续把任务塞进内存。监控单批对象大小、队列年龄、任务完成率、写速率和导出池拒绝数,并与订单池的活跃数隔离观察。这样 OOM(内存溢出)风险从不可预期的全量对象累积变成可计算的批次、并发和队列预算。 补充口述时,我会补上文件可见性边界:临时文件不能在上传未完成时暴露下载地址,成功状态只在校验后提交;重复点击返回同一任务而不是再次创建全量导出。这样既避免内存风险,也避免用户拿到半文件或看到两份不一致报表。恢复时按任务标识核对文件大小、校验结果和状态转换,确认重试没有生成重复可见文件。
    • 追问与直接回答:
      • 增大堆能解决吗?答:只能延后无界增长,不能消除根因。
      • 为什么任务不能带全量数据?答:排队期间会长期持有大量对象。
      • 取消如何做?答:在批次边界检查取消状态并安全清理资源。
    • 详细章节06-线程池与异步编排
  6. 问题:Runner(执行器)如何实现可停止、可恢复且不重复执行?

    • 口述答案:Runner(执行器)面对的是长任务和多实例,不应把“运行中”只放在线程或内存 Map(映射接口)里。我会用任务表保存状态、租约持有者、过期时间、心跳、步骤版本和业务幂等键;领取使用条件更新,把 READY(就绪)原子转为 RUNNING(运行中)并写入短租约。正常执行周期续租,在批次、远程调用返回或事务边界检查停止请求;停止时先关闭领取入口,再协作取消和清理,不能强杀线程。实例失联后租约过期,其他实例可以接管;旧实例恢复后续租失败就放弃提交。即便出现短暂双执行,外部物流推送和内部状态变更仍必须用稳定幂等键、版本或唯一约束拒重。排查时从任务年龄、租约续期失败、接管次数、步骤耗时和业务流水看问题,区分真正卡住与只是等待下游。这样中断负责本地协作,租约负责所有权恢复,幂等负责副作用正确。 补充口述时,我会说明租约不是业务完成证明。每个长步骤都要把输入版本、尝试号和结果摘要落库;接管者先读取这些事实再决定继续、补偿还是失败。这样即使旧实例在网络恢复后短暂继续运行,条件提交与幂等键仍能阻止它覆盖新所有者的结果。
    • 追问与直接回答:
      • 租约过期会不会双执行?答:可能,因此提交和外部调用仍要幂等。
      • 为什么不直接重启所有 Runner(执行器)?答:会扩大重复和恢复窗口,应滚动停止并保留状态。
      • 长任务怎么续租?答:拆成可重试步骤,按步骤心跳并检查所有权。
    • 详细章节01-线程生命周期与 Java(编程语言)内存模型
  7. 问题:支付回调幂等的锁边界应怎样划分?

    • 口述答案:支付回调的正确性来自稳定事件事实,而不是一把长时间持有的订单锁。我的入口先在锁外完成验签、字段校验和轻量路由;随后以渠道事件号或支付流水号作为幂等键,在数据库唯一约束下争夺首处理权。支付状态、账务记录和 Outbox(发件箱)事件在同一事务内提交,重复回调读取既有结果并返回可识别成功,乱序回调由状态机拒绝非法倒退。局部锁若存在,只保护极短的内存聚合或同实例缓存,不包住数据库慢操作、渠道调用或通知发送;这些慢操作放到事务后和独立线程池中。这样 BLOCKED(阻塞)线程不会因下游网络慢堆满回调池,且即使实例重启,唯一键和状态机仍能恢复事实。线上我会看重复率、唯一冲突、非法状态转换、Outbox(发件箱)积压、回调池队列和同订单热点栈,并用账务、支付流水、待发事件三方对账验证任何止血操作没有漏记。 补充口述时,我会把资金核验放在最后:对同一支付流水核对渠道结果、支付状态、账务分录和待发事件,任何一项缺失都进入补偿或人工队列。这样线程池和锁问题得到止血后,不会把“接口恢复”误认为“资金链路已经完全正确”。
    • 追问与直接回答:
      • 订单号可直接当幂等键吗?答:不总是,一个订单可能存在多次支付事件。
      • 通知失败要回滚支付吗?答:不应,使用 Outbox(发件箱)异步重试。
      • 本地 Set(集合接口)有什么作用?答:只能减压,不能替代持久化唯一约束。
    • 详细章节03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现
  8. 问题:如何治理 IoT(物联网)报警风暴而不丢业务事实?

    • 口述答案:我的第一原则是原始事件和派生通知分层:设备报警先可靠落到可恢复存储,后续的窗口聚合、去重、规则计算和通知都可以重算。处理按租户、设备和报警码等稳定键分片,让同一设备的局部顺序可控、不同设备并行;窗口状态设置条目数、时间和样本量上限,避免某个异常设备把内存无限占满。通知使用独立有界线程池,接近上限时按优先级做摘要、延迟或暂停低级别通知,绝不为了“每条都实时通知”而拖垮原始接入。多实例通过消息分区或共享状态协调,单机 Map(映射接口)只作短暂缓存。事故中先验证原始落盘成功率与存储余量,再看热点分片、队列年龄、聚合命中和通知滞后;恢复后依据原始事实重算窗口并补发关键告警。复盘要量化哪些派生结果延迟、是否有事实缺口、背压何时生效以及规则或设备重连是否是触发源。 补充口述时,我会讲清恢复顺序:先让原始接入稳定并确认没有事实缺口,再逐步恢复高优先级规则和通知,最后重算低优先级窗口。这样避免恢复瞬间把积压一次性推给同一个下游渠道,形成第二次报警风暴,并能量化每一级降级的实际影响。
    • 追问与直接回答:
      • 为什么不能同步发通知?答:下游限速会把每条报警变成长等待和重试。
      • 分片能消除热点吗?答:不能,只能把热点限制在局部并保留其他分片吞吐。
      • 何时允许丢弃?答:可丢可重算的低优先级通知,不丢原始事实。
    • 详细章节05-ThreadLocal(线程本地变量)与并发容器
  9. 问题:如何解释 CAS(比较并交换)自旋导致 CPU(中央处理器)高?

    • 口述答案:CAS(比较并交换)把“比较旧值与写入新值”合成硬件原子步骤,适合短小且冲突不高的更新;但它不保证一次成功,失败线程通常重读后重试。高并发线程反复更新同一个计数、热点状态或共享头指针时,大量失败重试会消耗 CPU(中央处理器),吞吐反而下降。我会先通过连续 jstack(线程栈工具)、JFR(Java 飞行记录器)或火焰图确认热点确实集中在原子更新循环,而不是把任何 CPU(中央处理器)高都归因于 CAS(比较并交换)。修复优先减少共享:把全局计数拆到分片或 LongAdder(长整型累加器)类的分散单元,把热点业务键按所有权路由,或将频繁小更新合并为批次;不是无限增加自旋次数。对于库存、金额等必须精确裁决的事实,CAS(比较并交换)只适合本地优化,跨实例仍要条件更新和流水。压测要比较冲突率、CPU(中央处理器)、尾延迟和正确性,避免只看到微基准快就上线。 补充口述时,我会说明不能只看成功率,因为 CAS(比较并交换)重试可能仍然得到正确结果却烧掉大量 CPU(中央处理器)。需要观察冲突频率、线程执行时间和尾延迟,并在真实热点分布下比较分片前后。若冲突来自业务单点,最终还应从限购或排队层面消除无效竞争。
    • 追问与直接回答:
      • CAS(比较并交换)会阻塞吗?答:通常不挂起线程,但失败重试会消耗 CPU(中央处理器)。
      • 自旋次数越多越好吗?答:不是,持有者很慢时自旋只会浪费计算资源。
      • 何时用锁?答:临界区较长、需要等待或组合不变量时锁往往更合适。
    • 详细章节02-volatile(可见性关键字)内存屏障与原子性
  10. 问题:上下文串请求或 ThreadLocal(线程本地变量)泄漏怎样定位和修复?

  • 口述答案:我先区分两类现象:日志、租户或用户标识串到别的请求,是线程复用后的上下文残留;堆持续增长则可能是长生命周期线程持有未清理的大对象。证据上会关联异常请求的线程名、提交时间、追踪标识和租户字段,并检查是否总发生在同一工作线程;内存趋势再用 jcmd(JVM 诊断命令)直方图和堆转储确认对象由线程本地映射或任务闭包持有。修复不是在每个业务分支补一行清理,而是在执行器任务装饰层建立协议:提交时显式捕获必要的不可变上下文,执行前设置,try(尝试)/finally(最终清理)中无条件 remove(移除),并清理第三方库注册的上下文。业务数据不应依赖隐式线程上下文跨异步边界传递,必须作为参数或任务字段持久化。验证包括线程池复用压测、异常路径和取消路径,确认错误标识不再出现且 GC(垃圾回收)后基线回落。 补充口述时,我会把安全边界纳入方案:上下文中若有身份或租户信息,日志、诊断输出和任务持久化都只保留最小必要字段,并确保异常路径也清理。验证不仅是功能正确,还要模拟线程复用、超时和取消,检查是否出现跨租户读写或追踪链错位。
  • 追问与直接回答:
    • 设为 null(空值)可以吗?答:应使用 remove(移除)表达生命周期结束。
    • 虚拟线程有这个问题吗?答:边界不同但仍应显式管理上下文,不能依赖偶然的线程生命周期。
    • 为什么异步最危险?答:提交线程与执行线程不同,隐式值可能丢失或串入旧值。
  • 详细章节05-ThreadLocal(线程本地变量)与并发容器
  1. 问题:内存持续增长时如何建立“对象—引用—业务入口”证据链?
  • 口述答案:我不会一开始就把所有现象叫内存泄漏。先看 GC(垃圾回收)后堆基线是否持续抬升,并把增长时间与流量、队列深度、版本和批任务关联;短峰后能回落可能只是容量不足,回收后仍增长才更像对象生命周期没有结束。接着用两次 jcmd(JVM 诊断命令)类直方图对比对象数量和字节增量,判断是导出行、字节数组、任务包装、缓存条目还是 ThreadLocal(线程本地变量)相关对象在增长。只有定位到可疑种类才在安全窗口导出堆转储,沿引用链找是谁持有它:无界队列、静态缓存、监听器、线程本地值或未关闭会话。最后回到业务入口验证为何没有消费、过期或清理。止血是限制入口、暂停可延后任务、缩小批次和释放可重建缓存;根治是上限、淘汰、明确所有权和 finally(最终清理)清理。验证必须看长期堆基线、吞吐与任务完成率,不能只看一次手工 GC(垃圾回收)。 补充口述时,我会区分可重建缓存和不可重建事实:前者可以在止血时清理或缩小容量,后者必须先完成持久化和对账再处理。对象分析结果要落到生命周期责任人,例如队列消费者、缓存淘汰任务或请求清理钩子,避免只加一次手动清理而下一轮再次增长。
  • 追问与直接回答:
    • 直方图能说明引用链吗?答:不能,只能帮助选择下一步堆分析目标。
    • 能在线上立刻导出堆吗?答:要评估停顿、磁盘和敏感数据风险,优先隔离实例。
    • 队列增长一定是泄漏吗?答:不一定,但无界且不回落会形成同样的内存风险。
  • 详细章节06-线程池与异步编排
  1. 问题:为什么说不可变快照是减少并发故障的设计工具?
  • 口述答案:不可变快照把共享状态的可修改期收缩到构造阶段:一个拥有者在局部变量中完成校验、计算和版本组装,发布后读者只读取完整对象,不再与写者交错修改字段。这样既减少锁竞争,也避免“金额更新了、规则版本还没更新”这类跨字段半状态。配置热更新、库存查询视图和 Runner(执行器)任务参数都适合用这种方式:刷新方生成新快照,再用 volatile(可见性关键字)引用、锁或线程安全容器一次替换;失败时继续保留最后成功版本并标记陈旧度。它不能替代库存扣减、支付记账等跨实例事实,因为快照只是本地读优化,最终写入仍要通过数据库条件更新、事务和流水裁决。线上排查旧配置或字段组合不一致时,要看对象是怎样构造、怎样发布、读者是否缓存旧引用,而不是给每个 getter(获取方法)加锁。快照的代价是对象复制和版本管理,适用于读多写少、整体一致比逐字段更新更重要的场景。 补充口述时,我会说明快照的版本号和来源很重要。读者遇到下游刷新失败时能选择最后成功版本并暴露陈旧度,写者则在构造期校验字段完整性;这样线上既不会把半成品发布出去,也不会因为追求绝对实时而让所有读请求阻塞在刷新锁上。
  • 追问与直接回答:
    • final(最终关键字)字段就天然够吗?答:还要避免构造期间逸出和内部可变对象泄露。
    • 快照更新要锁吗?答:构造在局部完成,发布可用一次同步边;视具体容器而定。
    • 何时不适合?答:高频逐项写入且复制成本过高时,应使用分片状态或其他协议。
  • 详细章节01-线程生命周期与 Java(编程语言)内存模型
  1. 问题:如何把“减少共享、分片、所有权、背压”讲成统一的并发设计原则?
  • 口述答案:我会把它们解释成同一目标的不同层次:让每份状态有尽量少的写者、尽量明确的归属和尽量可控的压力。减少共享用局部变量、不可变快照和消息传递,让大量读写根本不需要竞争;分片按仓库—SKU(库存单位)或设备键把必须共享的状态拆成独立单元,避免无关请求排队;所有权用租约、版本和状态机说明当前谁可以推进、失联后谁能接管;背压用有界队列、拒绝和降级让超出能力的压力在边界处可见,而不是积累到内存。它们共同避免把系统设计成“所有线程都可改所有数据、所有请求都必须立即成功”。库存项目中,最终库存由数据库拥有,应用只做本地分片减压;IoT(物联网)项目中原始事件拥有事实,通知只是可降级派生。面试时我还会说明代价:分片会有热点和路由,快照有复制成本,背压需要产品定义降级语义,所有权需要恢复协议。能说出代价才不是口诀。 补充口述时,我会用容量表说明这些原则如何落地:每个分片的最大排队、每类任务的内存预算、每个租约的最长恢复时间都可被监控。设计不是抽象口号,而是把共享面、故障域和恢复时间量化;超过预算就触发明确的限流、摘要或人工处置路径。
  • 追问与直接回答:
    • 分片能替代事务吗?答:不能,跨分片不变量仍需事务或补偿协议。
    • 背压是不是拒绝用户?答:是受控拒绝或延后,用来保护更重要的整体可用性。
    • 所有权怎样验证?答:用租约、版本、状态和审计流水证明当前提交者合法。
  • 详细章节04-CAS(比较并交换)、AQS(抽象队列同步器)与 Lock(锁接口)体系
  1. 问题:接口 P99(99 分位响应时间)升高时,如何区分排队、锁等待和下游慢?
  • 口述答案:我先把端到端延迟拆成入口排队、线程执行、锁等待、连接等待和下游耗时,而不是只看一个平均值。线程池的队列深度和任务等待时间持续增加、活跃线程接近上限时,优先怀疑排队;连续 jstack(线程栈工具)里大量 BLOCKED(阻塞)等待同一监视器且能找到持有者时,优先怀疑锁竞争;工作线程若大多停在数据库驱动、HTTP(超文本传输协议)客户端或 DNS(域名系统)调用,并且下游指标同步变差,则是依赖慢。三者可以叠加:下游慢会延长持锁时间或占满线程池,所以要按时间线找最先变化的信号。止血分别不同:排队要限流、拒绝或分池;锁等待要缩短临界区并移出慢操作;下游慢要超时、熔断、降级和隔离。根治后用压测验证等待时间、执行时间和 P99(99 分位响应时间)都回落,并确认没有因为超时重试把入口流量放大。这样给出的不是“接口慢”的笼统答案,而是一条可操作的归因路径。 补充口述时,我会把分解结果放进同一张延迟瀑布图:入口等待、执行、数据库、远程服务和响应编码各自有时间预算。这样即使某一项暂未异常,也能提前发现预算被侵蚀;修复后按相同维度比对,确认延迟不是从锁等待转移成更长的队列等待。
  • 追问与直接回答:
    • 为什么平均延迟不够?答:尾部排队会被平均值掩盖。
    • 慢查询一定是数据库问题吗?答:也可能是连接池等待或锁等待,需要分层指标。
    • 锁内远程调用为何危险?答:下游不确定性会放大成同锁请求的串行等待。
  • 详细章节03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现
  1. 问题:JFR(Java 飞行记录器)、Arthas(Java 诊断工具)和火焰图怎样组合使用?
  • 口述答案:三者解决的问题不同,我不会把它们当成可互换的“性能工具”。JFR(Java 飞行记录器)适合在明确时间窗内记录锁等待、对象分配、垃圾回收、线程调度和 I/O(输入输出)事件,回答“发布后从何时开始同时发生了什么”;火焰图把采样栈按累计消耗聚合,回答“CPU(中央处理器)时间主要花在什么调用链”;Arthas(Java 诊断工具)适合在小范围、短时间地观察某个方法的参数、返回、异常或耗时,验证具体假设。我的顺序是先用指标和 jstack(线程栈工具)缩小到实例和时间窗,再用 JFR(Java 飞行记录器)与火焰图确认全局比例,最后才用 Arthas(Java 诊断工具)定点查看,例如确认某个导出批次是否异常大。直接对高频核心方法做全量跟踪会增加开销并暴露敏感数据,必须设条件、采样率、时长和脱敏边界。无论哪个工具,都要回到订单、任务或流水事实验证影响;性能热点不自动等于业务根因。 补充口述时,我会说明诊断权限与数据最小化原则。对支付、地址和身份字段只观察摘要、长度或脱敏值;每次在线观察都有时间上限和清理动作。这样工具使用本身不会制造新的性能、合规或信息泄露事故,证据包也可以安全交给复盘与研发团队复查。
  • 追问与直接回答:
    • 火焰图宽度代表调用次数吗?答:通常代表累计采样时间或比例,需看采样类型。
    • Arthas(Java 诊断工具)能替代压测吗?答:不能,它观察生产现象,不能证明修复的容量边界。
    • 为什么先用轻量工具?答:故障时要优先减少对被观测系统的额外扰动。
  • 详细章节01-线程生命周期与 Java(编程语言)内存模型
  1. 问题:怎样处理任务“看起来丢失”的事故?
  • 口述答案:我先拒绝把“没有看到结果”直接解释成任务丢失,而是建立从接收至完成的事实链:请求或消息是否被接收、任务是否持久化、是否被领取、是否执行到业务步骤、是否提交状态、是否发送结果、消费者是否确认。每一跳都需要稳定任务标识和时间戳。常见根因包括先确认消息后落库、只把运行状态放在内存、实例宕机后租约没有释放、异常被吞掉但任务仍标记成功、以及重复执行后被错误覆盖。止血时先停止自动确认或暂停问题消费者,保留队列位置、任务表和日志证据,必要时把未终态任务重新置为可领取;不要直接批量重跑,否则会把重复副作用扩大。根治使用持久化状态机、条件领取、租约、幂等业务键和定期对账:接收数、落库数、完成数、失败数和终态数必须可平衡。对于外部副作用,任务完成不等于线程返回,而是要有可验证的外部确认或后续对账记录。恢复演练应覆盖提交前宕机、提交后通知失败和消费者重复投递。 补充口述时,我会把任务状态设计成可审计的有限集合,避免用一个模糊的“处理中”掩盖领取、执行、等待外部确认和补偿的差异。对每次状态变化记录原因与尝试号,恢复程序只处理规则明确的过期状态,不能因为一条告警就把未知任务全部重跑。
  • 追问与直接回答:
    • 如何决定重放范围?答:按未终态、过期租约和对账差异生成清单。
    • 任务表会不会成为瓶颈?答:可按状态、时间和业务键索引分片,但不能因此放弃持久化事实。
    • 消息确认何时做?答:在任务事实已安全持久化或可恢复后,而非刚收到时。
  • 详细章节06-线程池与异步编排
  1. 问题:怎么把 volatile(可见性关键字)的边界说清楚并落到线上问题?
  • 口述答案:volatile(可见性关键字)适合发布一个状态或引用:写入方更新后,后续读者能看到这次写及其之前按规则应可见的内容;它还能约束某些重排序。因此配置快照替换、停止标记、一次性初始化完成标识是典型场景。但它不能把读取、计算、写回三个步骤变成原子动作,count++、库存扣减和余额转账仍会丢更新。线上如果 Runner(执行器)停止标志不生效,我会先确认工作循环是否周期读取标志、是否被长时间不可中断调用卡住、异常是否吞掉中断,而不是仅把字段加 volatile(可见性关键字);如果库存出现超卖,也不能给数量字段加 volatile(可见性关键字)就认为安全,最终必须依赖条件更新和流水。设计上我偏向不可变快照加一次引用发布,而不是多个 volatile(可见性关键字)字段逐个修改,因为前者能保证多字段组合的一致视图。面试回答要说明它解决可见性与顺序,不承诺复合原子性,也不跨进程。 补充口述时,我会把发布约束说完整:写者先构造完整对象,再发布引用;读者在每轮循环或请求边界读取一次稳定值,不混用多个版本。对于需要停止的任务,还要把可见标志与中断、超时和资源关闭组合起来,避免可见但不可执行的“伪取消”。
  • 追问与直接回答:
    • volatile(可见性关键字)能替代锁吗?答:只能替代非常简单的单状态同步,不能保护复合不变量。
    • 停止标志为真为何任务仍跑?答:可能没有读取机会、在阻塞调用中或取消协议不完整。
    • 多实例配置同步怎么办?答:需要共享配置源、版本和发布协议,本地字段只负责单进程可见。
  • 详细章节02-volatile(可见性关键字)内存屏障与原子性
  1. 问题:AQS(抽象队列同步器)和 Lock(锁接口)在项目里怎样选,而不是只背 API(应用程序接口)?
  • 口述答案:我先从不变量和失败策略选,而不是因为 Lock(锁接口)“功能更多”就替代所有 synchronized(同步锁)。短小、结构清晰的单对象临界区使用 synchronized(同步锁)可读性高,异常释放也直观;需要可中断获取、超时尝试、多个条件队列或显式公平策略时,才考虑基于 AQS(抽象队列同步器)的 Lock(锁接口)。AQS(抽象队列同步器)本质是把同步状态、等待队列和获取释放协议抽象出来,但公平、可重入、读写分离都带来吞吐、饥饿或复杂度权衡。项目里我不会拿锁保护远程调用或数据库事务,因为那会把下游抖动变成线程阻塞;库存最终约束放数据库,Runner(执行器)跨实例所有权放租约,锁只保护本地短暂状态。排障时如果看到 AQS(抽象队列同步器)相关等待栈,要结合是否存在锁持有者、等待时间和业务键热点,而不是把所有 WAITING(无限等待)都当故障。选择正确的标准是失败时是否能退出、补偿和恢复,不是类名是否高级。 补充口述时,我会说明显式锁的上线验证重点是异常路径:获取第一把锁后第二把失败、超时返回、线程被中断、业务异常抛出时,已获得资源是否总能释放,状态是否能恢复。没有这些失败测试,锁的可中断或超时能力只停留在接口层,无法转化为可靠业务行为。
  • 追问与直接回答:
    • 公平锁一定更正确吗?答:正确性取决于不变量,公平锁只是调度策略且可能降低吞吐。
    • 读写锁适合库存吗?答:读多写少的本地缓存可考虑,库存最终事实仍需共享存储裁决。
    • tryLock(尝试加锁)失败后怎么办?答:释放已持有资源、退避或转异步补偿,不能无限忙等。
  • 详细章节04-CAS(比较并交换)、AQS(抽象队列同步器)与 Lock(锁接口)体系
  1. 问题:发布后接口卡顿,你如何判断是代码、配置还是容量变化?
  • 口述答案:我先冻结时间线:发布批次、配置变更、流量曲线、依赖版本、实例数和告警首次出现时间。若问题与某版本严格同步且回滚后恢复,代码或依赖变更优先;若没有发布但某开关、线程池参数、连接池上限或路由配置变化后出现,则检查配置版本和是否完整发布;若流量、热点键或下游耗时跨过既有容量基线,则是容量或外部环境触发。它们也可能叠加,例如新版本把导出任务放入订单池,平时无感,流量高峰才触发。证据包括线程池活跃数与队列、连续线程栈、JFR(Java 飞行记录器)事件、数据库与网络指标、以及按租户和接口的流量分布。止血优先使用可回滚开关、限流和隔离,避免在未知根因下多次发布;根治后重放相同流量和依赖延迟,确认新旧版本差异确实消失。复盘要写清触发条件,不把“发布导致”停在相关性层面。 补充口述时,我会保留一个可回滚的容量配置和一个最小化流量回放样本。这样先在隔离实例验证代码或参数变化对线程池、锁等待和下游调用的影响,再逐步扩大;避免事故期间连续调大线程、队列和超时,导致因果被多项变化同时掩盖。
  • 追问与直接回答:
    • 回滚后恢复就能证明代码问题吗?答:是强证据,但还需排除同时发生的流量或依赖变化。
    • 配置热更新怎么防半生效?答:用版本化不可变快照和原子发布。
    • 为什么保留发布信息?答:没有版本锚点,线程栈难映射到实际二进制和配置。
  • 详细章节01-线程生命周期与 Java(编程语言)内存模型
  1. 问题:怎样设计一次可信的并发事故复盘?
  • 口述答案:可信复盘首先区分事实、推断和行动项。事实部分用监控、日志、jstack(线程栈工具)、jcmd(JVM 诊断命令)输出、发布记录和业务流水给出影响范围与精确时间线:何时开始、哪些接口和租户受影响、错误和延迟到什么程度、数据是否完整。推断部分必须写出证据如何指向根因,例如三份栈都显示订单锁持有者停在下游调用,且队列从该时点增长;不能只写“线程池不足”。行动部分分为已完成止血、数据修复、代码根治、监控补充和演练验证,每项有负责人、期限和验证标准。对于库存、支付、Runner(执行器)等业务,要单列数据核验:用流水、账务、任务状态和外部回执说明是否有漏、重或乱序;性能恢复不等于业务正确。复盘还应识别系统性原因,例如没有队列等待指标、没有限流开关、线程池混用或测试未覆盖多实例故障。最后把演练脚本和阈值沉淀,使下一次能更早发现、更小范围降级。 补充口述时,我会让复盘行动项可验收,例如“队列等待超过两秒即告警且演练可触发降级”“库存对账差异在十五分钟内形成清单”。只有把目标写成阈值、演练和数据校验,团队才能确认机制已落地,而不是在下一次事故里再次依赖个人经验临场判断。
  • 追问与直接回答:
    • 复盘要追责个人吗?答:重点是机制和决策信息,不以个人归因替代系统改进。
    • 根因未知怎么办?答:明确未知边界、列证据和验证计划,不能编造单一原因。
    • 如何证明行动项有效?答:用压测、故障注入和生产指标阈值验证。
  • 详细章节03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现
  1. 问题:如何应对“线程数很多但吞吐很低”的问题?
  • 口述答案:线程数多而吞吐低通常说明线程不是在有效计算,而是在等待、争用或做重复工作。我先看状态分布:大量 BLOCKED(阻塞)指向监视器竞争,大量 WAITING(无限等待)可能在队列、条件变量或连接池等待,大量 RUNNABLE(可运行)则要用操作系统 CPU(中央处理器)和火焰图确认是否热点计算或 CAS(比较并交换)自旋。再看线程池队列和下游延迟:如果所有线程都在慢数据库或网络调用,增加线程只会增加连接竞争;如果都在同一热点 SKU(库存单位)锁,增加线程只会增加上下文切换;如果任务粒度太小,调度与序列化开销也会吞掉收益。治理是减少共享、合并小任务、按业务键分片、把慢依赖设超时并隔离、为可延后工作建立背压。线程数是资源预算不是目标值,应根据 CPU(中央处理器)核数、阻塞比例、下游并发能力和单任务内存共同定。验证时比较有效完成率、排队时间、上下文切换、CPU(中央处理器)和 P99(99 分位响应时间),而非只看活跃线程数量。 补充口述时,我会检查线程数增长是否引入了新的资源争用,例如数据库连接池、文件句柄、远程服务并发许可和堆中任务对象。吞吐低的系统经常不是一个瓶颈,而是多个排队点串联;优化应选择最早形成积压的限制点,并持续观察其余限制点是否随后浮现。
  • 追问与直接回答:
    • IO(输入输出)密集就无限加线程吗?答:不行,仍受连接、内存和下游限流约束。
    • 虚拟线程能解决吗?答:能降低部分线程资源成本,不能消除锁、下游和无限并发问题。
    • 如何发现任务过细?答:看调度开销、短栈比例和单位业务的有效处理时间。
  • 详细章节06-线程池与异步编排
  1. 问题:并发容器能解决哪些问题,不能解决哪些问题?
  • 口述答案:ConcurrentHashMap(并发哈希映射)等并发容器能封装内部桶、扩容、单次读写和部分复合操作的并发协议,适合本地临时状态、去重窗口、指标聚合和缓存索引;它避免普通集合并发修改导致的结构破坏,但不自动把“读取库存、判断、扣减、写流水”变成一个业务原子事务。即使使用原子计算方法,也只在当前 JVM(Java 虚拟机)内生效,多实例会各自维护独立容器,重启还会丢失状态。设计时我先定义容器中的数据是否可重建、是否有容量上限、谁负责清理、迭代是否允许弱一致,然后才决定容器类型。线上若容器内存增长,先查键数、过期策略、热点和清理是否执行,不要把“线程安全”误当成“不会泄漏”;若出现业务重复,检查幂等键和数据库唯一约束。支付、库存和原始事件的最终事实仍由共享持久化、状态机、事务和对账保证。这样我既能说明容器的价值,也不会夸大它的责任边界。 补充口述时,我会规定容器条目必须有来源、容量、失效和所有者四个信息。没有失效时间和清理责任的本地去重集合,即使线程安全也会在长时间运行后变成内存事故;没有稳定键和持久化边界的容器,即使并发正确也会在重启和多实例时产生业务重复。
  • 追问与直接回答:
    • computeIfAbsent(不存在则计算)能调用远程服务吗?答:不建议,函数应快速且无复杂副作用。
    • 弱一致迭代能做报表吗?答:只能做趋势或最佳努力,审计报表应读持久化快照。
    • 需要分布式缓存时怎么办?答:仍要定义失效、容量、幂等和最终事实来源。
  • 详细章节05-ThreadLocal(线程本地变量)与并发容器
  1. 问题:如何处理跨境物流同步中的重复、乱序和超时不确定性?
  • 口述答案:跨境物流同步的核心不是让一次 HTTP(超文本传输协议)调用“绝不失败”,而是把每个物流事件变成可识别、可重试、可对账的事实。任务以运单号、事件类型、版本或渠道事件号组成幂等键,先在任务表记录待处理状态;Runner(执行器)领取后调用外部系统,超时不立即认定失败,因为对方可能已经接收但响应丢失。此时先按幂等键查询对方状态或等待回执,只有确认未生效才沿用相同键重试。乱序事件由本地状态机判断是否允许跃迁,不能让“已签收”被旧的“运输中”覆盖;重复事件读取既有结果。执行权用租约恢复,外部副作用用幂等键兜底,通知和统计则由 Outbox(发件箱)或后续任务可靠传播。线上监控事件年龄、查询命中率、重试次数、租约接管和状态拒绝,异常时按运单流水对账,而不是用线程日志猜测。这样面对网络不确定性,系统承认不确定并把它留在可恢复状态,而非重复发送造成更大错误。 补充口述时,我会把外部调用结果划成成功、明确失败和不确定三类。明确失败可按规则重试或补偿;不确定结果不能直接覆盖状态,需要查询、回执或对账。这个分类让超时治理不再是简单增加重试次数,而是根据事实缺口选择继续等待、重试或人工处理。
  • 追问与直接回答:
    • 超时后为什么不能立即重试?答:无法知道对方是否已成功执行,重试可能造成重复副作用。
    • 状态机要不要允许回退?答:只允许业务定义的补偿或更正路径,不允许旧事件随意覆盖。
    • 对方没有查询接口怎么办?答:保留不确定状态并进入对账或人工流程,不能伪造确定结果。
  • 详细章节04-CAS(比较并交换)、AQS(抽象队列同步器)与 Lock(锁接口)体系
  1. 问题:如果面试官追问“为什么本地锁不能保证分布式一致性”,你会怎么回答?
  • 口述答案:我会先明确本地锁的保护域:它协调的是一个 JVM(Java 虚拟机)进程内、同一锁对象上的线程,保证临界区不交叉以及相关内存可见;服务一旦横向扩容,每个实例都有自己的对象和监视器,两个实例可以同时进入各自的“同一业务锁”。即使只部署一个实例,进程重启、消息重投、网络超时和数据库提交后失败也会让内存锁状态消失或无法说明外部事实。因此库存扣减要用数据库条件更新、版本或唯一流水决定最终可售量;支付回调要用渠道事件键、事务和状态机决定是否首次处理;Runner(执行器)要用任务租约和幂等键处理接管。分布式锁可以作为减少竞争的协调工具,但还要考虑过期、续约、客户端停顿和旧持有者恢复,最终写入必须有条件校验。面试中我会补一句:本地锁仍然很有价值,它保护本地缓存、短暂组合状态和同实例去重;错误不在使用它,而在把它宣传成跨实例事实的唯一保障。 补充口述时,我会用一次双实例演练证明边界:两个实例同时处理同一业务键、一台在提交前或提交后宕机、网络响应被丢弃。只有条件写入、唯一键和状态机仍能收敛到同一结果,才能说明方案具备分布式恢复能力;本地锁只作为减少同实例噪声的优化。
  • 追问与直接回答:
    • 单实例时本地锁够吗?答:只对进程存活期间的本地不变量够,仍不替代持久化恢复。
    • 分布式锁加数据库锁会不会重复?答:可分层,前者减压协调,后者最终拒绝非法写入。
    • 如何测试这个边界?答:多实例并发、宕机、超时重试和消息重复都要覆盖。
  • 详细章节03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现
  1. 问题:请用 5 分钟串讲一次并发线上事故与项目改造。
  • 口述答案:我会用“症状—证据—边界—改造—结果”组织。一次高峰期 WMS(仓储管理系统)库存接口 P99(99 分位响应时间)从 120ms(毫秒)升到 4s(秒),线程池队列持续增长。我们先对热点 SKU(库存单位)限流并暂停非核心通知,保护支付和下单;随后连续采集 jstack(线程栈工具),发现大量 BLOCKED(阻塞)线程等同一库存锁,持有者却在锁内调用下游服务。证据说明不是单纯线程数不足,而是锁内慢调用把局部热点放大为线程池耗尽。改造后,本地锁只保留仓库—SKU(库存单位)的短暂去重和状态组装,下游调用移到锁外并设置独立线程池、超时和有界队列;最终库存仍由数据库条件更新和预占流水裁决,通知通过可重试事件异步发送。我们同时补了热点分片、锁等待、队列年龄、条件更新失败率和流水差异监控,并演练下游变慢、实例重启和重复请求。结果不是承诺“再也不慢”,而是热点只影响局部分片,过载时可降级且不超卖,故障后能按流水恢复。最后我会主动说明本地并发正确与分布式一致的边界,体现方案可验证、可运维。 补充口述时,我会收束到可运营结果:高峰期间哪些业务被降级、核心下单和支付是否仍在预算内、多少任务延后并在何时恢复、库存与账务是否完成对账。这些数字让项目表达从“用了很多技术”变成“在故障约束下仍保护了哪些业务目标”,也是面试官最容易继续追问的部分。
  • 追问与直接回答:
    • 为什么不直接加线程?答:锁内慢调用会让更多线程一起等待,不能提高临界区吞吐。
    • 下游失败后库存怎么办?答:预占流水和状态机决定释放或补偿,不让内存锁承担恢复。
    • 如何量化效果?答:比较热点 P99(99 分位响应时间)、队列峰值、阻塞比例和流水差异。
  • 详细章节07-线上排障、项目话术与综合题库

7. 复述模板、复习清单与索引

3 分钟复述模板

“我先按症状把事故分成 CPU(中央处理器)高、接口慢、锁阻塞、线程池耗尽、任务异常和内存增长;先限流或降级保护核心交易,再用指标、连续 jstack(线程栈工具)、jcmd(JVM 诊断命令)和业务流水构造证据。并发设计上,我尽量减少共享、按业务键分片、用不可变快照发布本地读模型,用有界队列和背压限制压力。库存、支付和任务最终正确性不依赖本地锁,而依赖条件更新、唯一键、状态机、租约和对账。”

5 分钟复述模板

“排障先建立时间窗和影响范围,止血动作不破坏证据;CPU(中央处理器)高用操作系统线程占用关联连续栈,接口慢拆成队列、锁、下游和 GC(垃圾回收),死锁必须从等待—持有关系画出环。项目上,WMS(仓储管理系统)库存用本地分片减压、数据库条件更新和预占流水防超卖;导出用专属有界线程池和流式批处理防 OOM(内存溢出);Runner(执行器)用状态、租约、协作停止和幂等恢复;支付回调用唯一键、事务和 Outbox(发件箱)防重复;IoT(物联网)报警先保存原始事实,再分片聚合和背压通知。每个方案都说明失败路径、监控和降级。”

10 分钟复述模板

按“事故决策树—取证顺序—线程池故障域—五个项目案例—设计思想—复盘闭环”展开:对每一类症状给出指标、命令、证据、根因、止血、根治和验证;穿插两段线程栈说明如何从文本读出长临界区与死锁;最后以“本地并发正确不等于分布式一致”收束,指出共享持久化事实、幂等状态机和对账才支撑跨实例恢复。

复习清单

  • 能在不看稿的情况下说出 CPU(中央处理器)高、接口卡顿、BLOCKED(阻塞)、死锁、线程池耗尽、任务丢失和内存增长的证据链。
  • 能逐行解释一份热点锁线程栈与一份死锁线程栈,并说明不能从中推出什么。
  • 能区分 jstack(线程栈工具)、jcmd(JVM 诊断命令)、JFR(Java 飞行记录器)、Arthas(Java 诊断工具)和火焰图的使用顺序。
  • 能用背景、量级、约束、失败时序、监控、降级和结果讲完库存、导出、Runner(执行器)、支付、IoT(物联网)五个案例。
  • 能说明本地锁、并发容器、条件更新、唯一约束、租约、Outbox(发件箱)和对账各自的责任边界。

章节索引

主题详细章节本章入口
生命周期、停止与发布01-线程生命周期与 Java(编程语言)内存模型Runner(执行器)、快照、取栈
可见性、原子性与自旋02-volatile(可见性关键字)内存屏障与原子性CAS(比较并交换)热点
监视器、死锁与锁边界03-synchronized(同步锁)与 HotSpot(热点虚拟机)锁实现BLOCKED(阻塞)、支付、库存
同步器与显式锁04-CAS(比较并交换)、AQS(抽象队列同步器)与 Lock(锁接口)体系退避、分片、锁选型
上下文与并发容器05-ThreadLocal(线程本地变量)与并发容器串请求、状态缓存
线程池与异步编排06-线程池与异步编排耗尽、导出、背压
事故与项目表达07-线上排障、项目话术与综合题库复盘与口述训练