ConcurrentDictionary<TKey, TValue> 是 .NET 中一个线程安全的字典集合 ,专为高并发读写场景 设计。它是 System.Collections.Concurrent 命名空间下的核心类型之一,适用于多线程环境中需要高效、安全地共享键值对数据的场景。
✅ 一、为什么需要 ConcurrentDictionary?
普通 Dictionary<TKey, TValue> 不是线程安全的。如果多个线程同时读写:
csharp
var dict = new Dictionary<string, int>();
// 线程A: dict["a"] = 1;
// 线程B: dict["b"] = 2;
// 可能抛出 InvalidOperationException 或数据损坏!
即使加锁(lock)也能实现线程安全,但会带来性能瓶颈(串行化访问)。
而 ConcurrentDictionary:
- 无需外部加锁
- 内部使用细粒度锁或无锁算法
- 支持高并发读 + 适度并发写
🧱 二、核心特性
| 特性 | 说明 |
|---|---|
| 线程安全 | 所有公共成员(Add、Get、Remove 等)都是线程安全的 |
| 高性能并发读 | 读操作几乎无锁(lock-free),性能接近普通字典 |
| 分段/桶式结构 | 内部将数据分片(buckets),减少写冲突 |
| 原子操作支持 | 提供 AddOrUpdate, GetOrAdd 等复合原子操作 |
| 不保证顺序 | 和 Dictionary 一样,不维护插入顺序 |
⚠️ 注意:
ConcurrentDictionary的枚举(foreach)是线程安全的快照,但可能包含"过时"数据(因为其他线程可能正在修改)。
🔧 三、常用 API 与示例
1. 创建
csharp
var cache = new ConcurrentDictionary<string, int>();
// 或指定初始容量和并发级别(高级用法)
var cache2 = new ConcurrentDictionary<string, int>(concurrencyLevel: 4, capacity: 16);
2. 基本操作(线程安全)
csharp
// 添加(如果不存在)
cache.TryAdd("key1", 100); // 返回 bool
// 获取(如果存在)
if (cache.TryGetValue("key1", out int value))
{
Console.WriteLine(value); // 100
}
// 更新(如果存在)
cache.TryUpdate("key1", 200, 100); // 仅当当前值为100时更新为200
// 删除
cache.TryRemove("key1", out int removedValue);
3. 高级原子操作(⭐ 最常用!)
✅ GetOrAdd(key, valueFactory)
如果 key 不存在,则调用工厂方法创建值并添加;否则返回现有值。
csharp
var config = cache.GetOrAdd("config", key =>
{
// 模拟耗时加载(只执行一次!)
Thread.Sleep(1000);
return LoadConfigFromDatabase();
});
💡 多个线程同时调用
GetOrAdd("config", ...)时,工厂方法只会被调用一次(其他线程等待结果),避免重复初始化!
✅ AddOrUpdate(key, addValueFactory, updateValueFactory)
如果不存在则添加,存在则更新。
csharp
// 实现计数器
cache.AddOrUpdate("counter",
addValue: 1, // 不存在时设为1
updateValueFactory: (key, oldValue) => oldValue + 1 // 存在时+1
);
⚖️ 四、与加锁 Dictionary 的性能对比
| 场景 | Dictionary + lock |
ConcurrentDictionary |
|---|---|---|
| 高并发读 | 所有读需排队(慢) | 几乎无锁(快) |
| 低并发写 | 串行写(中等) | 分段锁(较快) |
| 高并发写 | 严重瓶颈 | 仍优于全局锁 |
| 代码简洁性 | 需手动管理锁 | 无需锁,API 更丰富 |
📊 在典型 Web 应用缓存场景(大量读 + 少量写),
ConcurrentDictionary性能可提升 5~10 倍。
🚫 五、常见误区
❌ 误区 1:认为 dict[key] = value 是原子的
csharp
// 错误!这实际上是:
// if (exists) update; else add;
// 但中间可能被其他线程干扰
cache["key"] = newValue; // 不是原子操作!
✅ 正确做法:
csharp
cache.AddOrUpdate("key", newValue, (k, old) => newValue);
❌ 误区 2:在 GetOrAdd 中做非幂等操作
csharp
// 危险!工厂方法可能被多次调用(虽然最终只存一个结果)
var obj = cache.GetOrAdd("key", k => new ExpensiveObject()); // OK
// 更危险:有副作用的操作
cache.GetOrAdd("key", k =>
{
Log("Creating instance"); // 可能被记录多次!
return new MyService();
});
💡 虽然最终值是唯一的,但工厂方法可能被多个线程同时调用 (.NET 6+ 已优化为单次调用,但旧版本不一定)。建议工厂方法无副作用、幂等。
🛠 六、典型应用场景
1. 内存缓存(Cache)
csharp
public class InMemoryCache
{
private readonly ConcurrentDictionary<string, object> _cache = new();
public T GetOrCreate<T>(string key, Func<T> factory)
{
return (T)_cache.GetOrAdd(key, k => factory());
}
}
2. 计数器 / 统计
csharp
private readonly ConcurrentDictionary<string, int> _hitCounts = new();
public void RecordHit(string page)
{
_hitCounts.AddOrUpdate(page, 1, (k, v) => v + 1);
}
3. 对象池(Object Pool)
csharp
private readonly ConcurrentDictionary<Type, Stack<object>> _pools = new();
public object Rent(Type type)
{
var pool = _pools.GetOrAdd(type, t => new Stack<object>());
return pool.TryPop(out var obj) ? obj : Activator.CreateInstance(type);
}
📏 七、性能调优建议
| 参数 | 说明 |
|---|---|
concurrencyLevel |
预期并发更新线程数(默认为 CPU 核心数) |
capacity |
初始容量(避免频繁扩容) |
csharp
// 预期 8 个线程并发写,初始存 1000 项
var dict = new ConcurrentDictionary<string, Data>(
concurrencyLevel: 8,
capacity: 1000
);
💡 大多数场景用默认构造函数即可,除非你有明确的性能测试数据。
✅ 总结:何时使用 ConcurrentDictionary?
| 场景 | 推荐 |
|---|---|
| 多线程读写共享字典 | ✅ 强烈推荐 |
| 高频读 + 低频写(如缓存) | ✅ 最佳选择 |
| 需要原子"获取或创建"语义 | ✅ 必选 |
| 单线程或只读场景 | ❌ 用普通 Dictionary 更轻量 |
| 需要保持插入顺序 | ❌ 考虑 ImmutableDictionary 或加锁的 SortedDictionary |
🔑 记住 :
ConcurrentDictionary不是万能的,但它是在并发字典场景下最高效、最安全的选择。