You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化同步块时Java双重检查锁失效:单例服务返回重复值

问题根源分析

你的双重检查锁失效的核心原因是**nextValue作为全局实例变量,被不同correlationId的请求互相覆盖**,完全失去了针对单个correlationId的缓存作用。我们来拆解一下两个线程同时处理首次请求(mapper返回null)时的执行流程:

假设线程A处理correlationId=X,线程B处理correlationId=Y:

  1. 线程A执行nextValue = mapper.mapToB(X) → 返回null,进入if(nextValue==null)分支,等待锁。
  2. 线程B执行nextValue = mapper.mapToB(Y) → 返回null,这里直接把线程A后续可能设置的nextValue覆盖成了null,然后进入if(nextValue==null)分支,等待锁。
  3. 线程A拿到锁,检查nextValue==null → 为true,调用rangeFetch.getAndIncrementAsString得到0000000019,设置nextValue=19,完成映射后退出锁,返回19。
  4. 线程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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.06 13:43:10