锁,共享锁,排他锁,死锁介绍
阅读 4:55·字数 1475·发布
锁
锁是一种并行状态下确保业务逻辑正常运转的机制,其出现的根本原因是并行带来的执行顺序的不确定性,当同一个资源被施加了顺序不确定的代码逻辑后,出现错误将是可以预期的。在不同的系统或语言环境中,锁的规则和具体实现会有所不同,这主要取决于需要对资源进行何种形式和程度的访问控制。
虽然规则和具体实现不会完全一致,但锁的使用流程是基本相同的,在某些操作执行之前,必须使用一种令牌请求相关锁,只有成功获取,操作才能继续,当操作完成时,持有的锁将被释放。
锁和多线程之间的关系
如上所述,锁之所以被使用是由于对共享资源的并行访问,因此锁和多线程并没有必然的联系,虽然他是锁常见的应用场景。当你以某种方式实现了单线程中某一资源的并行访问时,锁也可以被用在单线程中。
排他锁
排他锁只允许被一个目标持有,是拒绝授予共享锁或另一个排他锁的依据,当某个令牌被用于排他锁后,不能再使用该令牌申请共享锁或另一个排他锁。
由于排他锁仅允许单独持有,因此在其对应的业务逻辑中执行写入操作是安全的,当然,前提是同一资源的所有写入操作均运用了锁机制。
在下面的 C# 代码中,我们将变量locker
作为令牌,并使用排他锁限制变量balance
的访问,当新的线程通过函数Pay
修改balance
时,主线程调用的函数GetBalance
将陷入等待。
// 余额,初始值为 1000
int balance = 1000;
// 用来请求锁的令牌
object locker = new();
// 函数 Pay,进行支付操作
void Pay(int amount)
{
// 请求排他锁,确保只有一个目标正在访问 balance
lock (locker)
{
balance -= amount;
// 修改余额后等待 5 秒
Thread.Sleep(5000);
}
}
// 获取当前的余额
int GetBalance()
{
// 请求排他锁,确保只有一个目标正在访问 balance
lock (locker)
return balance;
}
Console.WriteLine("开启一个线程,支付 100");
Thread thread = new(() => Pay(100));
thread.Start();
Console.WriteLine("现在我要查看余额!");
// 调用 GetBalance 将陷入等待,因为此时 Pay 方法持有锁
Console.WriteLine($"余额:{GetBalance()}");
开启一个线程,支付 100
现在我要查看余额!
约 5 秒后…
余额:900
死锁
所谓的死锁,是指多个目标都在等待对方先释放持有的锁,导致他们均无法继续执行。在代码逻辑可以任意书写的情况下,死锁总是有可能发生,除非设置一些特殊的规则,比如,目标同一时间只能持有一个锁,在请求新的锁之前,必须释放旧的锁。当然,如果真的按照这种规则来书写代码,那么某些业务需求将无法被满足。
在下面的 C# 代码中,两个线程在持有自己的排他锁后,开始请求对方持有的锁,这导致了死锁的发生。
// 用来请求锁的令牌
object lockerA = new();
object lockerB = new();
// 线程 I 先后使用令牌 lockerA,lockerB 获取排他锁
new Thread(() =>
{
lock (lockerA)
{
Console.WriteLine("线程 I 已经获取锁 A");
Thread.Sleep(2000);
Console.WriteLine("线程 I 尝试获取锁 B");
lock (lockerB)
Console.WriteLine("线程 I 已经获取锁 B");
}
}).Start();
// 线程 II 先后使用令牌 lockerB,lockerA 获取排他锁
new Thread(() =>
{
lock (lockerB)
{
Console.WriteLine("线程 II 已经获取锁 B");
Thread.Sleep(2000);
Console.WriteLine("线程 II 尝试获取锁 A");
lock (lockerA)
Console.WriteLine("线程 II 已经获取锁 A");
}
}).Start();
线程 I 已经获取锁 A
线程 II 已经获取锁 B
约 2 秒后…
线程 I 尝试获取锁 B
线程 II 尝试获取锁 A