多线程 Bug 是程序员的噩梦。它不像 NullPointer 那样触手可及,而是隐蔽、偶发、难复现、难定位。
这篇推文,系统梳理排查并发问题的 6 个实战路径,结合 JDK 工具、日志策略与代码陷阱,让你少走弯路。
一、先确认症状:并发 Bug 的常见表现
在定位之前,明确目标。常见的多线程问题形式包括:
❌ 数据不一致(共享变量更新异常、脏读)
❌ 死锁(多个线程互相等待)
❌ 活锁(线程忙于协商却无法推进)
❌ 饥饿(线程长时间获取不到资源)
❌ 线程泄漏 / 创建过多线程(内存耗尽)
❌ 线程池阻塞(队列塞满)
❌ CPU 飙高(线程自旋、死循环)
搞清楚“Bug 是什么”永远比“怎么修”重要。
二、日志粒度不够?先加上线程 ID + 变量快照
日志,是排查并发 Bug 的第一利器,但很多日志存在两个问题:
不打印线程名([pool-1-thread-7] 是线索)
不打印关键共享变量状态(状态变化无从追踪)
建议:
log.info("[{}] 执行任务,当前状态为:{}", Thread.currentThread().getName(), state);
对多线程场景,还建议引入如下辅助:
任务唯一标识(traceId / taskId)
执行开始/结束时间戳
标记关键状态变更日志(变量变更时记录前后值)
三、借助 Java 提供的并发调试工具
1. jstack(定位死锁)
jstack
搜索关键词 Found one Java-level deadlock,直接定位锁等待链路。
2. VisualVM / JFR
可视化线程运行状态(RUNNABLE、WAITING)
查看锁竞争情况、阻塞栈
JFR 可分析方法调用热点、锁持有时间
3. Arthas
watch com.example.MyService method '{params, returnObj, target, thread}' -x 3
动态观察线程参数与状态
分析线程局部变量 + 调用栈
四、使用并发 Bug 注入法复现问题
如果问题是偶发的,必须人为制造冲突环境来扩大复现概率:
人为降低线程切换间隔(加 Thread.sleep(1))
打开 -XX:+UseLargePages 增强调度不可预知性
利用 CountDownLatch 精准控制多个线程同时进入临界区
实战中,构造这样的代码能快速暴露线程安全问题:
CountDownLatch latch = new CountDownLatch(1);
for (int i = 0; i < 1000; i++) {
new Thread(() -> {
latch.await();
// 执行共享操作
}).start();
}
latch.countDown();
五、从代码结构上初步甄别高危区域
重点检查以下几类代码段:
✅ 使用共享变量但无同步控制(volatile / synchronized / Lock)
✅ 使用集合类但未考虑线程安全(如使用 HashMap、ArrayList)
✅ 使用 ThreadLocal 后未调用 remove()
✅ 并发读写 List、Set、Map 时没有加锁或使用并发容器
✅ 缓存或队列类未加边界限制,可能 OOM
静态代码扫描工具如 SonarQube、IDEA 的 Inspections 也可辅助发现。
六、用正确的并发原语解决对应场景
不要滥用 synchronized,也不要轻信 volatile。选择合适的 JUC 工具:
场景
推荐工具
等待多个线程完成
CountDownLatch
限制并发访问量
Semaphore
分段任务同步点
CyclicBarrier
异步任务结果汇总
Future / CompletableFuture
高性能计数器
LongAdder
原子操作(无锁 CAS)
AtomicXXX
控制并发提交线程池任务
ThreadPoolExecutor + 队列策略
🔚 小结:并发 Bug 排查六步法
明确并发异常表现(死锁、脏读等)
精细化日志:打印线程名 + 关键变量
使用 jstack / VisualVM / Arthas 工具定位线程状态
模拟并发冲突重现 Bug
静态分析高危共享变量区域
用 JUC 原语针对性优化
七、为什么并发 Bug 难复现?
并发 Bug 极难通过常规单测手段捕捉:
❌ Bug 依赖特定线程调度时序(非确定性)
❌ JVM 优化、CPU 指令乱序、缓存同步导致行为不一致
❌ 多线程执行路径广,覆盖不全
你执行 100 次没问题,线上一个用户访问就能爆炸。
八、并发单测的核心策略:制造最大程度的冲突
想让 Bug 浮出水面,必须引导线程“撞车”。
方法一:使用 CountDownLatch 制造并发压力
模拟 1000 个线程同时访问共享资源:
@Test
public void testConcurrentBug() throws InterruptedException {
int threadCount = 1000;
CountDownLatch startGate = new CountDownLatch(1);
CountDownLatch endGate = new CountDownLatch(threadCount);
SharedService service = new SharedService();
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
startGate.await(); // 等待统一开始
service.doWork(); // 存在线程安全风险的方法
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endGate.countDown();
}
}).start();
}
startGate.countDown(); // 所有线程同时执行
endGate.await();
Assertions.assertTrue(service.getStateIsCorrect());
}
方法二:利用多轮循环 + 重试增加命中概率
for (int i = 0; i < 1000; i++) {
try {
testConcurrentBug();
} catch (AssertionError e) {
System.out.println("Fail at round: " + i);
throw e;
}
}
九、加入断言与状态检查机制
并发单测不止要“看不报错”,更要断言共享状态是否符合预期。
常见断言模式:
累加器结果一致性
最终状态校验(不会丢失更新)
条件变量是否误触发
举例:
AtomicInteger counter = new AtomicInteger();
Runnable task = () -> counter.incrementAndGet();
Assertions.assertEquals(1000, counter.get());
十、使用 JMH 做微基准测试验证并发性能边界
JMH(Java Microbenchmark Harness)
是官方支持的微基准框架,适合测试并发操作在不同线程数下的性能表现与行为一致性。
@Benchmark
@Threads(10)
public void testConcurrentAdd() {
map.putIfAbsent(Thread.currentThread().getName(), 1);
}
@Threads(n) 指定并发线程数
@State(Scope.Thread) 维持线程隔离状态
可观察吞吐量、锁竞争、响应时间分布等
十一、使用 JCStress 验证并发语义正确性(内存模型级别)
JCStress
是 JDK 官方并发一致性测试框架,能测试 CPU 缓存失效、JMM 指令重排导致的异常行为。
示例:
@JCStressTest
@Outcome(id = {"1,0", "0,1", "1,1"}, expect = Expect.ACCEPTABLE)
@Outcome(id = "0,0", expect = Expect.FORBIDDEN)
@State
public class VisibilityTest {
int a, b;
@Actor
public void writer() {
a = 1;
b = 1;
}
@Actor
public void reader(II_Result r) {
r.r1 = a;
r.r2 = b;
}
}
若 0,0 被命中,说明存在写入对读线程不可见的风险,即内存模型问题。
十二、引入 Thread Sanitizer / Intel Inspector 等原生竞态检测工具
这些工具通过原生字节码分析 / CPU 执行路径检查,能检测:
读写竞态
锁释放错误
内存泄露
线程逃逸
注意:使用这些工具需通过 JNI 或 JVM 编译参数开启调试符号。
十三、封装并发测试基类,提高测试复用效率
建议将并发测试封装为模板方法:
public abstract class AbstractConcurrentTest {
protected void runInParallel(int threadCount, Runnable task) {
CountDownLatch startGate = new CountDownLatch(1);
CountDownLatch endGate = new CountDownLatch(threadCount);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
startGate.await();
task.run();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endGate.countDown();
}
}).start();
}
startGate.countDown();
endGate.await();
}
}
测试类中只需覆写 task.run() 即可快速构造并发压测场景。
总结:写出“可复现”的并发测试需要什么?
✅ 控制并发执行时机(CountDownLatch)
✅ 多轮高频执行压测(loop + assert)
✅ 断言关键共享变量的正确性
✅ 借助 JMH / JCStress 等专业工具验证
✅ 重现场景封装,提升测试开发效率
Bug 不怕有,就怕你测不到。
多线程测试的核心不是测试执行,而是制造异常行为概率最大化的运行条件。