Spring Boot多线程场景下代码块非线程安全及空值异常排查求助
代码非线程安全原因分析及高性能解决方案
核心问题场景
Spring Boot应用通过调度任务从数据库拉取数据存入ConcurrentHashMap(tempMap),多线程基于key查询时,在tempMap未刷新的情况下出现返回空Optional,调用.get()抛出No value present异常,同时存在线程安全隐患。
非线程安全的具体原因
1. getSingle方法的错误Optional用法
tempMap.get(value)可能返回null(key不存在或刷新竞态导致),但代码中使用Optional.of(...)而非Optional.ofNullable(...)——虽会直接抛出NullPointerException,但如果刷新时存在可见性问题,线程读取到旧的空map,也会触发后续.get()的异常。更关键的是,该方法未正确处理key不存在的场景。
2. getMultiple方法的竞态条件
getMultiple中先执行tempMap.containsKey(value)再调用tempMap.get(s),这两个操作并非原子:
- 线程A执行
containsKey(value)返回true - 此时调度任务刷新
tempMap,移除了该key - 线程A再执行
get(s)返回null,map操作将其转为Optional.empty() - 最终调用
.get()抛出No value present异常
3. tempMap刷新的可见性与原子性缺失
即使ConcurrentHashMap本身线程安全,但如果刷新操作存在以下问题:
- 直接在原有map上执行
clear()+putAll(),这两个操作非原子,中间线程会看到空或半填充的map tempMap变量未用volatile修饰,刷新时替换map引用后,其他线程可能无法立刻看到最新的map,仍读取旧的失效数据
高性能修复方案
1. 修复查询方法的竞态与Optional用法
- 修正
getSingle:用ofNullable正确处理null值:
private Optional<DummyClassEntity> getSingle(String value) { return Optional.ofNullable(tempMap.get(value)); }
- 重构
getMultiple:直接通过get获取值并过滤null,消除containsKey与get的竞态:
private Optional<DummyClassEntity> getMultiple(String[] values) { return Arrays.stream(values) .map(tempMap::get) .filter(Objects::nonNull) .findFirst(); }
2. 保证刷新操作的原子性与可见性
推荐原子替换map引用的方式,性能最优且无锁:
// 用volatile保证引用的可见性 private volatile ConcurrentHashMap<String, DummyClassEntity> tempMap = new ConcurrentHashMap<>(); // 调度任务中的刷新逻辑 public void refreshTempMap() { // 先在新map中加载完整数据 ConcurrentHashMap<String, DummyClassEntity> newMap = new ConcurrentHashMap<>(); loadDataFromDatabase(newMap); // 原子替换引用,所有线程立刻看到完整的新map tempMap = newMap; }
这种方式下,线程要么读取旧的完整map,要么读取新的完整map,不会看到中间的无效状态,无需加锁,性能不受影响。
3. 安全处理Optional空值
调用方避免直接使用.get(),改用更安全的方式处理空场景:
if (sender.length == 1) { return getSingle(sender[0]) .orElseThrow(() -> new IllegalArgumentException("未找到key: " + sender[0])); } else { return getMultiple(sender) .orElseThrow(() -> new IllegalArgumentException("传入的key均无匹配数据")); }
内容的提问来源于stack exchange,提问作者Zedark
相关产品推荐
相关产品推荐

