GetOrAdd和AddOrUpdate是原子操作,但仅限键存在性判断与值获取/插入/更新逻辑路径,委托执行在锁外且可能被多次并发调用。
是的,GetOrAdd 和 AddOrUpdate 都是原子操作 —— 但仅限于“键存在性判断 + 值获取/插入/更新”这一整条逻辑路径在内部完成,不包括你传入的委托(Func)执行过程。
这两个方法的原子性体现在:它们会先检查键是否存在,再决定是读取现有值、还是调用你提供的工厂函数生成新值、或调用更新函数计算新值 —— 这个“检查+分支决策+写入”三步,在 ConcurrentDictionary 内部由底层分段锁或无锁算法保障不会被其他线程打断。
但注意:GetOrAdd(key, factory) 中的 factory,或 AddOrUpdate(key, addVal, updateFactory) 中的 updateFactory,是在锁外执行的。这意味着:
GetOrAdd,什么时候选 AddOrUpdate?看你的业务逻辑是否需要区分“首次添加”和“后续更新”:
GetOrAdd(key, value):适合缓存场景,比如“查用户配置,没有就用默认值初始化”,值是静态或轻量构造的;GetOrAdd(key, factory):适合值创建开销大或需上下文(如 key => new ExpensiveObject(key)),且你接受工厂可能被重复调用;AddOrUpdate(key, addVal, updateFactory):适合计数器、聚合类场景,比如“用户点击次数+1”,必须保证每次更新都基于最新值(updateFactory 会收到当前值,避免脏读);ContainsKey + Add 组合 —— 这不是原子的,竞态条件直接导致 ArgumentException 或数据丢失。下面这段代码看

var counter = new ConcurrentDictionary(); int sharedTotal = 0; counter.AddOrUpdate("hits", 1, (k, v) => { sharedTotal += 1; // ❌ 多线程并发执行,sharedTotal 会丢加法 return v + 1; });
正确做法是把聚合逻辑留在委托外,或改用线程安全类型(如 Interlocked.Increment):
counter.AddOrUpdate("hits", 1, (k, v) => v + 1); // ✅ 纯计算,安全
Interlocked.Increment(ref sharedTotal); // ✅ 如真需更新共享计数器
真正容易被忽略的点在于:原子性只管字典结构本身,不管你的委托干了什么。一旦委托里有 IO、锁、共享变量修改或非幂等操作,就得自己兜底 —— ConcurrentDictionary 不会替你管这些。