TryAdd 是原子性“仅新增”操作,键存在时返回 false 且不修改值;dict[key] = value 是“覆盖写入”,无论键是否存在均设为新值,语义和行为本质不同。

ConcurrentDictionary 不是普通 Dictionary 的“线程安全版替代品”,它有自己明确的适用边界和行为契约。盲目替换可能引入竞态、性能倒退或逻辑错误。
ConcurrentDictionary.TryAdd 和直接赋值 dict[key] = value 有什么区别?
两者都线程安全,但语义不同:
-
TryAdd是原子性的“仅新增”:键已存在时返回false,不修改原值;适合初始化、注册类场景(如注册唯一服务实例) -
dict[key] = value是“覆盖写入”:无论键是否存在,都会设为新值;等效于TryUpdate+TryAdd的组合,但更简洁 - 别用
ContainsKey+TryAdd判断再添加——两次调用间可能被其他线程插入,造成重复判断或漏操作
为什么 AddOrUpdate 的参数顺序容易出错?
它的签名是 AddOrUpdate(TKey key, Func<tkey tvalue> addValueFactory, Func<tkey tvalue> updateValueFactory)</tkey></tkey>,不是“新增值、更新值”两个值,而是两个委托:
- 第二个参数是“键不存在时执行的工厂函数”,只在新增分支触发
- 第三个参数是“键存在时执行的更新函数”,接收旧键和旧值,返回新值
- 常见错误:
dict.AddOrUpdate("k", (k, v) => v + 1, _ => "def")—— 这会把第二个参数当更新逻辑,实际永远走新增分支,旧值被丢弃 - 如果只是想“有则更新、无则跳过”,用
TryUpdate或直接赋值更清晰
GetOrAdd 的工厂函数会被多次执行,怎么避免?
是的,GetOrAdd 只保证“最终只有一个结果被存入”,但多个线程同时发现 key 缺失时,会各自执行工厂函数一次。这在 HTTP 请求、数据库查询、对象构造等耗时/有副作用操作中很危险。
- 典型现象:日志里看到 3 次“正在加载配置”,但字典里只存了一个结果
- 解决办法不是加锁,而是让工厂函数本身幂等或延迟执行:用
Lazy<t></t>包一层,比如dict.GetOrAdd("cfg", _ => new Lazy<myconfig>(() => LoadFromDb()))</myconfig>,再访问.Value -
Lazy<t></t>默认线程安全且只执行一次,和ConcurrentDictionary配合刚好补上这个缺口
遍历时能边读边删吗?
不能。虽然 foreach 遍历 ConcurrentDictionary 是线程安全的快照,但如果你在循环体内调用 TryRemove 或 Clear,不会抛异常,但行为不可靠:
- 被删的项可能仍在当前快照中,导致后续逻辑误处理
- 快照本身不可变,但原字典已被修改,下次遍历看到的是新状态——两次遍历之间没有一致性保证
- 若需“清理所有匹配项”,应先收集待删 key(
dict.Keys.Where(...).ToArray()),再逐个TryRemove;或者改用lock+ 普通Dictionary,当强一致性比并发吞吐更重要时
最常被忽略的一点:ConcurrentDictionary 的分段锁机制在写冲突高、key 分布集中时,性能可能不如预期——它不是万能锁消除器,而是权衡后的设计。真要压测,得看真实 key 分布和读写比例,而不是只盯着“线程安全”四个字。


















