lock锁详解
一、先解释:lock是什么?
lock是 C# 中用于"多线程同步"的关键字,它保证同一时刻只有一个线程能进入被锁住的代码块。
lock(锁对象){// 临界区:同一时刻只允许一个线程执行这里}核心概念:
| 概念 | 说明 |
|---|---|
| 锁对象 | lock括号里的对象,作为"门锁"的标识 |
| 临界区 | 被lock包裹的代码块,需要互斥保护的代码 |
| 互斥 | 一个线程进入后,其他线程必须等待,直到它退出 |
执行流程:
二、为什么需要锁?(问题背景)
多线程并发访问共享数据时,会产生"竞态条件"(Race Condition),导致数据不一致。
通用示例 1:银行账户并发取款(经典问题)
classBankAccount{privateint_balance=1000;// 没有锁:两个线程同时取款会出问题publicvoidWithdraw(intamount){if(_balance>=amount)// ① 检查余额{Thread.Sleep(10);// 模拟耗时(放大竞态窗口)_balance-=amount;// ② 扣款}}}问题:线程 A 和线程 B 同时取 800 元:
- A 执行①:
_balance=1000 >= 800✅ - B 执行①:
_balance=1000 >= 800✅(还没被 A 扣) - A 执行②:
_balance = 200 - B 执行②:
_balance = 200(错误!应该扣两次变 -600,或第二次失败)
加锁修复:
classBankAccount{privatereadonlyobject_lock=new();privateint_balance=1000;publicvoidWithdraw(intamount){lock(_lock)// 同一时刻只有一个线程能进来{if(_balance>=amount){Thread.Sleep(10);_balance-=amount;}}}}现在 A 和 B 会排队执行:A 先扣完(余额 200),B 再检查发现200 < 800,拒绝取款。✅
通用示例 2:多线程累加计数器
intcounter=0;vartasks=newList<Task>();// 1000 个任务同时 counter++for(inti=0;i<1000;i++){tasks.Add(Task.Run(()=>counter++));}Task.WaitAll(tasks.ToArray());Console.WriteLine(counter);// 结果可能不是 1000!(竞态导致丢失更新)counter++实际是三步:读值 → 加一 → 写回。多线程并发时,两个线程可能同时读到同一个值,各自加一写回,导致只加了一次。
加锁修复:
objectlockObj=new();intcounter=0;for(inti=0;i<1000;i++){tasks.Add(Task.Run(()=>{lock(lockObj){counter++;}// 串行化,保证结果 = 1000}));}三、lock的语法细节
lock(lockObject){// 临界区}等价于(编译器展开):
Monitor.Enter(lockObject);try{// 临界区}finally{Monitor.Exit(lockObject);// 无论是否异常,都释放锁}关键点:
lock会自动处理异常情况(finally保证释放锁),不会死锁在异常上。- 锁对象必须是引用类型(
object、string等),不能是值类型。 - 锁对象通常用
private readonly object声明,不对外暴露。
四、lock的注意事项与最佳实践
- 锁对象要私有:不要用
lock(this)或lock(typeof(类)),外部可能拿到同一对象造成意外阻塞。 - 锁粒度要小:只锁真正需要保护的代码,不要在锁内做耗时操作(如文件 IO、网络请求)。
- 不要在锁内调用外部回调:可能死锁。
lock不是万能的:它只保护"代码块",不保护"数据"。所有访问共享数据的代码都必须用同一把锁。- 替代方案:
ConcurrentDictionary、Interlocked、ReaderWriterLockSlim等,按需选择。
七、总结
lock= 多线程互斥锁,保证同一时刻只有一个线程进入临界区,防止竞态条件。- 锁分离设计让业务与日志互不阻塞,提高并发度。
- 事件广播放在锁外,避免持锁调用外部回调导致死锁。