lock 是 Monitor 的语法糖,没有“锁升级”概念
直接说结论:
lock关键字在 C# 中**不支持也不实现 JVM 那种偏向锁 → 轻量级锁 → 重量级锁的锁升级机制**。它底层始终调用
Monitor.Enter和
Monitor.Exit,走的是 .NET 运行时统一的同步块(SyncBlock)路径——一旦争用发生,就可能触发内核态等待(如
CRITICAL_SECTION或事件对象),不存在“先尝试轻量自旋、失败再升级”的分阶段策略。
lock 的真实实现:SyncBlock + 线程 ID 绑定
每个被
lock的引用类型对象(
object、
class实例等)在堆上分配时,会预留一个
SyncBlockValue字段(32 位)。这个字段前 6 位用于标记状态(比如第 5 位是“已锁定”标志),后 26 位存储指向全局
SyncTable中某个
SyncBlock条目的索引。
当线程 A 执行
lock(_obj)时: 检查
_obj.SyncBlockValue第 5 位是否为 0;是,则尝试从
g_pSyncTable分配一个新
SyncBlock,填入当前线程 ID,并把索引写回后 26 位 若第 5 位已被置 1,说明已被占用,就去查对应
SyncBlock里存的线程 ID —— 如果不是自己,就挂起等待(进入内核态) 释放锁时,
Monitor.Exit清除该
SyncBlock的持有者,并可能复用或回收条目
注意:
string类型禁止作为
lock对象(字符串驻留导致意外共享),值类型会触发装箱(每次新建对象,锁失效),
null直接抛
NullReferenceException。
别把 .NET 的 SpinLock / ReaderWriterLockSlim 当成 lock 升级
有人看到
SpinLock或
ReaderWriterLockSlim就联想“这是 lock 的升级版”,这是误解。它们是**完全独立的同步原语**:
SpinLock:纯用户态自旋(无上下文切换),适合极短临界区且低争用场景;但它是结构体,不能用于
lock语句,必须手动
Enter/
Exit
ReaderWriterLockSlim:支持读多写少的细粒度并发,内部有状态机和队列,和
Monitor无继承/升级关系
lock永远是排他、不可重入、基于对象头的简单互斥 —— 它没“读写分离”,也没“尝试获取超时”能力
想替代
lock?选对工具就行,但别指望编译器或运行时自动帮你“升级”它。
容易踩的坑:看似安全,实则锁失效
最常见错误不是不会用
lock,而是锁的对象不对:
private readonly object _lock = new object();✅ 安全 —— 私有、只读、静态(如果跨实例共享)才真正唯一
lock(this)❌ 危险 —— 外部可拿到
this并也去
lock,造成死锁或意外同步
lock("mylock") ❌ 错误 —— 字符串字面量被驻留,所有地方用同一字符串字面量都会锁住同一个对象
lock(someProperty)❌ 不稳 —— 属性 getter 可能返回新对象,每次
lock实际锁不同实例
另外,
lock块里别做耗时操作(I/O、网络、长循环),否则阻塞其他线程,且无法取消 —— 这时候该换
SemaphoreSlim.WaitAsync()或专用异步锁。
归根结底,
lock就是一把朴素但可靠的门栓:它不聪明,不自适应,也不升级;你给它一个好门(正确对象),它就守好;你给它一扇破窗(
string或可变属性),它就形同虚设。
