优化同步块时Java双重检查锁失效:单例服务返回重复值
问题根源分析
你的双重检查锁失效的核心原因是**nextValue作为全局实例变量,被不同correlationId的请求互相覆盖**,完全失去了针对单个correlationId的缓存作用。我们来拆解一下两个线程同时处理首次请求(mapper返回null)时的执行流程:
假设线程A处理correlationId=X,线程B处理correlationId=Y:
- 线程A执行
nextValue = mapper.mapToB(X)→ 返回null,进入if(nextValue==null)分支,等待锁。 - 线程B执行
nextValue = mapper.mapToB(Y)→ 返回null,这里直接把线程A后续可能设置的nextValue覆盖成了null,然后进入if(nextValue==null)分支,等待锁。 - 线程A拿到锁,检查
nextValue==null→ 为true,调用rangeFetch.getAndIncrementAsString得到0000000019,设置nextValue=19,完成映射后退出锁,返回19。 - 线程B拿到锁,此时
nextValue已经被它自己之前的mapper.mapToB(Y)赋值为null,所以检查nextValue==null→ 仍为true,再次调用rangeFetch.getAndIncrementAsString得到0000000020,设置nextValue=20,完成映射后返回20。
哪怕是同一correlationId的两个请求,如果其中一个线程在等待锁时,另一个线程已经修改了nextValue,但等待的线程又通过mapper.mapToB把nextValue改回null,同样会导致重复执行rangeFetch。
修复方案
你需要把缓存从全局变量改为按correlationId维度隔离,使用线程安全的容器来存储每个correlationId对应的已映射值。推荐用ConcurrentHashMap,它本身支持高效的并发操作,结合双重检查锁可以避免不必要的同步开销:
public class RangeQueryService { // 用ConcurrentHashMap缓存每个correlationId对应的nextValue private final ConcurrentHashMap<String, String> correlationValueCache = new ConcurrentHashMap<>(); private static final Object LOCK = new Object(); // 独立全局锁,避免外部干扰 public String getNextIncrement(String name, String correlationId) throws SomeCheckedException { try { if (correlationId == null) { return rangeFetch.getAndIncrementAsString(name); } else { // 先查缓存,避免重复查询mapper String cachedValue = correlationValueCache.get(correlationId); if (cachedValue != null) { return cachedValue; } // 查询mapper,若已有映射则存入缓存返回 String mappedValue = mapper.mapToB(SOME_CONST, correlationId); if (mappedValue != null) { correlationValueCache.put(correlationId, mappedValue); return mappedValue; } // 仅当缓存和mapper都无值时,进入同步分支 synchronized (LOCK) { // 双重检查:防止等待锁期间其他线程已完成映射 cachedValue = correlationValueCache.get(correlationId); if (cachedValue != null) { return cachedValue; } mappedValue = mapper.mapToB(SOME_CONST, correlationId); if (mappedValue != null) { correlationValueCache.put(correlationId, mappedValue); return mappedValue; } // 执行增量获取并映射,最后存入缓存 String nextValue = rangeFetch.getAndIncrementAsString(name); idMapper.mapToB(SOME_CONST, correlationId, nextValue, DURATION); correlationValueCache.put(correlationId, nextValue); return nextValue; } } } catch (Exception e) { throw new SomeCheckedException("Error!" + e.getMessage()); } } }
关键优化点:
- 缓存隔离:用
ConcurrentHashMap给每个correlationId单独存储映射值,彻底避免全局变量的互相覆盖问题。 - 前置缓存检查:优先查询缓存再查mapper,减少不必要的数据库访问和同步开销。
- 双重检查锁升级:同步块内再次检查缓存和mapper,防止等待锁期间其他线程已经完成映射操作。
- 锁对象优化:使用独立的
LOCK对象,避免用this锁可能带来的外部干扰(比如其他代码也锁定了该实例)。
内容的提问来源于stack exchange,提问作者Sameer
相关产品推荐
相关产品推荐

