绝大多数场景下用lock就够了;但需等待条件、唤醒线程或超时控制时,必须改用monitor。lock对象须唯一且私有,禁用new object()、this和字符串字面量;推荐static readonly object。

lock 不是万能的同步开关,它只在你用对对象、写对范围、避开常见误用时才真正线程安全。直接上结论:**绝大多数场景下,用 lock 就够了;但一旦需要等待条件、唤醒其他线程或控制超时,就必须切到 Monitor**。
为什么 lock(obj) 不能用 new object() 或 this 作锁对象
常见错误是每次进方法都 new object(),或者直接 lock(this)。这两种写法等于没锁——因为锁对象不唯一,线程之间根本无法形成互斥。
-
new object()每次新建一个实例,不同线程拿到的是不同对象,lock完全失效 -
this是公开引用,外部代码可能也拿它加锁,造成意外死锁或干扰 - 字符串字面量(如
lock("mykey"))因字符串驻留(interning)可能被跨类共享,同样危险
正确做法是声明一个 private readonly object 字段,且推荐 static readonly(静态只读),确保锁对象生命周期与资源访问范围一致。例如:
private static readonly object _syncRoot = new object();
public void UpdateCounter() {
lock (_syncRoot) {
_counter++;
}
}
lock 和 Monitor.Enter/Exit 的等价性与关键差异
lock 看似简单,但它背后是编译器生成的 try-finally 块 + Monitor.Enter(..., ref lockTaken) 调用。自 .NET Framework 4.0 起,这个 ref bool 参数不是可选的——它是为了应对 ThreadAbortException 等极端中断,保证锁不会“卡住”。
-
lock自动处理异常安全释放,你没法绕过它去手动Monitor.Exit -
Monitor.Enter(obj, ref taken)允许你检查taken再决定是否执行临界区,适合做“尝试获取锁”逻辑 - 如果你需要带超时(比如最多等 100ms),
Monitor.TryEnter(obj, 100)是唯一选择,lock不支持
下面这段代码和 lock 行为一致,但多了超时控制能力:
bool lockTaken = false;
try {
if (Monitor.TryEnter(_syncRoot, 100)) {
lockTaken = true;
_counter++;
} else {
// 获取锁失败,可降级或记录
throw new TimeoutException("Failed to acquire lock within 100ms");
}
} finally {
if (lockTaken) Monitor.Exit(_syncRoot);
}
Monitor.Wait/Pulse 为什么不能和 lock 混用
很多人以为 lock 块里调用 Monitor.Wait 就能“释放锁并等待”,结果程序卡死或抛 SynchronizationLockException。这是因为:Wait 要求当前线程必须已持有该对象的锁,且 Wait 会原子性地释放锁 + 进入等待队列;而 lock 块结束时自动 Exit,时机不可控。
- 必须用
Monitor.Enter/Exit显式管理锁,才能配合Wait/Pulse -
Wait只能在已持有锁的线程中调用,否则抛异常 -
Pulse不会唤醒正在运行的线程,只通知等待队列中的一个线程;如果没人等,信号就丢了
典型生产者-消费者模式中,Get 方法必须这样写(不能用 lock):
public T Get() {
Monitor.Enter(_syncRoot);
try {
while (_queue.Count == 0) {
Monitor.Wait(_syncRoot); // 释放锁并等待
}
return _queue.Dequeue();
} finally {
Monitor.Exit(_syncRoot);
}
}
性能影响和替代方案的取舍点
锁本身不慢,慢的是争抢。当多个线程频繁竞争同一把锁时,lock 会退化成串行执行,CPU 利用率反而下降。这时候要问自己:是不是真需要独占?
- 纯读多写少场景,
ReaderWriterLockSlim比lock高效得多 - 只是保护一个整数累加?
Interlocked.Increment(ref _counter)零开销,比锁快一个数量级 - 操作集合?优先考虑
ConcurrentQueue<t></t>、ConcurrentDictionary<k></k>,它们内部做了无锁优化 - 跨进程同步?
lock和Monitor都无效,必须用Mutex或命名信号量
最容易被忽略的一点:锁的粒度。不要把整个方法体包进 lock,只锁真正访问共享状态的几行。比如日志写入前拼接字符串、格式化时间,这些完全可以放到锁外面做。











