排查全局变量竞态需先识别被多线程读写的非final可变全局变量,再通过放大并发、添加线程日志复现竞争,最后用atomic类、并发容器或threadlocal修复,并从作用域控制和静态扫描预防。

排查全局变量作用域过大引发的竞态条件,核心是识别“谁在什么时候改了什么”,而不是只看变量声明位置。问题往往不出现在定义处,而藏在多线程并发调用路径中对它的读写交叠。
定位被意外共享的全局变量
先确认哪些全局变量实际参与了并发写入:
- 检查所有 非 final、非常量、可变类型 的模块级变量(如 Python 的
counter = 0、Java 的public static int count) - 重点关注被 多个线程启动的方法(如 Runnable、Thread.run()、@Async 方法、并行流 forEach 内部)直接或间接修改的变量
- 留意看似只读、实则内部修改状态的工具类——例如单例格式化器、静态缓存容器、全局配置对象的 setter 调用
- 用 IDE 的 “Find Usages” 功能追踪该变量所有读写点,标出是否跨线程执行路径
复现与观察竞态行为
竞态不是必现 bug,需主动制造竞争窗口:
- 将并发线程数设为 CPU 核心数的 2–4 倍(如
ForkJoinPool.commonPool().setParallelism(8)),数据量放大至 1000+ 条 - 在关键读写前后插入日志,包含线程名 + 变量值 + 时间戳,例如:
[pool-1-thread-3] before write: counter=42 → [pool-1-thread-2] read: counter=42 → [pool-1-thread-3] after write: counter=43 - 运行 50 次以上,统计结果分布:若总和/计数/集合大小出现多个不同值,基本可确认存在竞态
- 用
jstack或jcmd <pid> VM.native_memory summary</pid>观察线程是否频繁阻塞在同步块或容器 add 方法内
验证修复方案的有效性
优先选择侵入小、语义明确的修正方式:
- 把普通整型计数器换成 AtomicInteger,用
incrementAndGet()替代++;浮点数用AtomicLong或DoubleAccumulator - 若需收集对象列表,弃用
list.add()+ 外部 List,改用stream.parallel().map(...).collect(Collectors.toList()) - 必须保留外部容器时,用 ConcurrentLinkedQueue 或 CopyOnWriteArrayList(注意后者写操作开销大,仅适合读远多于写的场景)
- 对复杂对象状态,用 ThreadLocal 隔离每线程副本(如
private static final ThreadLocal<simpledateformat> sdfHolder = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));</simpledateformat>)
预防:缩小作用域 + 显式标注
从编码习惯上切断隐患源头:
- 默认不声明全局可变变量;需要跨方法传递的状态,通过参数传入,由调用方负责线程安全
- 确需全局状态时,在变量声明旁加注释说明同步策略,例如:
// @SharedState: guarded by 'synchronized(Config.class)' - 单元测试中增加并发用例:用
ExecutorService提交 10+ 相同任务,断言最终状态符合预期 - 静态代码扫描接入规则(如 SonarQube 的 S2275),自动告警非 final 全局可变变量的赋值行为











