lock用错对象会导致完全失效,因锁非同一实例或值类型装箱致各线程互不感知;lock内禁用await,应改用SemaphoreSlim.WaitAsync();读多写少宜用ReaderWriterLockSlim或无锁设计;锁不能防外部修改,需封装状态并控制暴露。
lock 用错对象会导致完全失效
很多人以为只要写了 lock 就安全了,结果并发时还是读到脏数据。根本原因是锁的对象没选对——比如每次新建一个对象当锁,或者锁的是值类型(自动装箱后产生新引用),实际各线程锁的压根不是同一个东西。
实操建议:
- 锁变量必须是 静态的、同一实例的引用类型,常见写法是
private static readonly object _lockObj = new object(); - 绝对不要锁
this、锁局部变量、锁字符串字面量(字符串驻留可能让多个地方意外共享锁) - 如果锁的是集合或自定义类实例,确保该实例在整个生命周期中不被替换(比如别在锁块里给它重新赋值)
lock 块里调用异步方法会崩掉且不报错
lock 是同步原语,它会阻塞当前线程;而 await 可能导致线程切换,一旦 await 后续代码在别的线程执行,就可能尝试再次进入 lock(甚至死锁),更糟的是编译器不会报错,运行时行为不可控。
实操建议:
- lock 块内禁止出现
await,哪怕只是调用一个看似简单的 async 方法也不行 - 需要异步 + 线程安全?改用
SemaphoreSlim.WaitAsync()替代lock,它是真正支持异步等待的同步原语 - 如果只是想保护一段“读-改-写”逻辑,且操作本身很快,优先保持同步 +
lock;别为了“看起来更现代”硬套 async
读多写少场景下 lock 性能拖垮吞吐量
所有读操作都得排队等写锁释放,哪怕只是读一个字段,也会被 lock 拦住。高并发读场景下,CPU 很多时间花在争抢锁上,而不是干活。
实操建议:
- 纯读操作不需要锁——前提是被读的数据本身是线程安全的(如
ConcurrentDictionary、ImmutableArray<t></t>) - 如果必须自己维护状态,考虑用
ReaderWriterLockSlim:读操作用EnterReadLock(),写操作用EnterWriteLock(),允许多个读并发 - 注意
ReaderWriterLockSlim不是免费的,读锁也有开销;简单场景下,用Volatile.Read()或Interlocked.CompareExchange()配合无锁设计,反而更轻量
lock 不能防止外部修改引发的逻辑不一致
锁只能保证临界区代码原子执行,但挡不住外部代码在你锁外悄悄改了共享状态。比如你用 lock 保护了订单状态更新,但另一个线程直接改了订单的 Status 字段,你的锁毫无意义。
实操建议:
- 把共享状态封装成类,字段设为
private,只暴露带锁的公开方法(如CompleteOrder()) - 避免把可变对象(如
List<t></t>)作为公共属性返回——外部拿到引用后直接改,你的锁拦不住 - 必要时用不可变类型(
ImmutableList<t></t>)或深拷贝返回,切断外部修改路径
真正难的不是加锁,而是判断“哪段逻辑必须原子”和“哪些状态不该暴露”。锁只是工具,边界划错了,锁得再严也没用。











