背景与问题
并发问题最烦人的地方,不是它“难”,而是它偶发、隐蔽、难复现。你本地跑 100 次没事,一上线在流量高峰就炸;日志里看起来都正常,但结果就是少数据、重复扣款、线程池打满、接口超时。
我自己做 Java 服务时,最怕看到这几类告警:
- 订单金额统计偶尔不准
- 某个接口 RT 突然从 50ms 飙到 5s
- CPU 不高,但请求就是卡死
- 线程数一路上涨,最后 OOM
- 明明加了
synchronized,结果还是线程不安全
这些现象背后,往往不是单点 bug,而是对并发模型理解不完整:可见性、原子性、有序性、锁竞争、线程池策略、容器线程安全、死锁、上下文切换等问题交织在一起。
这篇文章不打算泛泛讲概念,而是按“现象 -> 定位 -> 修复”的 troubleshooting 方式,拆 8 个 Java 开发中最常见的并发坑,并给出可运行代码和排查路径。
核心原理
并发排查之前,先把三个关键词钉牢:
1. 原子性
一个操作要么全部完成,要么完全不做。
比如 count++ 看起来是一行,实际上通常分三步:
- 读取
count count + 1- 写回
count
多个线程同时做这件事,就会丢失更新。
2. 可见性
一个线程改了共享变量,另一个线程能不能马上看到?
如果没有同步手段,线程可能一直读到旧值。
典型修复手段:
volatilesynchronizedLock- 并发容器
- 原子类
3. 有序性
编译器和 CPU 可能会做指令重排。
单线程下没问题,多线程下可能触发“看起来不可能”的现象,比如双重检查锁失效。
flowchart TD
A[并发异常现象] --> B{表现类型}
B --> C[结果不准]
B --> D[偶发卡死]
B --> E[吞吐下降]
B --> F[线程暴涨]
C --> G[原子性/可见性问题]
D --> H[死锁/锁等待/阻塞队列]
E --> I[锁竞争/线程池配置不当]
F --> J[线程泄漏/任务堆积]
并发排查的一个通用脑图
我一般按下面这条线排:
- 先看现象:数据错了?慢了?卡死?OOM?
- 再看线程状态:
RUNNABLE、BLOCKED、WAITING、TIMED_WAITING - 再看共享资源:变量、集合、缓存、连接池、线程池、锁
- 最后确认 happens-before 关系:有没有同步保护?
sequenceDiagram
participant U as 用户请求
participant T1 as 业务线程A
participant T2 as 业务线程B
participant S as 共享资源
U->>T1: 更新共享变量
U->>T2: 读取共享变量
alt 没有同步措施
T1-->>S: 写入可能延迟可见
T2-->>S: 读取旧值
else 使用 volatile/锁/原子类
T1-->>S: 写入建立可见性
T2-->>S: 读取最新值
end
现象复现
下面 8 个问题,我会尽量按“线上最常见的表现”来组织。
count++结果不对volatile用错,以为能保证线程安全HashMap并发写导致异常数据SimpleDateFormat多线程下解析错乱- 死锁导致接口卡死
- 线程池配置不当,请求堆积
ThreadLocal使用不当导致内存泄漏或脏数据- 锁粒度过大,吞吐明显下降
实战代码(可运行)
下面这段示例代码把 8 个坑拆成了多个可独立运行的方法。为了便于阅读,我把它们放在一个类里,运行时手动打开对应测试即可。
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrentPitfallsDemo {
// 1. 原子性问题
private static int unsafeCounter = 0;
private static AtomicInteger safeCounter = new AtomicInteger(0);
// 2. volatile 仅保证可见性
private static volatile int volatileCounter = 0;
// 3. 非线程安全集合
private static Map<Integer, Integer> unsafeMap = new HashMap<>();
private static Map<Integer, Integer> safeMap = new ConcurrentHashMap<>();
// 4. 非线程安全工具类
private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
// 5. 死锁演示
private static final Object lockA = new Object();
private static final Object lockB = new Object();
// 7. ThreadLocal
private static final ThreadLocal<String> context = new ThreadLocal<>();
// 8. 锁粒度问题
private static final ReentrantLock bigLock = new ReentrantLock();
private static final ConcurrentHashMap<String, AtomicInteger> segmentedCounter = new ConcurrentHashMap<>();
public static void main(String[] args) throws Exception {
testAtomicityProblem();
testVolatileMisuse();
testHashMapProblem();
testSimpleDateFormatProblem();
testDeadlock();
testThreadPoolProblem();
testThreadLocalProblem();
testLockGranularity();
}
// 1) count++ 丢失更新
public static void testAtomicityProblem() throws InterruptedException {
unsafeCounter = 0;
safeCounter.set(0);
int threadCount = 20;
int loop = 10000;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
pool.execute(() -> {
for (int j = 0; j < loop; j++) {
unsafeCounter++;
safeCounter.incrementAndGet();
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
System.out.println("期望值: " + (threadCount * loop));
System.out.println("unsafeCounter: " + unsafeCounter);
System.out.println("safeCounter: " + safeCounter.get());
}
// 2) volatile 不能保证复合操作原子性
public static void testVolatileMisuse() throws InterruptedException {
volatileCounter = 0;
int threadCount = 10;
int loop = 10000;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
pool.execute(() -> {
for (int j = 0; j < loop; j++) {
volatileCounter++;
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
System.out.println("volatileCounter 期望值: " + (threadCount * loop));
System.out.println("volatileCounter 实际值: " + volatileCounter);
}
// 3) HashMap 并发写
public static void testHashMapProblem() throws InterruptedException {
unsafeMap.clear();
safeMap.clear();
int threadCount = 20;
int loop = 1000;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
for (int i = 0; i < threadCount; i++) {
final int threadNo = i;
pool.execute(() -> {
for (int j = 0; j < loop; j++) {
int key = threadNo * loop + j;
unsafeMap.put(key, j);
safeMap.put(key, j);
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
System.out.println("unsafeMap size: " + unsafeMap.size());
System.out.println("safeMap size: " + safeMap.size());
}
// 4) SimpleDateFormat 并发问题
public static void testSimpleDateFormatProblem() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(20);
CountDownLatch latch = new CountDownLatch(100);
for (int i = 0; i < 100; i++) {
pool.execute(() -> {
try {
Date date = sdf.parse("2023-06-27 12:34:56");
String formatted = sdf.format(date);
if (!"2023-06-27 12:34:56".equals(formatted)) {
System.out.println("格式化异常: " + formatted);
}
} catch (ParseException e) {
System.out.println("解析异常: " + e.getMessage());
} finally {
latch.countDown();
}
});
}
latch.await();
pool.shutdown();
}
// 5) 死锁
public static void testDeadlock() throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) {
System.out.println("t1 done");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) {
System.out.println("t2 done");
}
}
});
t1.start();
t2.start();
Thread.sleep(500);
System.out.println("如果程序卡住,可能发生了死锁");
}
// 6) 线程池配置不当
public static void testThreadPoolProblem() throws InterruptedException {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2,
2,
60,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(2),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 10; i++) {
final int taskNo = i;
try {
executor.execute(() -> {
System.out.println("task " + taskNo + " start");
sleep(1000);
System.out.println("task " + taskNo + " end");
});
} catch (RejectedExecutionException e) {
System.out.println("task " + taskNo + " 被拒绝");
}
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
}
// 7) ThreadLocal 未清理
public static void testThreadLocalProblem() throws InterruptedException {
ExecutorService pool = Executors.newFixedThreadPool(1);
pool.execute(() -> {
context.set("userA");
System.out.println("第一次请求设置: " + context.get());
// 故意不 remove
});
Thread.sleep(500);
pool.execute(() -> {
System.out.println("第二次请求读取: " + context.get());
context.remove();
});
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
}
// 8) 锁粒度过大
public static void testLockGranularity() throws InterruptedException {
int threadCount = 10;
int loop = 10000;
CountDownLatch latch = new CountDownLatch(threadCount);
ExecutorService pool = Executors.newFixedThreadPool(threadCount);
long start = System.currentTimeMillis();
for (int i = 0; i < threadCount; i++) {
final int threadNo = i;
pool.execute(() -> {
for (int j = 0; j < loop; j++) {
String key = "k" + (threadNo % 4);
// 大锁,所有 key 竞争同一把锁
bigLock.lock();
try {
segmentedCounter.computeIfAbsent(key, k -> new AtomicInteger()).incrementAndGet();
} finally {
bigLock.unlock();
}
}
latch.countDown();
});
}
latch.await();
pool.shutdown();
long cost = System.currentTimeMillis() - start;
System.out.println("大锁耗时(ms): " + cost);
System.out.println("结果: " + segmentedCounter);
}
private static void sleep(long ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException ignored) {
}
}
}
8个高频并发问题的排查思路与修复方案
1. count++ 结果不对:典型原子性缺失
现象
- 计数器偏小
- 请求量越大,误差越明显
- 本地单线程测试完全正常
为什么会这样
count++ 不是原子操作,多线程同时更新时会覆盖彼此的结果。
定位路径
先看以下代码有没有共享变量的复合操作:
++--put if absent的手工实现if (x == null) x = new ...- 余额扣减、库存扣减
如果日志里只有“最终值不对”,但没有报错,这类问题概率很高。
修复方案
按场景选:
- 简单计数:
AtomicInteger - 高并发热点计数:
LongAdder - 需要复合一致性:
synchronized/Lock
修复示例
private final AtomicInteger counter = new AtomicInteger();
public void incr() {
counter.incrementAndGet();
}
2. volatile 用错:能保证可见性,不保证复合操作安全
现象
- 变量最新值能看见,但统计仍然不准
- 开发同学说:“我都加
volatile了怎么还不行?”
问题根源
volatile 解决的是:
- 一个线程写,其他线程可见
- 禁止部分重排序
但它不保证多个线程同时 ++ 的原子性。
常见误区
错误写法:
private volatile int count = 0;
public void incr() {
count++;
}
正确边界
适合 volatile 的典型场景:
- 停机标志位
- 配置开关
- 单次写、多次读状态位
例如:
private volatile boolean running = true;
public void shutdown() {
running = false;
}
public void work() {
while (running) {
// do something
}
}
3. HashMap 并发写:数据丢失、覆盖甚至结构异常
现象
size不准- 数据偶发丢失
- 遍历时报错
- 压测时问题明显,开发环境不稳定复现
为什么是坑
HashMap 不是线程安全的。多个线程同时写同一个 HashMap,结果不可预期。
排查方法
重点搜这些位置:
static Map- 缓存容器
- 单例服务里的成员变量
List/Set/Map作为共享对象
修复方案
- 并发读写:
ConcurrentHashMap - 读多写少:不可变对象 + 原子替换
- 复合操作:用
computeIfAbsent等原子 API
推荐写法
private final ConcurrentHashMap<String, Integer> cache = new ConcurrentHashMap<>();
public void put(String key, Integer value) {
cache.put(key, value);
}
更进一步,避免“先查再放”的竞态:
cache.computeIfAbsent("k1", k -> 1);
4. SimpleDateFormat 线程不安全:老项目里非常常见
现象
- 时间解析偶尔异常
- 格式化结果错乱
- 很难稳定复现
根因
SimpleDateFormat 内部维护可变状态,多线程复用同一个实例会互相污染。
排查建议
全局搜:
static final SimpleDateFormat- Spring 单例 Bean 里注入的
SimpleDateFormat
修复方案
优先使用 Java 8+ 的 DateTimeFormatter,它是线程安全的。
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class SafeDateDemo {
private static final DateTimeFormatter FMT =
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
public static void main(String[] args) {
String s = LocalDateTime.now().format(FMT);
System.out.println(s);
}
}
止血方案
如果短期内改动有限,可以:
- 每次 new 一个
SimpleDateFormat - 或者用
ThreadLocal<SimpleDateFormat>
但长期建议直接迁移到 java.time。
5. 死锁:线程都活着,但业务像“挂了”
现象
- 接口一直不返回
- CPU 不一定高
- 线程池线程都卡着
- 应用没崩,但就是处理不过来
常见场景
- 锁顺序不一致
- 嵌套锁
- 数据库锁 + Java 锁混合等待
- 多资源申请没有统一顺序
定位路径
线上最实用的两个动作:
jstack <pid>- 看线程 dump 中是否出现
Found one Java-level deadlock
死锁关系图
flowchart LR
T1[线程1] -->|持有| A[锁A]
T1 -->|等待| B[锁B]
T2[线程2] -->|持有| B
T2 -->|等待| A
修复方案
- 所有线程按统一顺序加锁
- 尽量避免嵌套锁
- 用
tryLock + 超时降低永久等待风险
示例:
ReentrantLock lock1 = new ReentrantLock();
ReentrantLock lock2 = new ReentrantLock();
public void safeMethod() throws InterruptedException {
if (lock1.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
// do work
} finally {
lock2.unlock();
}
}
} finally {
lock1.unlock();
}
}
}
6. 线程池配置不当:不是越大越好
现象
- 请求高峰时 RT 急剧上升
- 任务堆积
- 拒绝策略频繁触发
- 内存上涨,队列越积越多
常见错误
错误一:直接用 Executors.newFixedThreadPool
它默认使用无界队列,流量打进来时可能一直堆任务,最终把内存拖垮。
错误二:核心线程太少,队列太大
表现是:
- 线程没打满
- 任务已经排很长队
- 用户感知很慢
错误三:CPU 密集和 IO 密集用同一套参数
这类问题特别常见。
排查指标
重点看:
activeCountpoolSizequeue.size()- 任务平均执行时间
- 拒绝次数
- 最大线程数是否触顶
修复建议
自己显式构造线程池,不要偷懒:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
8,
16,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(200),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy()
);
参数边界
- CPU 密集:线程数接近 CPU 核数
- IO 密集:线程数可以更高,但要看外部资源承载能力
- 队列不能无限大
- 拒绝策略要结合业务兜底
7. ThreadLocal 用完不清理:线程复用后数据串请求
现象
- 用户 A 的上下文跑到用户 B 请求里
- 某些线程本地变量长期不释放
- 在线程池场景下问题明显
为什么会这样
ThreadLocal 的值挂在线程对象上。线程池线程会被复用,如果你不 remove(),下一个任务可能读到上一次残留的数据。
定位路径
重点看:
- 用户上下文
- TraceId
- 租户信息
- 数据源切换
- 权限上下文
修复方案
必须在 finally 中清理:
public void process(String userId) {
try {
context.set(userId);
// do work
} finally {
context.remove();
}
}
一条经验
如果项目里 ThreadLocal 很多,但没有统一过滤器、拦截器、AOP 清理,那基本迟早出问题。
8. 锁粒度过大:功能对了,性能却崩了
现象
- 没有错误,但吞吐很差
- 线程大量
BLOCKED - CPU 利用率不高,响应却变慢
本质问题
大家都抢同一把锁,串行化太严重。
最常见的写法就是“大而全”同步块,把本可并行的逻辑全包起来。
典型反例
public synchronized void update(String key) {
// 所有 key 共用同一把锁
}
优化思路
- 缩小临界区
- 分段锁
- 使用并发容器
- 读写分离时考虑
ReadWriteLock - 避免在锁内做 IO、RPC、数据库操作
状态变化示意
stateDiagram-v2
[*] --> Running
Running --> Blocked: 竞争大锁
Blocked --> Running: 获取到锁
Running --> Waiting: 条件等待/阻塞调用
Waiting --> Running: 被唤醒
Running --> [*]
一个很实用的判断标准
如果锁保护的代码里包含以下任意一种,基本就该警觉了:
- 远程调用
- SQL 查询
- 文件操作
- 大对象创建
- 复杂循环
常见坑与排查
这部分我尽量给一条可落地的排查清单,线上救火时很好用。
1. 先看线程栈,不要先猜
常用命令:
jstack <pid> > thread_dump.txt
看重点:
- 大量
BLOCKED:锁竞争严重 - 大量
WAITING:可能在等队列、条件变量、Future - 大量
TIMED_WAITING:可能 sleep、poll、超时等待 - 少数线程一直卡在相同代码行:高概率热点锁或死锁
2. 对“共享变量”做全文搜索
全局搜这些关键词很有用:
static
HashMap
ArrayList
SimpleDateFormat
ThreadLocal
synchronized
lock()
volatile
尤其注意单例 Bean 的成员变量,这里最容易藏线程安全问题。
3. 区分“功能正确”和“并发正确”
很多代码在单线程测试里是对的,但并发下不成立。
例如:
if (!map.containsKey(key)) {
map.put(key, value);
}
这是典型的 check-then-act 竞态条件。
应该改成:
map.putIfAbsent(key, value);
或者:
map.computeIfAbsent(key, k -> value);
4. 不要只盯代码,也要看运行时配置
比如线程池问题,代码可能没错,错在配置:
- 核心线程数太小
- 队列太大
- 拒绝策略不合理
- 下游依赖慢导致线程被占满
5. 复现时尽量加压,不要只跑一两次
并发 bug 很吃时序。复现技巧:
- 提高线程数
- 加循环次数
- 用
CountDownLatch对齐起跑时间 - 多跑几轮
- 把日志打在线程名、对象 id、锁对象上
定位路径
如果你在线上遇到“偶发数据错乱或接口卡住”,我建议按这个顺序走:
第一步:判断是“错”还是“慢”
- 结果错:优先查原子性、可见性、线程不安全容器
- 响应慢/卡死:优先查锁、线程池、死锁、外部依赖阻塞
第二步:看线程池和线程状态
重点回答几个问题:
- 是线程不够,还是线程都在等?
- 是队列积压,还是任务执行太慢?
- 是锁竞争,还是外部 IO 阻塞?
第三步:缩小共享资源范围
列出所有可能被多线程同时访问的对象:
- 全局缓存
- 单例 Bean 成员变量
- 工具类静态对象
- 上下文变量
- 连接池/线程池/阻塞队列
第四步:验证修复是否真的生效
不要“改完看着没报错就上线”。
至少做这几件事:
- 压测复现前后的差异
- 观察错误率和 RT
- 看线程数、队列长度、GC
- 做多轮验证,避免只碰巧没复现
止血方案
有些并发问题线上已经在炸,先止血,再彻底修。
可快速止血的办法
1. 降并发
- 限流
- 熔断
- 降级
- 缩短调用链
2. 临时串行化关键路径
比如库存扣减、余额变更,先用粗锁保正确,再优化性能。
3. 缩小线程池入口流量
如果任务堆积明显:
- 调小入口 QPS
- 启用拒绝保护
- 快速失败,避免系统雪崩
4. 关闭有风险的异步任务
很多事故不是主流程炸,而是异步消费者无限堆积把系统拖死。
安全/性能最佳实践
1. 优先使用成熟并发工具,而不是手写同步
优先级通常是:
java.util.concurrent- 原子类
- 并发容器
CompletableFuture- 显式锁
少写“自己觉得线程安全”的逻辑。
2. 共享可变状态越少越好
这是最根本的办法。
如果能做到:
- 无状态服务
- 方法内局部变量
- 不可变对象
很多并发 bug 会直接消失。
3. 锁内不要做慢操作
锁内做这些最危险:
- RPC
- SQL
- HTTP 调用
- 日志大量拼接
- 复杂对象序列化
4. 线程池要显式命名、显式配置、显式监控
建议至少监控:
- 活跃线程数
- 队列长度
- 拒绝次数
- 任务执行耗时
- 最大线程利用率
5. 对上下文类数据统一收口
像 ThreadLocal、TraceId、租户信息,最好由:
- Filter
- Interceptor
- AOP
统一设置与清理,不要散落在业务代码里。
6. 选择正确的数据结构
不要拿 HashMap、ArrayList 去硬扛并发。
也不要所有场景都上重锁。先看场景:
- 高并发计数:
LongAdder - 并发字典:
ConcurrentHashMap - 生产消费:
BlockingQueue - 读多写少:不可变 + 复制替换
7. 建立并发代码的 Code Review 清单
我比较推荐团队固定检查这几项:
- 有没有共享可变状态?
- 有没有复合操作未加保护?
- 容器是否线程安全?
- 线程池是否有边界?
ThreadLocal是否清理?- 锁顺序是否一致?
- 锁内是否做了慢操作?
总结
Java 并发问题真正难的,不是 API 多,而是表面现象和真正根因往往隔着一层甚至几层。一个“统计不准”,可能是 count++;一个“接口偶发超时”,可能是死锁;一个“内存慢慢涨”,可能是线程池无界队列,也可能是 ThreadLocal 没清理。
这篇文章拆了 8 个高频坑,核心可以浓缩成一句话:
先判断是原子性、可见性、锁竞争还是资源配置问题,再沿着线程状态和共享资源去定位。
如果你让我给最实用的建议,我会给这 4 条:
- 共享可变状态能少就少
- 并发容器和原子类优先于手写同步
- 线程池一定要有边界
- 出了并发问题先看线程栈,不要靠猜
最后补一个边界条件:
不是所有并发问题都该用“更重的锁”解决。对于资金、库存这类强一致场景,先保正确;对于统计、监控这类弱一致场景,可以换吞吐更高的方案。修复并发 bug,永远要把业务一致性要求一起带上看。
如果你现在就想在项目里做一次快速自检,最值得先查的地方就是:
- 单例 Bean 成员变量
static集合与工具类- 自定义线程池
ThreadLocal- 共享缓存与计数逻辑
这几个地方,往往一查一个准。