并发代码是否存在竞态?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
相关产品推荐
相关产品推荐

