面试题答案
一键面试锁的粒度控制
- 细化锁的范围:
- 在代码中,尽量缩小持有信号量或互斥锁的代码块范围。例如,如果只有部分数据需要同步访问,就只在操作这部分数据的代码段加锁。
// 假设data是需要保护的共享数据 private static object dataLock = new object(); private static List<int> data = new List<int>(); public void AddData(int value) { lock (dataLock) { // 只在操作共享数据时加锁 data.Add(value); } }
- 使用多个锁:
- 对于复杂的数据结构或系统,可以将其分解为多个部分,为每个部分分配单独的锁。这样不同部分的操作可以并行进行,减少锁竞争。
- 例如,一个包含多个独立子系统的应用程序,可以为每个子系统设置单独的互斥锁。
private static object subsystem1Lock = new object(); private static object subsystem2Lock = new object(); public void Subsystem1Operation() { lock (subsystem1Lock) { // 子系统1的操作 } } public void Subsystem2Operation() { lock (subsystem2Lock) { // 子系统2的操作 } }
等待策略调整
- 使用自旋锁:
- 在短时间内需要频繁获取锁的场景下,可以考虑使用自旋锁。自旋锁在等待锁时不会立即将线程挂起,而是在一定时间内不断尝试获取锁,减少线程上下文切换的开销。
- 在C#中,可以使用
System.Threading.SpinLock
结构体。
private static SpinLock spinLock = new SpinLock(); public void SpinLockOperation() { bool lockTaken = false; try { spinLock.Enter(ref lockTaken); // 临界区代码 } finally { if (lockTaken) { spinLock.Exit(); } } }
- 超时等待:
- 对于信号量和互斥锁,设置合理的等待超时时间。如果在超时时间内未能获取到锁,可以执行其他逻辑,而不是一直无限期等待。
- 以
SemaphoreSlim
为例:
private static SemaphoreSlim semaphore = new SemaphoreSlim(1, 1); public async Task SemaphoreOperation() { if (await semaphore.WaitAsync(TimeSpan.FromSeconds(5))) { try { // 临界区代码 } finally { semaphore.Release(); } } else { // 超时处理逻辑 } }
资源管理避免内存泄漏
- 正确释放资源:
- 对于信号量和互斥锁,确保在使用完毕后正确释放资源。对于
Mutex
,使用using
语句来确保其正确释放。
using (Mutex mutex = new Mutex()) { mutex.WaitOne(); try { // 临界区代码 } finally { mutex.ReleaseMutex(); } }
- 对于
SemaphoreSlim
,同样要在使用后调用Release
方法。
- 对于信号量和互斥锁,确保在使用完毕后正确释放资源。对于
- 避免不必要的对象创建:
- 尽量复用已有的信号量或互斥锁对象,而不是频繁创建和销毁。例如,可以将信号量或互斥锁定义为静态成员变量,在应用程序生命周期内复用。
避免死锁
- 获取锁的顺序一致:
- 在多线程环境中,确保所有线程以相同的顺序获取多个锁。如果线程A按照锁1、锁2的顺序获取锁,那么线程B也应该按照相同的顺序获取锁,否则可能会导致死锁。
- 死锁检测与恢复:
- 可以使用一些工具或算法来检测死锁。例如,通过记录线程获取锁的顺序和状态,定期检查是否存在死锁。如果检测到死锁,可以采取一些恢复措施,如终止一个或多个线程,重新分配资源等。在一些高级的分布式系统中,可能会使用分布式死锁检测算法来处理跨节点的死锁问题。