ConcurrentDictionary 并非万能线程安全字典,其方法单个原子但组合不安全;ContainsKey+赋值有竞态,应改用TryAdd;AddOrUpdate参数顺序易错;GetOrAdd工厂可能重复执行,宜结合Lazy;遍历时删元素会抛异常,需先收集key再删除。

ConcurrentDictionary 不是“线程安全的 Dictionary 替代品”,它是为特定并发模式设计的独立类型——用错方法,照样出 bug。
别用 ContainsKey + 赋值组合
这是新手最常写的“看起来合理但实际危险”的代码:
if (!dict.ContainsKey("user123")) dict["user123"] = new User();
两步操作之间存在竞态窗口:两个线程同时通过 ContainsKey 检查(都返回 false),然后都执行赋值,最终只有一个值留下,另一个被覆盖,还可能触发意外逻辑分支。
- 正确做法是改用
TryAdd:它原子地完成“检查不存在 + 添加”,失败就说明键已存在 - 如果目标是“有则不管、无则新建”,
TryAdd最简洁;如果想“有则更新”,直接跳到下个副标题 -
ContainsKey本身线程安全,但只要它后面跟着任何写操作,整个片段就不是原子的
AddOrUpdate 的参数顺序不能反
很多人把 AddOrUpdate 当成“更新优先”函数,传参时把更新逻辑放在第二个位置,结果每次调用都走新增分支:
// ❌ 错误:把更新逻辑当成了“新增工厂”
dict.AddOrUpdate("key", (k, v) => v + 1, _ => 100); // 永远执行第三个参数
它的签名是 AddOrUpdate(TKey, Func<tkey tvalue>, Func<tkey tvalue>)</tkey></tkey>,即:
- 第二个参数:key 不存在时调用的工厂函数(只接收
key) - 第三个参数:key 存在时调用的更新函数(接收
key和oldValue) - 如果你只是想“存在就赋新值、不存在就算了”,别用
AddOrUpdate,改用TryUpdate或直接dict[key] = value(后者是线程安全的写入)
GetOrAdd 的工厂函数可能重复执行
当多个线程同时发现 key 不存在,GetOrAdd 会各自触发一次工厂函数——这不是 bug,是设计使然。常见后果:
- 日志里出现多次“正在加载配置”
- HTTP 请求发了三遍
- 数据库连接对象被构造三次,其中两个立刻丢弃
解决思路不是加锁(破坏并发意图),而是让工厂函数本身幂等或延迟初始化:
dict.GetOrAdd("config", _ => new Lazy<Config>(() => LoadConfigFromDb()));
后续使用时取 .Value,Lazy<t></t> 保证内部委托只执行一次,且线程安全。
遍历时删元素会抛 InvalidOperationException
ConcurrentDictionary 的枚举器是快照式(snapshot-based),不支持边遍历边修改。以下代码在 .NET 6+ 会直接崩溃:
foreach (var kvp in dict) {
if (kvp.Value.IsStale) dict.TryRemove(kvp.Key, out _);
}
可行替代方案:
- 先收集要删的 key:
var keysToRemove = dict.Where(x => x.Value.IsStale).Select(x => x.Key).ToArray(),再循环TryRemove - 用
dict.Keys.ToList()遍历副本(注意:Keys属性本身开销大,避免高频调用) - 如果删除是常态需求,考虑是否真需要
ConcurrentDictionary—— 有时用ConcurrentBag+ 外部协调更合适
真正容易被忽略的是:它的“线程安全”是有前提的——每个方法自身原子,但多个方法组合就不安全;所有看似“顺手”的写法,都要先问一句:这一步是不是原子的?


















