调试一个诡异的并发Bug
问题描述
线上服务偶发 500 错误,错误率约 0.1%。日志显示 NullPointerException,但从代码逻辑上看,这个变量不可能为 null。
java
// 错误堆栈
java.lang.NullPointerException
at com.example.service.OrderService.processOrder(OrderService.java:127)
at com.example.controller.OrderController.createOrder(OrderController.java:45)本地无法复现,只在高并发时偶发。
传统思路
- 盯着代码看 - 反复阅读,找不到问题
- 加大量日志 - 上线后等待复现,可能要等几天
- 猜测+验证 - 凭经验猜测可能的原因,逐个排除
- 求助同事 - 组织 Code Review 会议,多人一起看
预计耗时:数小时到数天
AI思维
第一步:整理上下文
把所有相关信息整理清楚,一次性提供给 AI:
我遇到一个生产环境的偶发问题,请帮我分析可能的原因。
## 错误信息
java.lang.NullPointerException
at com.example.service.OrderService.processOrder(OrderService.java:127)
## 相关代码
```java
@Service
public class OrderService {
@Autowired
private UserCache userCache;
public void processOrder(Order order) {
// 第127行
User user = userCache.get(order.getUserId());
String username = user.getName(); // NPE发生在这里
// ... 后续逻辑
}
}
@Component
public class UserCache {
private Map<Long, User> cache = new HashMap<>();
public User get(Long userId) {
if (!cache.containsKey(userId)) {
User user = userRepository.findById(userId);
cache.put(userId, user);
}
return cache.get(userId);
}
public void refresh() {
cache.clear();
// 异步重新加载
loadAllUsers();
}
}上下文
- 问题在高并发时更容易出现(QPS > 1000)
- 有一个定时任务每5分钟刷新缓存
- 最近没有代码变更
- 错误率约 0.1%
请分析可能的原因,从最可能到最不可能排序。
### 第二步:AI 分析结果
AI(Claude)的回复:
> 这是一个典型的**并发安全问题**。我按可能性排序分析:
>
> **最可能的原因:HashMap 的线程安全问题**
>
> 你的 `UserCache` 使用了普通的 `HashMap`,这不是线程安全的。当 `refresh()` 方法清空缓存的同时,`get()` 方法可能正在执行,导致:
>
> 1. 线程A执行 `get()`,检查 `containsKey` 返回 false
> 2. 线程B执行 `refresh()`,调用 `cache.clear()`
> 3. 线程A从数据库加载用户,执行 `put()`
> 4. 线程A执行 `return cache.get(userId)` - 但此时线程B的 `clear()` 可能刚好执行完
>
> 更严重的是,HashMap 在并发修改时可能导致内部结构损坏,`get()` 返回 null 即使 key 存在。
>
> **修复方案:**
>
> ```java
> // 方案1:使用 ConcurrentHashMap
> private Map<Long, User> cache = new ConcurrentHashMap<>();
>
> // 方案2:使用读写锁(如果需要原子性刷新)
> private final ReadWriteLock lock = new ReentrantReadWriteLock();
> ```
### 第三步:验证假设
根据 AI 的分析,设计验证方案:AI的分析很有说服力。请帮我设计一个验证方案,确认这确实是并发问题:
- 如何在本地复现这个问题?
- 如何在不影响线上的情况下验证?
AI 给出的验证方案:
```java
// 本地压测复现
@Test
void testConcurrencyIssue() throws InterruptedException {
UserCache cache = new UserCache();
ExecutorService executor = Executors.newFixedThreadPool(100);
AtomicInteger nullCount = new AtomicInteger();
// 模拟并发读写
for (int i = 0; i < 10000; i++) {
executor.submit(() -> {
User user = cache.get(1L);
if (user == null) {
nullCount.incrementAndGet();
}
});
if (i % 100 == 0) {
executor.submit(() -> cache.refresh());
}
}
executor.shutdown();
executor.awaitTermination(10, TimeUnit.SECONDS);
System.out.println("Null count: " + nullCount.get());
// 如果 nullCount > 0,说明存在并发问题
}第四步:实施修复
验证确认是并发问题。请给我完整的修复代码,要求:
1. 使用 ConcurrentHashMap
2. 确保 refresh 操作的原子性
3. 不影响读取性能最终修复代码:
java
@Component
public class UserCache {
private volatile Map<Long, User> cache = new ConcurrentHashMap<>();
public User get(Long userId) {
return cache.computeIfAbsent(userId,
id -> userRepository.findById(id));
}
public void refresh() {
// 构建新缓存,然后原子替换
Map<Long, User> newCache = new ConcurrentHashMap<>();
loadAllUsers(newCache);
this.cache = newCache; // 原子替换引用
}
}结果对比
| 指标 | 传统方式 | AI思维 |
|---|---|---|
| 定位时间 | 数小时-数天 | 10分钟 |
| 准确性 | 依赖经验和运气 | 系统性分析 |
| 验证方案 | 可能想不全面 | 完整的测试代码 |
| 修复质量 | 可能引入新问题 | 考虑边界情况 |
关键洞察
- 信息完整性:给 AI 足够的上下文(代码、错误、环境信息)
- 选对模型:并发问题需要深度推理,用 Claude Opus 或 Sonnet
- 验证思维:AI 的分析需要验证,让 AI 帮你设计验证方案
- 迭代优化:修复后让 AI Review,避免引入新问题
延伸学习
- Java 并发编程常见陷阱 (coming soon)
- 如何写出线程安全的代码 (coming soon)