一、介绍
在多线程中,每个线程都有自己的资源,但是有些数据是共享的,即每个线程都可以访问修改。这可能带来的问题就是几个线程同时执行一个数据,导致数据的混乱,产生不可预料的结果,因此我们必须避免这种情况的发生。
c#语言自带的lock(){}语句只能用于锁定引用类型,基于引用地址的锁定,但是如果遇到需要让值类型作为锁的业务情况【例如用户Id有可能是long类型】,也有可能是字符串类型需要作为锁定的值的时候【只有在一开始定义在代码中的字符串是存储在字符串常量区中的,如果在代码执行过程中生成的字符串是没有存储在字符串常量区中的,这样就会导致他们引用的地址不一样,从而不能达到线程同步的效果】
验证:执行过程中生成的字符串不是存在在字符串常量区中的
var str1 = "123";
var str2 = "123";
var str3 = 123.ToString();
//判断str1和str2字符串值是否相同
Console.WriteLine(str1 == str2);
//判断str1和str3字符串值是否相同
Console.WriteLine(str1 == str3);
Console.WriteLine("------------------");
//判断str1和str2字符串的引用地址是否一致
Console.WriteLine(object.ReferenceEquals(str1, str2));
//判断str1和str3字符串的引用地址是否一致
Console.WriteLine(object.ReferenceEquals(str1, str3));
Console.ReadKey();
结果
True
True
------------------
True
False
请按任意键继续. . .
为了使得这种情况也支持锁定,就得锁定的是对象的内容而非引用地址,当前组件实现的原理是:
锁定对象【值类型 OR 引用类型】->映射锁定对象【引用类型lockObject】->lock(lockObject)
注意:因为映射实现方式是字典,所以作为锁定的数据类型最好实现了GetHashCode()、Equals(object obj)两个方法
二、方法介绍
方法 | 介绍 |
---|---|
MonitorLock.Enter(T lockObject) | 锁定指定的资源,如果没有获取到此资源就一直阻塞当前线程等待资源释放 |
MonitorLock.TryEnter(T lockObject) | 尝试锁定指定资源,如果没有获取到就返回“假”,如果获取到就返回“真” |
MonitorLock.TryEnter(T lockObject, int millisecondsTimeout) | 尝试锁定指定资源,尝试等待指定的毫秒数[在此时间内是阻塞当前线程的],如果都还没有获取到就返回“假”,如果在指定的时间内获取到就返回“真” |
MonitorLock.Exit(T lockObject) | 释放当前资源的锁定,如果资源不是当前线程锁定的就会报错 |
MonitorLock.IsEntered(T lockObject) | 判断当前线程是否是否锁定了当前资源,他配合MonitorLock.Exit(oLock) 使用防止释放的时候出错 |
MonitorLock.Lock(T key) | 生成锁定对象 配合 using(){} 实现 lock(){}效果 |
三、实现代码
/// <summary>
/// 锁对象【根据对象值的锁,而非基于引用的锁】
/// </summary>
/// <typeparam name="T">锁的资源类型【引用类型】</typeparam>
public class MonitorLock<T> : IDisposable
{
/// <summary>
/// 锁值字典
/// </summary>
private static readonly ConcurrentDictionary<T, object> LockDictionary;
static MonitorLock()
{
LockDictionary = new ConcurrentDictionary<T, object>();
}
/// <summary>
/// 锁定指定的资源,如果没有获取到此资源就一直阻塞当前线程等待资源释放
/// </summary>
/// <param name="lockObject">锁定的对象</param>
public static void Enter(T lockObject)
{
var obj = LockDictionary.GetOrAdd(lockObject, new object()); //得到或者添加当前Key对象对应的锁定对象
Monitor.Enter(obj); //锁定当前对象
}
/// <summary>
/// 尝试锁定指定资源,
/// 如果没有获取到就返回“假”,
/// 如果获取到就返回“真”
/// </summary>
/// <param name="lockObject">锁定的对象</param>
/// <returns>是否获取锁成功</returns>
public static bool TryEnter(T lockObject)
{
var obj = LockDictionary.GetOrAdd(lockObject, new object()); //得到或者添加当前Key对象对应的锁定对象
return Monitor.TryEnter(obj); //锁定当前对象
}
/// <summary>
/// 尝试锁定指定资源,
/// 尝试等待指定的毫秒数[在此时间内是阻塞当前线程的],
/// 如果都还没有获取到就返回“假”,
/// 如果在指定的时间内获取到就返回“真”
/// </summary>
/// <param name="lockObject">锁定的对象</param>
/// <param name="millisecondsTimeout">等待超时的时间(毫秒)</param>
/// <returns>是否获取锁成功</returns>
public static bool TryEnter(T lockObject, int millisecondsTimeout)
{
var obj = LockDictionary.GetOrAdd(lockObject, new object()); //得到或者添加当前Key对象对应的锁定对象
return Monitor.TryEnter(obj, millisecondsTimeout); //锁定当前对象
}
/// <summary>
/// 释放当前资源的锁定,如果资源不是当前线程锁定的就会报错
/// </summary>
/// <param name="lockObject">锁定的对象</param>
public static void Exit(T lockObject)
{
object obj;
if (!LockDictionary.TryGetValue(lockObject, out obj)) return; //得到当前key对应的锁定对象
Monitor.Exit(obj); //解锁锁定对象
}
/// <summary>
/// 判断当前线程是否是否锁定了当前资源,
/// 他配合MonitorLock.Exit(oLock) 使用防止释放的时候出错
/// </summary>
/// <param name="lockObject">锁定的对象</param>
/// <returns>当前线程是否是否锁定了当前资源</returns>
public static bool IsEntered(T lockObject)
{
object obj;
if (!LockDictionary.TryGetValue(lockObject, out obj)) return false; //得到当前key对应的锁定对象
return Monitor.IsEntered(obj); //判断当前线程是否是否锁定了当前资源
}
/// <summary>
/// 生成锁定对象 配合 using(){} 实现 lock(){}效果
/// </summary>
/// <param name="key">锁定的Key</param>
/// <returns>锁定对象</returns>
public static MonitorLock<T> Lock(T key)
{
return new MonitorLock<T>(key);
}
/// <summary>
/// 锁定的Key
/// </summary>
private readonly T _key;
/// <summary>
/// 是否已经释放
/// </summary>
private bool _isDispose;
/// <summary>
/// 实例化锁对象
/// </summary>
/// <param name="key">锁定的键值</param>
public MonitorLock(T key)
{
_key = key;
Enter(key); //锁定
}
/// <summary>
/// 释放锁
/// </summary>
public void Dispose()
{
if (_isDispose) return;
Exit(_key); //解锁
_isDispose = true;
}
}
四、测试代码
1.锁定字符串例子(模拟锁定的值内容相同但是引用地址不相同的情况)
class Program
{
static void Main(string[] args)
{
Test1();
Test2();
Console.ReadKey();
}
private static void Test1()
{
var str1 = "123";//存储在字符串常量区中的字符串
var str3 = 123.ToString();//执行代码生成的字符串
var count = 0;
var t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000000; i++)
{
//将str1字符串作为锁定的值
lock (str1)
{
count++;
}
}
});
var t2 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000000; i++)
{
//将str3字符串作为锁定的值
lock (str3)
{
count++;
}
}
});
Task.WaitAll(t1, t2);
Console.WriteLine($"使用Lock语句,结果{count},正确结果:{2000000},结果正确:{count == 2000000}");
}
private static void Test2()
{
var str1 = "123";//存储在字符串常量区中的字符串
var str3 = 123.ToString();//执行代码生成的字符串
var count = 0;
var t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000000; i++)
{
//将str1字符串作为锁定的值
using (MonitorLock<string>.Lock(str1))
{
count++;
}
}
});
var t2 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000000; i++)
{
//将str3字符串作为锁定的值
using (MonitorLock<string>.Lock(str3))
{
count++;
}
}
});
Task.WaitAll(t1, t2);
Console.WriteLine($"使用MonitorLock<string>.Lock,结果{count},正确结果:{2000000},结果正确:{count == 2000000}");
}
}
输出结果
使用Lock语句,结果1149203,正确结果:2000000,结果正确:False
使用MonitorLock<string>.Lock,结果2000000,正确结果:2000000,结果正确:True
请按任意键继续. . .
MonitorLock<string>.Enter(str);//锁定str
.....
MonitorLock<string>.Exit(str);//解锁str
//等价于
using (MonitorLock<string>.Lock(str))
{
....
}
2.使用值类型作为锁定值
var count = 0;
var t1 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000000; i++)
{
//使用int类型的1024作为锁定值
using (MonitorLock<int>.Lock(1024))
{
count++;
}
}
});
var t2 = Task.Factory.StartNew(() =>
{
for (int i = 0; i < 1000000; i++)
{
//使用int类型的1024作为锁定值
using (MonitorLock<int>.Lock(1024))
{
count++;
}
}
});
Task.WaitAll(t1, t2);
Console.WriteLine($"使用MonitorLock<int>.Lock,结果{count},正确结果:{2000000},结果正确:{count == 2000000}");
结果
使用MonitorLock<int>.Lock,结果2000000,正确结果:2000000,结果正确:True
请按任意键继续. . .