lock 就是 Monitor.Enter + Monitor.Exit 的语法糖
直接说结论:
lock(obj) { ... } 在编译后,等价于手动调用 Monitor.Enter(obj)和
Monitor.Exit(obj),并自动包在
try-finally块里。这意味着:你用
lock能做到的,
Monitor全都能做;但反过来,
Monitor能做的(比如超时、等待唤醒),
lock做不到。
lock只支持引用类型锁对象 —— 如果传值类型(如
int、
struct),编译器会报错
Monitor.Enter理论上可传值类型,但会触发装箱,每次装箱生成新对象,导致锁失效甚至死锁,绝对不要这么做
lock自动确保异常下锁释放;而手写
Monitor.Enter必须配
try-finally,漏掉
Monitor.Exit就是典型死锁源头
Monitor.Enter 的正确用法(含 C# 4.0+ 安全重载)
老式写法(易出错):
Monitor.Enter(lockObj);
try
{
// 临界区代码
}
finally
{
Monitor.Exit(lockObj);
}问题在于:如果
Monitor.Enter本身失败(极罕见)或线程被中断,
try块可能根本没执行,但
finally还是会跑 —— 此时调
Monitor.Exit会抛
SynchronizationLockException。
C# 4.0 起推荐用带
ref bool的安全重载:
bool lockTaken = false;
try
{
Monitor.Enter(lockObj, ref lockTaken);
// 临界区代码
}
finally
{
if (lockTaken)
Monitor.Exit(lockObj);
}
lockTaken由
Monitor.Enter自动设置为
true仅当成功获取锁 即使
Enter抛异常或未进入临界区,
lockTaken仍为
false,
Exit不会被误调 这是目前最健壮的手动
Monitor用法
什么时候非得用 Monitor 而不是 lock?
只有这三类场景值得放弃
lock的简洁性,去碰
Monitor: 需要带超时的锁获取:用
Monitor.TryEnter(obj, timeoutMs)或
TryEnter(obj, timeout, ref lockTaken),避免线程无限等待 要实现线程协作(如生产者-消费者):必须用
Monitor.Wait()主动释放锁并挂起,再靠
Monitor.Pulse()或
PulseAll()唤醒特定等待线程 动态控制锁粒度或嵌套逻辑:比如先尝试加锁,失败则走降级路径,而不是硬等
其他所有普通互斥场景 —— 比如保护字段、同步日志输出、更新共享集合 ——
lock更安全、更短、更不易错。
常见踩坑点:锁对象选错 or 改了
无论
lock还是
Monitor,锁失效往往不是语法问题,而是对象语义错了: 锁对象不能是
public或可变字段(比如
public object SyncRoot = new object();),外部代码改了它,等于换锁,同步就崩了 必须用
private readonly object _syncLock = new object();——
readonly保证引用不变,
private防止外部干扰 别用
this、
typeof(T)、字符串字面量或装箱值类型作锁对象,它们要么暴露给外界,要么不可控地复用/新建 多个逻辑相关但不完全相同的资源,别共用一个锁对象(性能瓶颈);也不要把无关资源塞进同一个锁(扩大竞争面)
Monitor 本身没有魔法,它只认“对象标识”。锁对象一旦变了(哪怕只是被重新赋值),之前持有的锁就跟它再无关系 —— 这种错误不会编译报错,但会让多线程行为彻底失控。
