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

并发代码是否存在竞态?DataProvider需锁定所有IDataManager吗?

并发问题:多IDataManager实例遍历的线程安全问题

背景代码

IDataManager接口

interface IDataManager
{
  Integer GetData(String key); // 未找到数据时返回null
  void UpdateData(String key, int value);
}

DataProvider类

public class DataProvider {
    private final List<IDataManager> dataManagers; // 不可变,通过构造函数初始化

    public DataProvider(List<IDataManager> dataManagers) {
        this.dataManagers = dataManagers;
    }

    public List<Integer> getData(String key) {
        List<Integer> found = new ArrayList<>();

        for (int i = 0; i < dataManagers.size(); i++) {
            IDataManager dataManager = dataManagers.get(i);
            Integer data = dataManager.GetData(key);
            
            if (data != null) {
                found.add(data);
            }
        }

        return found;
    }
}

问题描述

当部分线程执行DataProvider.getData()时,其他线程可能调用单个IDataManager实例的UpdateData方法更新数据。虽然每个IDataManager自身已实现正确同步,但我担心它们独立变化会带来问题:是否需要在遍历所有实例前,获取每个IDataManager的锁(假设锁机制支持)?

这个疑问源于《Java并发编程实战》清单4.10的示例:

public class NumberRange {
 // 不变量:lower <= upper
 private final AtomicInteger lower = new AtomicInteger(0);
 private final AtomicInteger upper = new AtomicInteger(0);

 public void setLower(int i) {
    // 警告:不安全的检查再执行操作
    if (i > upper.get())
    throw new IllegalArgumentException("can’t set lower to " + i + " > upper");
    lower.set(i);
 }

 public void setUpper(int i) {
    // 警告:不安全的检查再执行操作
    if (i < lower.get())
    throw new IllegalArgumentException("can’t set upper to " + i + " < lower");
    upper.set(i);
 }

 public boolean isInRange(int i) {
    return (i >= lower.get() && i <= upper.get());
 }
}

示例中isInRange方法分别读取lower和upper的值,未在同一锁保护下执行,这和我的DataProvider.getData()场景类似。

解答

首先要明确两个场景的核心差异,再根据业务需求决定是否加锁:

1. 对比两个场景的本质区别

NumberRange的问题在于类的不变量(lower <= upper)可能被破坏:setLower和setUpper的检查-执行操作不是原子的,导致isInRange可能读到不一致的状态(比如先读了较小的lower,之后upper被改得比lower还小,此时返回的结果不符合实际的范围约束)。

而你的DataProvider场景,不存在跨实例的不变量约束——每个IDataManager是独立的,遍历过程中单个实例的更新不会破坏整体的逻辑约束,只会影响该实例对应的数据项。

2. 是否需要加全局锁,取决于业务对结果的一致性要求

  • 不需要加锁的场景:如果业务只需要获取每个IDataManager在读取瞬间的最新值,不要求所有结果对应同一时间点的快照,那无需额外加锁。因为每个IDataManager的GetData已经是线程安全的,遍历过程中某实例被更新,只是意味着你拿到的是它的最新状态,这在大多数收集多源数据的场景下是可接受的。
  • 需要加锁的场景:如果业务要求返回的列表是所有IDataManager在同一时刻的状态快照,那必须加全局锁。否则遍历过程中,前面的实例已读取,后面的实例被更新,会导致列表中的数据是不同时间点的混合状态,不符合快照要求。

3. 若要加锁的注意事项

如果确定需要加全局锁,必须注意:

  • 固定锁的获取顺序:必须按统一的顺序获取所有IDataManager的锁(比如按实例的hashCode排序,或构造时的列表顺序),否则可能引发死锁。
  • 性能权衡:这种全局锁会大幅降低并发性能——getData()执行期间,所有UpdateData操作都会被阻塞,反之亦然。如果对性能敏感,可以考虑替代方案:比如让每个IDataManager支持快照读取(如基于Copy-On-Write的实现),或定期生成所有实例的快照供查询使用。

内容的提问来源于stack exchange,提问作者Alexander Zveniev

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.30 02:13:14