ConcurrentDictionary.GetOrAdd 多线程并发下的重复执行
ConcurrentDictionary
是 C# 中非常强大的线程安全集合之一,尤其是在多线程场景下表现出色。然而,它的 GetOrAdd(TKey key, Func<TKey, TValue> valueFactory)
方法在特定情况下会多次执行 valueFactory
,这可能导致额外的性能消耗。本文将分析这个问题的根本原因,并提供一种结合 Lazy
的优雅解决方案。
-
问题展示
以下代码模拟了在项目中使用多个 Redis,不同的 Redis 通过
cluster_id
区分,并存放在ConcurrentDictionary
中。理想情况下,每个 Redis 只实例化一个RedisConnection
对象。internal class Program { static void Main(string[] args) { ConcurrentDictionary<string, RedisConnection> redisConnectionDic = new ConcurrentDictionary<string, RedisConnection>(); string redisConn = "10"; Parallel.For(1, 10, i => { redisConnectionDic.GetOrAdd(redisConn, redisConn => { return CreateRedisClient(redisConn); }); }); Console.WriteLine("Hello, World!"); Console.ReadLine(); } public static RedisConnection CreateRedisClient(string cluster_id) { Console.WriteLine($"正在初始化 cluster_id={cluster_id} 的redis 连接"); Thread.Sleep(1000); return new RedisConnection(); } } public class RedisConnection { }
运行结果分析
运行上述代码后,控制台会输出多条“正在初始化 cluster_id=10 的redis 连接”,这表明
CreateRedisClient
被多次执行。原因剖析
通过查看
ConcurrentDictionary.GetOrAdd
的源码,可以发现问题的根源:public TValue GetOrAdd(TKey key, Func<TKey, TValue> valueFactory) { if (key == null) { System.ThrowHelper.ThrowKeyNullException(); } if (valueFactory == null) { System.ThrowHelper.ThrowArgumentNullException("valueFactory"); } Tables tables = _tables; IEqualityComparer<TKey> comparer = tables._comparer; int hashCode = GetHashCode(comparer, key); if (!TryGetValueInternal(tables, key, hashCode, out var value)) { TryAddInternal(tables, key, hashCode, valueFactory(key), updateIfExists: false, acquireLock: true, out value); } return value; }
关键在于以下代码段:
if (!TryGetValueInternal(tables, key, hashCode, out var value)) { TryAddInternal(tables, key, hashCode, valueFactory(key), updateIfExists: false, acquireLock: true, out value); }
当第一个线程正在执行
valueFactory(key)
但尚未插入到 ConcurrentDictionary 时,其他线程可能会并发执行到此处,导致 valueFactory(key) 被多次调用。虽然最终只有一个结果会被存入 ConcurrentDictionary,但多次调用 valueFactory 是不可避免的。解决方案:结合 Lazy
为了确保
CreateRedisClient
只执行一次,可以使用Lazy
类来延迟初始化对象:internal class Program { static void Main(string[] args) { ConcurrentDictionary<string, Lazy<RedisConnection>> redisConnectionDic = new ConcurrentDictionary<string, Lazy<RedisConnection>>(); string redisConn = "10"; Parallel.For(1, 10, i => { redisConnectionDic.GetOrAdd(redisConn, new Lazy<RedisConnection>(() => CreateRedisClient(redisConn))); var connection = redisConnectionDic[redisConn].Value; }); Console.WriteLine("Hello, World!"); Console.ReadLine(); } public static RedisConnection CreateRedisClient(string cluster_id) { Console.WriteLine($"正在初始化 cluster_id={cluster_id} 的redis 连接"); Thread.Sleep(1000); return new RedisConnection(); } } public class RedisConnection { }
Lazy 的作用
Lazy<T>
是 .NET 中提供的一个线程安全的延迟初始化类,只有在访问Value
属性时,才会执行初始化逻辑。结合Lazy
后,即使多个线程并发调用GetOrAdd
,初始化逻辑也只会执行一次。总结
-
ConcurrentDictionary
的GetOrAdd
方法在多线程场景下无法保证valueFactory
只执行一次,这可能带来性能问题。 -
通过引入
Lazy
,可以将初始化逻辑封装为线程安全的延迟加载,从而避免重复执行初始化代码。
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· [.NET]调用本地 Deepseek 模型
· 一个费力不讨好的项目,让我损失了近一半的绩效!
· .NET Core 托管堆内存泄露/CPU异常的常见思路
· DeepSeek “源神”启动!「GitHub 热点速览」
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· C# 集成 DeepSeek 模型实现 AI 私有化(本地部署与 API 调用教程)
· DeepSeek R1 简明指南:架构、训练、本地部署及硬件要求
· 2 本地部署DeepSeek模型构建本地知识库+联网搜索详细步骤