lock锁的是一个引用类型的对象实例,不是代码块或类型本身;必须为private readonly(静态或实例)对象,避免lock(this)、字符串、typeof等共用对象导致锁失效或死锁。

lock 语句到底锁的是什么
它锁的是一个引用类型的对象实例,不是代码块,也不是类型本身。很多人写 lock(typeof(MyClass)) 或 lock(this),结果在多线程下出现死锁或锁失效——因为这些对象可能被其他无关代码共用。
真正起作用的是那个对象头里的同步块索引(Sync Block Index),由 Monitor.Enter 和 Monitor.Exit 操作。所以必须确保锁对象是私有、稳定、不可变的。
- ✅ 推荐:
private static readonly object _lockObj = new object(); - ❌ 避免:
lock("myLock")(字符串驻留导致跨方法共享同一把锁) - ❌ 避免:
lock(null)(直接抛ArgumentNullException) - ❌ 避免:
lock(_counter)(值类型会装箱成不同对象,锁无效)
为什么 lock 块里不能 await
lock 是同步原语,依赖线程持有锁对象;而 await 会让出当前线程,控制流恢复时很可能在另一个线程上继续执行——此时原线程已释放锁,临界区完全失控。
现象是:数据被并发修改、计数器跳变、集合报 Collection was modified,但堆栈里根本看不到 lock 相关线索。
- 错误写法:
lock(_lockObj) { await Task.Delay(100); } - 正确替代:
await _semaphore.WaitAsync(); try { ... } finally { _semaphore.Release(); }(用SemaphoreSlim) - 更轻量选择:如果只是递增/赋值,优先用
Interlocked.Increment(ref _counter),它不阻塞、无锁、原子
锁的范围该多大才合适
锁越小越好。锁住整个方法、或包含 I/O、网络、用户输入的逻辑,会导致其他线程长时间排队,CPU 利用率低、响应卡顿,甚至触发超时或重试风暴。
典型反例:在 lock 块里调用 HTTP API、读文件、弹窗等待用户确认——这些都不是原子操作,也不该由锁来保护。
- ✅ 合适场景:更新一个字段、往字典里加一个键、检查并设置标志位(如
_isRunning = true) - ✅ 技巧:读操作若不要求强一致性,可不加锁;写操作必须覆盖所有路径(包括异常分支)
- ⚠️ 注意:不要在锁内调用虚方法、事件或委托——外部代码可能再次尝试获取同一把锁,引发死锁
单例初始化里的双重检查锁(Double-Checked Locking)为什么需要两个 if
外层 if (_instance == null) 是性能优化:避免每次访问都进入 lock,毕竟锁有开销;内层 if (_instance == null) 是安全兜底:防止多个线程同时通过外层判断后,都进入锁块重复创建实例。
这个模式只在静态单例中成立,且要求字段用 volatile 或 Lazy<t></t> 保证可见性。手动实现容易出错,.NET 6+ 更推荐直接用 Lazy<t></t>。
- 关键点:
lock不解决“内存可见性”,只解决“执行互斥”;没volatile或正确屏障,一个线程创建完实例,另一个线程可能仍读到旧值 - 简化方案:
private static readonly Lazy<myservice> _instance = new(() => new MyService());</myservice>
lock 从防护墙变成定时炸弹。


















