真实面经题目 · 原创解析
如何在实际中判断是否会出现线程安全问题?
判断实际项目中是否会出现线程安全问题,核心不是先看有没有多线程,而是追踪共享可变状态是否被多个执行路径并发访问,以及访问是否包含读改写、检查后执行、跨字段一致性、对象发布等风险。实战判断要结合代码审查、并发入口梳理、锁边界分析、压测和线上偶发症状,而不是依赖一次本地复现。
真实面经题目 · 原创解析
判断实际项目中是否会出现线程安全问题,核心不是先看有没有多线程,而是追踪共享可变状态是否被多个执行路径并发访问,以及访问是否包含读改写、检查后执行、跨字段一致性、对象发布等风险。实战判断要结合代码审查、并发入口梳理、锁边界分析、压测和线上偶发症状,而不是依赖一次本地复现。
我会按五步判断。第一,找共享可变状态,包括成员变量、static 字段、单例内部字段、缓存、集合、计数器、状态机、复用对象和上下文对象。第二,追访问路径,看这些状态是否会被 Web 请求线程、定时任务、消息消费、异步回调、线程池任务、事件监听器同时访问。第三,看操作性质,单次不可变读风险较低,但 i++、先查再插、先判断再扣减、遍历同时修改、多字段联动更新都属于复合操作,需要原子化。第四,看对象发布和不变式,构造未完成是否逃逸,多个字段是否必须同时一致。第五,看保护边界,锁是否覆盖所有相关读写,锁对象是否统一,并发集合是否只保证容器方法安全却没有保证业务流程安全。最后用并发压测、静态分析、关键日志和线上症状交叉验证。
线程安全判断的入口是状态。需要列出所有会被修改的成员变量、static 变量、单例字段、本地缓存、Map/List/Set、计数器、状态机、连接对象、复用 DTO、配置热更新对象。局部变量通常是线程私有的,但如果局部变量引用了共享对象,仍然有风险。只要同一份对象会被多个执行单元读写,就要继续分析。
很多并发问题不是一个方法内部暴露的,而是多个入口共同访问同一状态。要把 HTTP 请求线程、RPC 线程、消息消费者、定时任务、异步 CompletableFuture、线程池任务、事件监听器、缓存刷新线程、监控采集线程都纳入分析。只要两个路径可能重叠执行,就不能用正常顺序推断安全。
复合操作是高风险来源。典型例子包括读取后再写入、自增自减、先判断为空再初始化、缓存未命中后加载并回填、判断余额后扣减、遍历集合时另一个线程修改、根据多个字段计算新状态。这些操作表面可能只有一行代码,实际包含多个步骤,中间被其他线程插入就会导致丢失更新、重复初始化或业务不一致。
线程安全不只是程序不崩溃,还包括业务状态始终合法。例如库存不能小于零,订单状态不能回退,余额和流水必须一致,缓存索引和明细对象必须同步更新。判断时要把不变式写出来,再看每次修改是否在同一原子边界内完成。如果多个字段共同表达一个事实,只保护其中一个字段通常不够。
对象创建完成后交给其他线程使用叫发布。风险包括构造函数里把 this 注册到监听器,构造未完成就放进缓存,返回内部可变集合引用,延迟初始化单例缺少 volatile 或同步保护,配置对象更新后仍被外部修改。即使对象后续不复杂修改,如果其他线程看到未完全构造状态,也可能出现空字段或默认值。
ConcurrentHashMap、AtomicInteger、volatile、ThreadLocal 都有边界。ConcurrentHashMap 保证容器结构安全,但 containsKey 后 put 不一定是业务原子操作,map 里的普通 ArrayList value 仍可能被并发破坏。锁也要看是否保护完整不变式,所有读写是否用同一把锁,是否存在锁外读、锁对象不统一、在锁内调用外部服务等问题。
线程安全问题通常偶发,单次本地跑通没有证明价值。验证时可以用固定线程池、起跑屏障、高循环次数、随机短暂停顿和最终不变式校验,提高线程交错概率。线上排查要收集请求 ID、业务 ID、线程名、状态变更前后值、对象版本号和关键分支日志,把同一对象的多线程访问时间线拼出来。
可以问三个问题:它是否会被多个线程访问,它是否会被修改,修改结果是否影响业务。方法里的普通局部变量通常安全,因为每个线程有自己的栈帧;但对象成员变量、static 字段、缓存和集合常被多个请求共享。还要注意局部变量引用的对象是否共享。
ConcurrentHashMap 保证单个容器方法有并发语义,但不保证一整段业务逻辑自动原子化。比如先判断 key 不存在,再加载数据,再 put,这几个步骤之间可能被其他线程插入。再比如 value 是普通 ArrayList,多个线程取出同一个 value 后同时修改,问题发生在 value 上。
volatile 主要解决可见性和一定的有序性,适合开关标志、状态标记、单引用替换等场景。它不能让 i++ 这种读改写操作变成原子,也不能维护多个字段之间的一致性。计数可用 AtomicInteger 或 LongAdder,多字段不变式通常需要锁或更高层原子设计。
先扫共享状态:static 字段、单例字段、缓存、集合、可变配置、复用对象;再查并发入口:请求、定时任务、消息消费、异步任务;然后看复合操作、懒加载、读改写、多字段更新;最后看保护方式是否统一、是否锁外读写、是否对象发布安全。
线程安全问题常表现为偶发、难复现、与并发量相关、重试后成功、状态偶尔回退、计数不准、同一业务对象被重复处理。需要补充请求 ID、业务 ID、线程名、状态变更日志和版本号,把同一对象的访问时间线拼出来,再回到代码里确认原子边界。