使用嵌套锁维护并发字典时是否存在死锁风险?
代码死锁风险分析与问题修复
首先明确:这段代码不存在死锁风险。死锁的核心条件是「循环等待」——线程A持有锁X并等待锁Y,线程B持有锁Y并等待锁X,但你的代码里所有线程获取锁的顺序完全一致:先拿_listLock全局锁,再拿对应UID的对象锁,不会出现循环等待的情况,所以死锁不会发生。
但这段代码有更致命的问题,远比重构死锁风险更紧急:
- 全局锁彻底扼杀并发性能:
_listLock是全局排他锁,所有调用RunWithLock的线程都必须排队等待这个锁,等于把所有操作串行化了。每分钟处理数千对象的高并发场景下,这会直接导致系统性能暴跌,完全发挥不出多核CPU的能力,比死锁的影响还严重。 - 未初始化的
_listLock会直接崩溃:_listLock没有赋值,第一次执行lock (_listLock)时会抛出NullReferenceException,这是必须立刻修复的bug。 - 浪费了ConcurrentDictionary的线程安全特性:你用了线程安全的
ConcurrentDictionary,却套了一层全局锁,完全没用到它的原子操作能力,属于冗余设计。
优化方案
去掉全局锁,利用ConcurrentDictionary的原子操作管理锁对象,同时给锁对象加引用计数,避免并发场景下锁对象被提前移除导致的线程安全问题:
private static readonly ConcurrentDictionary<Guid, (object LockObj, int RefCount)> _lockDict = new ConcurrentDictionary<Guid, (object, int)>(); public static void RunWithLock(Guid uid, Action body) { // 原子创建或获取锁对象,并增加引用计数 var entry = _lockDict.AddOrUpdate( uid, key => (new object(), 1), (key, existing) => (existing.LockObj, existing.RefCount + 1) ); try { lock (entry.LockObj) { body(); } } finally { // 原子减少引用计数,只有计数为0时才从字典移除 while (true) { if (!_lockDict.TryGetValue(uid, out var current)) break; var newRefCount = current.RefCount - 1; if (newRefCount == 0) { if (_lockDict.TryRemove(uid, out _)) break; } else { if (_lockDict.TryUpdate(uid, (current.LockObj, newRefCount), current)) break; } } } }
这个优化后的版本:
- 不同UID的操作可以并行执行,保留了高并发能力
- 同一个UID的操作依然互斥,保证线程安全
- 锁对象会在没有线程使用时被自动移除,保持字典整洁
- 完全没有死锁风险
内容的提问来源于stack exchange,提问作者Pavel Grishin
相关产品推荐
相关产品推荐

