在C#多线程开发中,ConcurrentDictionary是常用的线程安全字典容器,但是如果在并发场景下直接调用Add方法添加已经存在的键,就会抛出AddDuplicateKeyException异常。这个异常会导致当前操作失败,甚至可能影响整个业务流程,因此需要掌握正确的避免方式。

AddDuplicateKeyException的产生原因
ConcurrentDictionary的设计目标是支持多线程并发读写,但是它的Add方法本身的逻辑是:如果键已经存在,直接抛出异常,并不会做自动覆盖或者忽略处理。在多线程同时调用Add方法添加同一个键的场景下,就很容易触发这个异常。比如下面这段简单的示例代码,就会大概率抛出AddDuplicateKeyException:
using System;
using System.Collections.Concurrent;
using System.Threading;
class Program
{
static ConcurrentDictionary<int, string> dict = new ConcurrentDictionary<int, string>();
static void Main()
{
// 两个线程同时添加同一个键
Thread t1 = new Thread(() => dict.Add(1, "value1"));
Thread t2 = new Thread(() => dict.Add(1, "value2"));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
避免AddDuplicateKeyException的常用方案
1. 使用TryAdd方法替代Add方法
ConcurrentDictionary提供了TryAdd方法,这个方法的逻辑是:如果键不存在,添加键值对并返回true;如果键已经存在,直接返回false,不会抛出异常。这是最推荐的避免方式,不需要额外加锁,性能也比较好。
using System;
using System.Collections.Concurrent;
using System.Threading;
class Program
{
static ConcurrentDictionary<int, string> dict = new ConcurrentDictionary<int, string>();
static void Main()
{
Thread t1 = new Thread(() =>
{
bool result = dict.TryAdd(1, "value1");
Console.WriteLine($"线程1添加结果:{result}");
});
Thread t2 = new Thread(() =>
{
bool result = dict.TryAdd(1, "value2");
Console.WriteLine($"线程2添加结果:{result}");
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
运行上述代码,只会有一个线程的TryAdd返回true,另一个返回false,不会抛出任何异常。
2. 使用AddOrUpdate方法处理重复键场景
如果你的需求是当键存在时更新对应的值,不存在时添加新键值对,那么可以直接使用AddOrUpdate方法。这个方法会自动处理键存在和不存在的两种情况,不会抛出异常。
using System;
using System.Collections.Concurrent;
using System.Threading;
class Program
{
static ConcurrentDictionary<int, string> dict = new ConcurrentDictionary<int, string>();
static void Main()
{
Thread t1 = new Thread(() => dict.AddOrUpdate(1, "value1", (k, oldV) => "value1"));
Thread t2 = new Thread(() => dict.AddOrUpdate(1, "value2", (k, oldV) => "value2"));
t1.Start();
t2.Start();
t1.Join();
t2.Join();
Console.WriteLine($"最终键1的值:{dict[1]}");
}
}
3. 先判断键是否存在再添加(需配合锁保证原子性)
如果一定要使用Add方法,那么需要先判断键是否存在,但是普通的判断和添加不是原子操作,多线程下还是会出现问题,因此需要配合锁来保证操作的原子性。不过这种方式性能不如前两种,只适合特殊场景使用。
using System;
using System.Collections.Concurrent;
using System.Threading;
class Program
{
static ConcurrentDictionary<int, string> dict = new ConcurrentDictionary<int, string>();
static readonly object lockObj = new object();
static void Main()
{
Thread t1 = new Thread(() =>
{
lock (lockObj)
{
if (!dict.ContainsKey(1))
{
dict.Add(1, "value1");
}
}
});
Thread t2 = new Thread(() =>
{
lock (lockObj)
{
if (!dict.ContainsKey(1))
{
dict.Add(1, "value2");
}
}
});
t1.Start();
t2.Start();
t1.Join();
t2.Join();
}
}
不同方案的选择建议
如果只是需要添加不重复的键,忽略重复添加的操作,优先选择TryAdd方法,性能最好也最简单。如果需要键存在时更新值,优先选择AddOrUpdate方法。只有在有特殊逻辑需要先判断再处理,且前两种方法无法满足需求时,才考虑使用加锁的方式,避免不必要的性能损耗。
另外需要注意,ConcurrentDictionary的其他方法比如GetOrAdd也会处理键存在的情况,不会抛出异常,可以根据具体的业务场景选择合适的方法,从根源上避免AddDuplicateKeyException的出现。
ConcurrentDictionaryAddDuplicateKeyException线程安全字典操作键值对修改时间:2026-06-29 01:24:27