C# ConcurrentDictionary的使用小结
ConcurrentDictionary<TKey, TValue>是 .NET 中一个线程安全的字典集合,专为高并发读写场景设计。它是System.Collections.Concurrent命名空间下的核心类型之一,适用于多线程环境中需要高效、安全地共享键值对数据的场景。
✅ 一、为什么需要ConcurrentDictionary?
普通Dictionary<TKey, TValue>不是线程安全的。如果多个线程同时读写:
1 2 3 4 |
|
即使加锁(lock)也能实现线程安全,但会带来性能瓶颈(串行化访问)。
而ConcurrentDictionary:
- 无需外部加锁
- 内部使用细粒度锁或无锁算法
- 支持高并发读 + 适度并发写
🧱 二、核心特性
| 特性 | 说明 |
|---|---|
| 线程安全 | 所有公共成员(Add、Get、Remove 等)都是线程安全的 |
| 高性能并发读 | 读操作几乎无锁(lock-free),性能接近普通字典 |
| 分段/桶式结构 | 内部将数据分片(buckets),减少写冲突 |
| 原子操作支持 | 提供AddOrUpdate,GetOrAdd等复合原子操作 |
| 不保证顺序 | 和Dictionary一样,不维护插入顺序 |
⚠️ 注意:ConcurrentDictionary 的枚举(foreach)是线程安全的快照,但可能包含“过时”数据(因为其他线程可能正在修改)。
🔧 三、常用 API 与示例
1. 创建
1 2 3 |
|
2. 基本操作(线程安全)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
3. 高级原子操作(⭐ 最常用!)
✅GetOrAdd(key, valueFactory)
如果 key 不存在,则调用工厂方法创建值并添加;否则返回现有值。
1 2 3 4 5 6 |
|
💡 多个线程同时调用 GetOrAdd("config", ...) 时,工厂方法只会被调用一次(其他线程等待结果),避免重复初始化!
✅AddOrUpdate(key, addValueFactory, updateValueFactory)
如果不存在则添加,存在则更新。
1 2 3 4 5 |
|
⚖️ 四、与加锁Dictionary的性能对比
| 场景 | Dictionary + lock | ConcurrentDictionary |
|---|---|---|
| 高并发读 | 所有读需排队(慢) | 几乎无锁(快) |
| 低并发写 | 串行写(中等) | 分段锁(较快) |
| 高并发写 | 严重瓶颈 | 仍优于全局锁 |
| 代码简洁性 | 需手动管理锁 | 无需锁,API 更丰富 |
📊 在典型 Web 应用缓存场景(大量读 + 少量写),ConcurrentDictionary 性能可提升 5~10 倍。
🚫 五、常见误区
❌ 误区 1:认为dict[key] = value是原子的
1 2 3 4 |
|
✅ 正确做法:
1 |
|
❌ 误区 2:在GetOrAdd中做非幂等操作
1 2 3 4 5 6 7 8 9 |
|
💡 虽然最终值是唯一的,但工厂方法可能被多个线程同时调用(.NET 6+ 已优化为单次调用,但旧版本不一定)。建议工厂方法无副作用、幂等。
🛠 六、典型应用场景
1.内存缓存(Cache)
1 2 3 4 5 6 7 8 9 |
|
2.计数器 / 统计
1 2 3 4 5 6 |
|
3.对象池(Object Pool)
1 2 3 4 5 6 7 |
|
📏 七、性能调优建议
| 参数 | 说明 |
|---|---|
| concurrencyLevel | 预期并发更新线程数(默认为 CPU 核心数) |
| capacity | 初始容量(避免频繁扩容) |
1 2 3 4 5 |
|
💡 大多数场景用默认构造函数即可,除非你有明确的性能测试数据。
✅ 总结:何时使用ConcurrentDictionary?
| 场景 | 推荐 |
|---|---|
| 多线程读写共享字典 | ✅ 强烈推荐 |
| 高频读 + 低频写(如缓存) | ✅ 最佳选择 |
| 需要原子“获取或创建”语义 | ✅ 必选 |
| 单线程或只读场景 | ❌ 用普通 Dictionary 更轻量 |
| 需要保持插入顺序 | ❌ 考虑 ImmutableDictionary 或加锁的 SortedDictionary |
🔑 记住:ConcurrentDictionary 不是万能的,但它是在并发字典场景下最高效、最安全的选择。
