ConcurrentDictionary.AddOrUpdate参数顺序错误会导致覆盖而非更新,正确为AddOrUpdate(key, addFactory, updateFactory);GetOrAdd工厂函数可能重复执行,应结合Lazy<T>保证幂等;禁止ContainsKey+赋值组合及遍历时删除。

ConcurrentDictionary.AddOrUpdate 用错参数顺序会覆盖值
很多人以为 AddOrUpdate 的第三个参数是“更新时用的函数”,其实它接收两个委托:第一个是 key 不存在时的工厂函数,第二个才是 key 存在时的更新函数。传反了,或者只传一个 lambda 当作更新逻辑,结果就是每次调用都走新增分支,旧值被无视。
- 正确写法:
dict.AddOrUpdate(key, _ => newValue, (k, old) => newValue)—— 注意第二、三个参数语义不同 - 常见错误:
dict.AddOrUpdate(key, (k, old) => newValue, _ => newValue),这会导致旧值永远不参与计算,等效于dict[key] = newValue - 如果只是想“有则更新、无则跳过”,别用
AddOrUpdate,改用TryUpdate或直接赋值(dict[key] = value是线程安全的写入)
ConcurrentDictionary.GetOrAdd 在初始化耗时场景下可能重复执行工厂函数
GetOrAdd 看似原子,但它的工厂函数(第二个参数)**不是同步互斥执行的**。当多个线程同时发现 key 不存在,它们会各自触发一次工厂函数,最终只有一个结果被保留,其余白跑——尤其当你在里面做 HTTP 请求、文件读取或构造重量级对象时,问题立刻暴露。
- 典型现象:日志里看到多次“正在加载配置”、数据库连接被意外创建三次
- 解决办法不是加锁(破坏并发本意),而是把工厂函数本身做成幂等或缓存前置;例如先用
Lazy<t></t>包一层:dict.GetOrAdd(key, _ => new Lazy<myservice>(() => new MyService()))</myservice>,再调用.Value - 注意
Lazy<t></t>默认是线程安全的,且只执行一次,和ConcurrentDictionary配合刚好补上这个缺口
别用 ContainsKey + 索引器组合判断+赋值
这是最经典的非线程安全写法:if (!dict.ContainsKey(key)) dict[key] = value;。两步操作之间存在竞态窗口,两个线程可能同时通过 ContainsKey 检查,然后都执行赋值,导致后者覆盖前者,或者触发 KeyNotFoundException(如果用的是 dict.TryAdd 以外的方式)。
- 应该用
dict.TryAdd(key, value)替代——失败就说明已存在,不需要提前查 - 如果必须“存在则修改”,用
dict.AddOrUpdate或dict.TryUpdate,不要自己拼逻辑 -
ContainsKey本身是安全的,但只要它后面跟着任何写操作,整个片段就不再是原子的
迭代 ConcurrentDictionary 时删元素会抛 InvalidOperationException
和普通 Dictionary 一样,边遍历边删会触发 InvalidOperationException: Collection was modified。但很多人误以为 ConcurrentDictionary “并发”就等于“遍历时可修改”,其实它的线程安全仅针对单个操作(Add/Update/Remove/Get),不保证枚举器一致性。
- 错误写法:
foreach (var kvp in dict) if (kvp.Value.IsStale) dict.TryRemove(kvp.Key, out _); - 安全做法:先收集要删的 key(
dict.Keys.Where(...).ToList()),再遍历列表调用TryRemove - 性能提示:
Keys和Values属性返回的是快照,不是实时视图,所以ToList()是安全的,但要注意内存开销










