锁的粒度指被保护的共享资源范围大小,如对象级锁保护实例字段,键级锁按业务主键隔离,并发性与管理成本呈反比。

锁的粒度到底指什么
锁的粒度,说白了就是“你锁住的东西有多大”。不是指代码行数,而是指被保护的**共享资源的范围大小**。比如:lock(_globalLock) 锁整个类实例,是粗粒度;而为每个 userId 分配独立锁对象,只锁该用户相关的操作,就是细粒度。
粒度越粗,并发越低——所有线程排队等同一把锁;粒度越细,并发越高——但管理成本上升,比如要维护几百个锁对象、防字典写竞争、避免内存泄漏。
常见粒度层级与适用场景
按实际影响排序(从最常用到慎用):
-
对象级锁:用
private readonly object _lock = new object();锁某个实例字段。适合保护该对象内部状态(如一个List<int>的增删),是大多数业务场景的起点。 -
键级锁(key-based locking):对不同
id使用不同锁对象,例如用户 A 和用户 B 的余额更新互不阻塞。适用于“按业务主键隔离”的场景,如订单处理、会话管理。 -
类级/静态锁:用
typeof(MyClass)或静态private static readonly object。仅当真正需要全局互斥(如单例初始化、配置热重载)时才用,极易成为性能瓶颈。 -
方法级锁:每个 public 方法都套一层
lock(this)——这是反模式。不仅破坏封装,还因this可能被外部锁定引发死锁,禁止使用。
怎么实现键级锁?别直接抄老代码
网上流传的 DictionaryBasedKeyLockEngine 示例(含 WaitCounter 和双重 lock(SyncRoot))在高并发下有明显缺陷:字典写操作(Add/Remove)集中争抢 SyncRoot,变成新的瓶颈。
更稳妥的做法:
- 用
ConcurrentDictionary<string, object>替代普通Dictionary,避免写竞争; - 用
Lazy<object>或GetOrAdd原子创建锁对象,无需额外同步; - 锁对象本身不回收(或极低频回收),避免
Remove引发的哈希桶重排和 GC 压力; - 绝不锁
string字面量或this,防止跨模块意外锁冲突。
简版示意:
private static readonly ConcurrentDictionary<string, object> _keyLocks = new();
public void ProcessOrder(string orderId, Action action)
{
var lockObj = _keyLocks.GetOrAdd(orderId, _ => new object());
lock (lockObj)
{
action();
}
}
最容易被忽略的三个坑
选错粒度往往不是因为不会写,而是没想清“谁真正在竞争”:
-
在 lock 里调 I/O:哪怕粒度再细,只要
lock块里有await File.ReadAllTextAsync(...)或 HTTP 调用,就等于把整把锁“挂起”几秒——其他线程全卡住。必须把耗时操作移出锁外,或改用异步锁(如SemaphoreSlim.WaitAsync)。 -
锁顺序不一致:如果线程 A 先锁
_userLock再锁_orderLock,而线程 B 反过来,就可能死锁。键级锁天然规避这点,但混合多锁时必须约定全局顺序(比如“总是先锁 userId,再锁 orderId”)。 -
误判共享范围:以为
static List<T>是唯一共享点,却忘了 LINQ 查询中的.ToList()返回新实例——结果锁了根本没人访问的对象。确认真正被多线程读写的变量,才是锁的起点。
粒度没有标准答案,只有“这次请求里,哪些线程真的会踩到同一块数据”。测不出来,就加日志看锁等待时间;压不出问题,就说明当前粒度大概率合适。









