GetOrAdd和AddOrUpdate是原子操作,但仅限键存在性判断与值获取/插入/更新逻辑路径,委托执行在锁外且可能被多次并发调用。

是的,GetOrAdd 和 AddOrUpdate 都是原子操作 —— 但仅限于“键存在性判断 + 值获取/插入/更新”这一整条逻辑路径在内部完成,不包括你传入的委托(Func)执行过程。
为什么说“是原子操作”,但又得小心委托?
这两个方法的原子性体现在:它们会先检查键是否存在,再决定是读取现有值、还是调用你提供的工厂函数生成新值、或调用更新函数计算新值 —— 这个“检查+分支决策+写入”三步,在 ConcurrentDictionary 内部由底层分段锁或无锁算法保障不会被其他线程打断。
但注意:GetOrAdd(key, factory) 中的 factory,或 AddOrUpdate(key, addVal, updateFactory) 中的 updateFactory,是在锁外执行的。这意味着:
- 如果工厂函数很慢、有副作用(比如发 HTTP 请求、写文件),它可能被多个线程并发执行多次(即使最终只有一份结果被写入);
- 如果工厂函数依赖外部状态(如静态变量、共享对象),仍需自行保证该状态的线程安全;
- 它不是“整个表达式原子”,而是“键操作原子 + 工厂可重入”。
什么时候该用 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 不会替你管这些。










