lock本质是编译器强制保证的Monitor.Enter与Monitor.Exit成对调用,非语法糖;必须用try/finally确保异常下锁释放,且锁对象须为稳定生命周期的私有只读引用类型。

lock 本质就是 Monitor.Enter + Monitor.Exit
直接说结论:lock 不是语法糖,而是编译器强制保证的 Monitor.Enter 和 Monitor.Exit 成对调用,底层完全依赖 Monitor 类。这意味着你手写 Monitor 调用时,必须自己处理异常路径下的 Monitor.Exit,否则锁永远不释放——这是最常踩的坑。
常见错误现象:NullReferenceException 或 ThreadInterruptedException 发生在 Monitor.Enter 后、业务逻辑前,导致 Monitor.Exit 被跳过,后续线程永久阻塞。
- 正确做法永远用
try/finally包裹Monitor.Exit(lock自动帮你做了) -
lock(obj)中的obj必须是非 null 引用类型;值类型会装箱,每次新建对象,锁失效 - 不要 lock(this)、lock(typeof(X)) 或 lock(string),它们暴露锁对象,外部可随意争抢
lock 只能锁住引用对象,不能锁字段或属性
lock 的参数必须是引用类型变量,且该变量指向的对象生命周期要稳定。很多人试图 lock 一个 public 字段或自动属性的 backing field,结果发现没效果——因为字段值可能被意外修改,或者 getter 每次返回新对象。
使用场景:共享资源如 Dictionary<string int></string>、静态缓存、日志队列等需要保护的实例。
- 推荐声明私有只读字段:
private readonly object _syncLock = new object(); - 避免 lock 公共对象(比如 public static object SyncRoot),外部代码可能拿它做别的同步,引发死锁
- 不要 lock 可变引用:比如
lock(_items),如果_items在别处被重新赋值,锁对象就变了
Monitor.Wait / Pulse 配合 lock 实现条件等待
单纯 lock 只能互斥,无法解决“等某个条件成立再继续”的问题。这时必须进入 Monitor 的进阶用法:Monitor.Wait 会原子性地释放锁并挂起当前线程,直到另一个线程调用 Monitor.Pulse 或 Monitor.PulseAll 唤醒它。
关键约束:所有 Wait/Pulse 必须在已获取同一把锁的前提下执行,也就是必须在 lock 块内调用。
-
Monitor.Wait返回 true 表示被 Pulse 唤醒,false 表示超时(如果传了 timeout) - 唤醒后线程不会立即执行,而是重新竞争锁;所以要用 while 循环检查条件,不能用 if(防止虚假唤醒)
-
Pulse只唤醒一个等待线程,PulseAll唤醒全部;后者更安全但开销大
lock (_syncLock)
{
while (!_dataReady)
Monitor.Wait(_syncLock);
ProcessData();
}
lock 性能瓶颈和替代方案的取舍点
单个 lock 在高并发下会成为串行瓶颈,尤其临界区较长时。这不是锁本身慢,而是线程排队等待时间长。这时候要考虑是否真的需要全局互斥。
性能影响明显的表现:CPU 使用率低但响应延迟高,ThreadPool 线程大量处于 WaitSleepJoin 状态。
- 优先缩小临界区:把非共享操作(如计算、IO)移出
lock块 - 读多写少场景考虑
ReaderWriterLockSlim,但它比lock重,仅当读操作远多于写时才值得换 - 无锁结构(如
ConcurrentDictionary、Channel<t></t>)适合标准场景,但无法覆盖自定义复合逻辑 - 分段锁(如按 key.GetHashCode() % N 分桶)能显著降低争用,但增加实现复杂度
真正容易被忽略的是锁粒度与业务语义的匹配——锁太粗,吞吐上不去;锁太细,可能破坏一致性,比如转账操作中“扣款+入账”必须在同一把锁下完成,拆成两个锁就出错了。


















