lock锁的是引用类型的对象实例,非代码块或类型本身;应使用私有静态只读object字段,避免锁值类型、字符串、null或public对象。

lock 语句到底锁的是什么
它锁的是一个引用类型的对象实例,不是代码块,也不是类型本身。很多人误以为 lock(typeof(MyClass)) 或 lock(this) 安全,其实极易引发死锁或跨实例失效。
-
lock内部靠Monitor.Enter和Monitor.Exit实现,本质是给对象头加一个同步块索引 - 必须用私有、静态、只读的
object字段,比如private static readonly object _lockObj = new object(); - 不能锁值类型(会装箱成不同对象)、字符串(可能被驻留)、
null(直接抛ArgumentNullException) - 锁 public 对象(如
this或 public 字段)等于把锁暴露给外部,其他代码也能lock它,协作失控
什么时候该用 lock,什么时候不该用
它只适合短时间、确定无阻塞的临界区保护。一旦涉及 I/O、网络、用户输入、或可能抛异常又没写 finally,就该换方案。
- 适合:递增计数器、更新字典中的某个键、维护简单状态标志
- 不适合:读文件后更新缓存、调用 HTTP API 后写日志、等待用户确认再修改数据
- 替代选择优先级:先看能不能用
Interlocked(如Interlocked.Increment),再考虑ReaderWriterLockSlim(读多写少),最后才是lock - 异步方法里不能直接
await在lock块内——lock不支持异步,会卡住线程,要用SemaphoreSlim.WaitAsync
常见报错和对应修复
最典型的是死锁和 NullReferenceException,但错误信息往往不直接指向 lock 本身。
- 现象:
NullReferenceException在lock(obj)行 —— 检查obj是否为null,尤其在构造函数未完成时就调用了加锁方法 - 现象:程序卡死、CPU 低、线程堆栈里大量
Monitor.Enter等待 —— 很可能是嵌套锁顺序不一致,比如方法 A 先锁_lockA再锁_lockB,而方法 B 反过来 - 现象:并发修改集合时报
InvalidOperationException: Collection was modified——lock没包住整个遍历+修改过程,或者用了foreach却在循环里改集合 - 修复原则:所有访问共享资源的路径,无论多深,都必须经过同一把锁;避免在锁内调用外部可重入的代码(如事件、虚方法、委托)
lock 和 async/await 的冲突怎么绕开
lock 是同步原语,和 await 天然不兼容。强行在 lock 里 await 会导致锁被释放,其他线程趁虚而入,临界区失效。
- 错误写法:
lock(_lockObj) { await Task.Delay(100); ... }—— 编译不过,且逻辑已崩 - 正确做法:用
SemaphoreSlim替代,初始化时设initialCount: 1,然后await _semaphore.WaitAsync()+try/finally _semaphore.Release() - 注意:
SemaphoreSlim支持异步等待,但它是基于信号量的,不是严格意义上的“互斥锁”,若需完全等价语义,还得配合async友好的状态管理 - 更轻量的选择:如果只是保护单个变量读写,优先用
Interlocked系列方法,它们原子、无锁、零等待
真正难的不是写对 lock,而是判断「这里真的需要锁吗」——多数性能问题来自过早加锁,或锁了不该锁的东西。别让 lock 成为掩盖设计缺陷的胶带。










