
1) 【一句话结论】
在.NET中,使用lock实现线程安全的核心是依赖Monitor的Enter/Exit机制控制共享资源访问,需遵循避免死锁原则(如避免嵌套锁、遵循加锁顺序),同时需优化锁粒度(缩小保护范围)和减少持有时间(降低阻塞),并理解锁的性能影响(如自旋锁在高负载下的开销)。
2) 【原理/概念讲解】
.NET的lock是同步原语,底层由Monitor封装操作系统互斥锁。执行lock语句时,会自动调用Monitor.Enter()进入临界区,此时Monitor会先尝试自旋(循环检查锁是否可用,默认5次,减少上下文切换开销),若自旋失败则将线程加入等待队列(按FIFO顺序等待锁释放)。当线程退出lock块时,调用Monitor.Exit()释放锁,唤醒等待队列中的第一个线程。此外,Monitor还支持优先级继承机制,若高优先级线程等待低优先级线程持有的锁,会临时提升低优先级线程的优先级,避免低优先级线程阻塞高优先级线程。可以类比“共享会议室”:会议室是共享资源,钥匙是锁,进入前必须先拿到钥匙(Monitor.Enter),离开时归还(Monitor.Exit),同一时间只有一个线程在会议室操作,保证数据安全。Monitor自动管理线程的等待和唤醒,无需手动处理。
3) 【对比与适用场景】
| 特性/要点 | 说明 |
|---|---|
| 定义 | C#中的同步块,基于Monitor实现,用于保护共享资源访问 |
| 原理 | 调用Monitor.Enter进入临界区(含默认5次自旋+等待队列调度),Monitor.Exit退出,自动管理优先级继承 |
| 使用场景 | 保护共享变量、对象、方法等,需互斥访问的情况(如计数器、队列操作) |
| 注意点 | 1. 避免嵌套锁(锁内再锁,导致死锁);2. 遵循加锁顺序(如按A→B顺序加锁,避免循环等待);3. 锁粒度优化(缩小锁范围,如只锁关键变量,提高并发性能);4. 减少持有时间(减少阻塞,提升吞吐量,如异步操作中快速释放锁) |
4) 【示例】
伪代码示例(共享计数器操作):
public class SafeCounter
{
private int _count = 0;
private readonly object _lock = new object(); // 锁对象
public void Increment()
{
lock (_lock) // 进入临界区,保护计数器递增
{
_count++;
}
}
public int GetCount()
{
lock (_lock) // 进入临界区,保护读取操作
{
return _count;
}
}
}
多个线程调用Increment或GetCount时,通过lock保证每次只有一个线程执行关键操作,避免数据竞争。若不使用lock,多个线程同时递增会导致计数器值错误(如两个线程同时读取_count为0,各自加1后写回,最终计数器为1而非2)。
5) 【面试口播版答案】
面试官您好,关于.NET中使用lock实现线程安全,核心是通过Monitor的Enter/Exit机制控制共享资源访问。具体来说,lock会自动调用Monitor.Enter()进入临界区,Monitor.Exit()退出,确保同一时间只有一个线程访问被保护的资源。为了避免死锁,关键是要避免嵌套锁(比如锁内再锁)和遵循加锁顺序(比如多个锁A、B,始终按A→B的顺序加锁)。另外,还要注意锁的粒度,比如如果只需要保护某个变量,而不是整个对象,可以缩小锁范围,避免影响其他方法的并发性能。长时间持有锁也会阻塞其他线程,降低系统吞吐量,所以应尽量减少锁的持有时间,比如在lock内快速执行关键操作,或者考虑异步操作中快速释放锁。
6) 【追问清单】
lock和Mutex有什么区别?lock是C#语法糖,基于Monitor实现,用于同一进程内线程同步;Mutex是操作系统级互斥对象,可用于跨进程同步,但更复杂,需要手动管理,且性能较低。List),如何用lock保护?lock保护对集合的所有操作,比如遍历时也要加锁(因为遍历可能涉及修改,如Remove),否则可能导致并发问题,比如一个线程在遍历,另一个线程在移除元素,导致集合状态不一致。lock更高效的同步方式?Interlocked类(如Interlocked.Increment)避免锁开销;对于细粒度控制,可考虑信号量(Semaphore)或条件变量(ConditionVariable),但适用场景不同。7) 【常见坑/雷区】
lock块内再次使用lock,会导致死锁(外层锁未释放,内层锁无法获取,形成循环等待)。