monitor 复用 clr 对象同步块,必须用私有引用类型对象作锁;wait 释放锁并入等待队列,需 pulse 唤醒后重抢锁;tryenter 超时返回 false 不挂起线程;lock 仅是 monitor.enter/exit 语法糖,不支持 wait/pulse/超时。

Monitor 不是“自己实现锁”,而是直接复用 CLR 为每个引用类型对象内置的同步块(sync block)——锁状态、等待队列、唤醒逻辑全由运行时托管,你调用 Monitor.Enter 实际是在操作对象头里的一个整数字段。
为什么必须用引用类型对象作 lock 参数
值类型会被装箱,每次 Monitor.Enter 都可能拿到新对象,导致锁失效;字符串字面量因字符串驻留(interning)更危险——看似同一个字符串,实则被多个线程锁住不同实例。只有引用类型(如 new object()、private static readonly object _lock = new object();)能稳定绑定唯一 sync block。
- 常见错误:传入
this或 public 字段,外部代码可能也对它调用Monitor.Enter,造成意外死锁 - 安全做法:声明
private readonly object字段,且不暴露出去 - 禁止用
typeof(MyClass)或string.Empty—— 它们共享全局 sync block,跨类/跨模块污染严重
Monitor.Wait 为什么会释放锁并阻塞
Monitor.Wait 不是“挂起线程”那么简单:它会把当前线程从持有锁的状态中踢出,清空其对该对象的 ownership,然后将线程放入该对象的等待队列(wait queue),最后触发一次 Monitor.Exit 的等效操作。线程真正恢复需满足两个条件:被 Pulse 唤醒 + 再次成功获取锁(重新进入 entry queue 竞争)。
- 必须在已持有锁的前提下调用,否则抛
SynchronizationLockException - 调用后当前线程立刻失去锁,其他线程可立即进入临界区 —— 所以
Wait前的判断逻辑(如队列是否为空)必须和Wait在同一锁区内 -
Pulse只唤醒一个线程,且该线程不会立刻执行,要等当前锁持有者退出临界区后才开始抢锁
Monitor.TryEnter 超时后线程状态如何
Monitor.TryEnter(obj, timeout) 返回 false 时,线程没拿到锁,但也没被挂起;它只是轮询或短暂等待后直接返回,不进等待队列,也不触发上下文切换。这对避免长时阻塞很关键,比如 UI 线程或 I/O 密集型服务中做非关键资源访问。
- 超时单位是毫秒,传
0表示“只试一次”,适合低竞争场景的快速探测 - 返回
true后必须配对Monitor.Exit,否则锁永远不释放 - 不要在循环里无休止重试
TryEnter,容易引发 CPU 尖峰;建议搭配Thread.Sleep或退避策略
和 lock 语句的本质区别在哪
lock(obj) { ... } 编译后就是 Monitor.Enter + try/finally + Monitor.Exit,仅此而已。它不支持 Wait/Pulse,也不支持超时或非阻塞尝试。所有需要“等待条件成立再继续”的场景,都绕不开显式用 Monitor。
- 用
lock写生产者-消费者?不可能——没有Wait就只能轮询或 Sleep,浪费资源 -
Monitor.PulseAll常被误用:它唤醒所有等待线程,但只有一个能抢到锁,其余再次阻塞;若条件只满足一次,多数线程醒来发现条件仍不成立,又得回去Wait - 最易忽略的一点:
Monitor是纯同步机制,不支持async/await;需要异步等待,请换SemaphoreSlim.WaitAsync











