lock对象必须是private readonly引用类型,正确写法为静态资源用private static readonly object、实例资源用private readonly object;.NET 9+推荐SemaphoreSlim替代lock实现异步等待。

lock(obj) 里 obj 必须是 private readonly object
锁对象不是随便 new 一个就行,它必须是 private、readonly、引用类型。常见错误包括:lock(this)、lock("myLock")、lock(typeof(MyClass))、lock(123)。
原因很直接:this 把锁暴露给外部,别人也能 lock(obj);字符串字面量会被驻留(interned),所有同内容字符串指向同一对象,跨类锁冲突;typeof 是公共类型对象,同样不可控;值类型会装箱成新对象,每次 lock 实际锁的是不同实例,等于没锁。
正确写法只有两种场景:
- 保护静态资源:用
private static readonly object _lockObj = new object(); - 保护实例资源:用
private readonly object _lockObj = new object();
.NET 9+ 推荐用 System.Threading.Lock 类型(需 new Lock()),性能更好,但语义和用法完全兼容老式 object 锁。
lock 块里不能 await,也不能做 I/O 或耗时操作
lock 是同步原语,底层调用 Monitor.Enter,它会阻塞线程。一旦你在 lock 块里写 await,编译器直接报错;如果退而求其次用 .Wait() 或 .Result,会导致线程池饥饿、死锁或异常吞没。
典型错误模式:
-
lock(_lockObj) { await File.ReadAllTextAsync(path); }—— 编译失败 -
lock(_lockObj) { httpClient.GetAsync(url).Wait(); }—— 线程卡死,后续请求排队
替代方案很明确:用 SemaphoreSlim。
初始化:private readonly SemaphoreSlim _sem = new SemaphoreSlim(1, 1);
使用:await _sem.WaitAsync(); try { /* 临界区 */ } finally { _sem.Release(); }
注意:不要用 using 包裹 SemaphoreSlim,它不是一次性资源,要复用。
锁的范围必须覆盖完整的读-改-写周期
很多“偶发数据错乱”或 Collection was modified 异常,根源不是没加锁,而是锁没盖住全部共享访问路径。
比如对 Dictionary<string, int> 做“先查后删”:
- 错:先
if (_dict.ContainsKey(key))(没锁),再lock { _dict.Remove(key); }—— 中间可能被其他线程修改 - 对:整个判断+删除都放进
lock块里
更隐蔽的问题是多个字段共用状态,例如 _cache 和 _lastUpdated。它们必须用同一把锁,不能一个用 _lockA、一个用 _lockB,否则状态不一致。
还有嵌套锁:如果 A 方法先锁 _lockA 再锁 _lockB,B 方法反着来,就必然死锁。解决办法只有一个:全局约定锁获取顺序,代码审查强制执行。
比 lock 更快、更安全的替代方案优先级
lock 是兜底方案,不是首选。真正该用它的场景非常窄:极短、无 I/O、无 await、无虚方法调用的临界区。
大多数情况下,有更优解:
- 纯计数器增减 → 用
Interlocked.Increment(ref _count),零锁开销 - 读多写少的缓存 → 用
ReaderWriterLockSlim,允许多个读线程并发 - 需要异步等待 → 用
SemaphoreSlim,支持WaitAsync - 简单标志位开关 → 用
Volatile.Read/Volatile.Write或MemoryBarrier
最容易被忽略的一点:lock 的性能损耗不在加锁本身,而在**阻塞带来的上下文切换和线程排队**。当临界区哪怕只多出 1ms,高并发下吞吐就会断崖下跌。所以别迷信“加了 lock 就线程安全”,得看它锁住了什么、锁了多久、有没有更轻量的替代。


















