C# ConcurrentDictionary详解

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 不是万能的,但它是在并发字典场景下最高效、最安全的选择

相关推荐
DataIntel1 小时前
WPF 中的数据模板(DataTemplate)与样式/控件模板(Style / ControlTemplate)详解
c#
Han.miracle1 小时前
Maven 基础与 Spring Boot 入门:环境搭建、项目开发及常见问题排查
java·spring boot·后端
特拉熊1 小时前
23种设计模式之桥接模式
java·架构
半瓶榴莲奶^_^1 小时前
后端Web进阶(AOP)
java·开发语言
麻辣烫不加辣1 小时前
跑批调额系统说明文档
java·后端
虚伪的空想家1 小时前
arm架构TDengine时序数据库及应用使用K8S部署
服务器·arm开发·架构·kubernetes·arm·时序数据库·tdengine
中杯可乐多加冰1 小时前
openEuler软件生态体验:快速部署Nginx Web服务器
服务器·前端·nginx
一只乔哇噻1 小时前
java后端工程师+AI大模型开发进修ing(研一版‖day61)
java·开发语言·学习·算法·语言模型
拾忆,想起1 小时前
Dubbo服务降级全攻略:构建韧性微服务系统的守护盾
java·前端·网络·微服务·架构·dubbo