关于常见的锁策略

乐观锁和悲观锁

锁的实现者要预测接下来锁冲突的概率是大还是小, 根据锁冲突概率, 来决定是用乐观锁还是悲观锁.

悲观锁

总是假设最坏的情况,每次去拿数据的时候都认为别人会修改,所以每次在拿数据的时候都会上锁,这样别人想拿这个数据就会阻塞等待, 直到它拿到锁.

乐观锁

假设线程一般情况下不会产生锁冲突,所以在数据进行提交更新的时候,才会正式对数据是否产生锁冲突进行检测,如果发现锁冲突了,则让返回用户错误的信息,让用户决定如何去做.

总结

悲观锁 : 预测接下来发生锁冲突概率比较大.
乐观锁 : 预测接下来发生锁冲突概率比较小.

通常来说, 悲观锁要做的工作要更多一点, 效率也更低一点. 乐观锁做的工作要更少一点, 效率要更高一点.

重量级锁和轻量级锁

重量级锁 : 涉及到大量的内核态用户态切换, 很容易引发线程的调度.
轻量级锁 : 涉及到少量的内核态用户态切换., 不太容易引发线程调度.

总结

重量级锁, 加锁解锁过程更慢, 更低效.
轻量级锁, 加锁解锁过程更快, 更高效.

注 :

一个乐观锁很可能是个轻量级锁.
一个悲观锁很可能是个重量级锁.

自旋锁和挂起等待锁

自旋锁

产生锁冲突时,线程在抢锁失败后会进入阻塞状态,放弃 CPU,需要过很久才能再次被调度.但实际上, 大部分情况下,虽然当前抢锁失败,但过不了很久,锁就会被释放。没必要就放弃 CPU. 这个时候就可以使用自旋锁来处理这样的问题.
伪代码 :

	while (抢锁(lock) == 失败) {}

如果获取锁失败, 会立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来.一旦锁被其他线程释放, 就能第一时间获取到锁.

挂起等待锁

与自旋锁不同, 挂起等待锁获取锁失败后 不会再次尝试获取锁, 而是等系统再次调度到了, 再尝试获取锁.

总结

自旋锁是轻量级锁的一种典型实现, 通常是纯用户态的, 不需要经过内核态.(时间相对更短)
挂起等待锁是重量级锁的一种典型实现, 通过内核的机制来实现挂起等待.(时间更长)

synchronized 的锁策略

synchronized 即是乐观锁, 也是悲观锁, 即是轻量级锁, 也是重量级锁, 轻量级锁部分基于自旋锁实现, 重量级锁部分基于挂起等待锁实现.

synchronized 会根据当前锁竞争的激烈程度, 进行自适应.
如果锁冲突不激烈, 则以轻量级锁 / 乐观锁状态运行.
反之, 以重量级锁 / 悲观锁状态运行.

互斥锁和读写锁

synchronized 只有两个操作 :

  1. 进入代码块, 加锁
  2. 出代码块. 解锁.
    synchronized 的加锁没有更细致的划分, 它是互斥锁.

注意, 只要是涉及到 “互斥”, 就会产生线程的挂起等待. 一旦线程挂起, 再次被唤醒就不知道隔了多久了.
因此尽可能减少 “互斥” 的机会, 就是提高效率的重要途径.

读写锁就是减少"互斥"来提高效率, 他能把读和写两种加锁区分开来.

读写锁:

  1. 给读加锁
  2. 给写加锁
  3. 解锁

读写锁中规定 :

  1. 读锁和读锁之间, 不会产生锁竞争, 不会产生阻塞等待.
  2. 写锁与写锁之间, 会产生锁竞争.
  3. 写锁与读锁之间, 会产生锁竞争.

读写锁是非必要不加锁, 更适合一写多读的情况.

Java 标准库提供了 ReentrantReadWriteLock 类, 实现了读写锁.
ReentrantReadWriteLock.ReadLock 类表示一个读锁, 提供了 lock / unlock 方法进行加锁解锁.
ReentrantReadWriteLock.WriteLock 类表示一个写锁, 提供了 lock / unlock 方法进行加锁解锁.

可重入锁与不可重入锁

在一个线程中, 如果对同一个锁对象连续加锁两次, 且不死锁, 就叫做可重入锁, 如果死锁了, 就叫不可重入锁.

来段伪代码来理解下 :

synchronized(lock) {
	synchronized(lock) {

	}
}

这个代码便是加锁两次, 第二次加锁需要等待第一个锁释放, 第一个锁所释放需要第二次加锁成功 并走完代码块, 逻辑上产生了矛盾,产生了死锁.
实际上上述代码并不会死锁, 因为 synchronized 是可重入锁, 那对于不可重入锁, 上述情况就会产生死锁.

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
C# 中的(lock)用于实现线程同步,确保在多线程环境下对共享资源的访问是安全的。设计的使用策略可以帮助我们避免潜在的并发问题和死。 下面是一些常见使用策略: 1. 定共享资源:当多个线程需要访问同一个共享资源时,使用 lock 关键字将代码块包裹起来,确保同一时间只有一个线程可以进入临界区,避免并发访问问题。 ```csharp lock (sharedResource) { // 访问共享资源的代码 } ``` 2. 最小化的范围:的范围越小,竞争的机会就越小,从而提高并发性能。只在必要的代码段中使用,并在尽可能早的时候释放。 ```csharp lock (sharedResource) { // 只在需要访问共享资源的代码段中使用 // 尽可能早地释放 } ``` 3. 避免嵌套:避免在一个内部再次尝试获取另一个,以防止死的发生。 4. 使用 Monitor 类:除了 lock 关键字外,C# 还提供了 Monitor 类来实现线程同步。Monitor 类提供了更多的功能,比如可以超时等待的释放。 ```csharp Monitor.Enter(sharedResource); try { // 访问共享资源的代码 } finally { Monitor.Exit(sharedResource); } ``` 5. 使用互斥体:另一种实现线程同步的方式是使用 Mutex(互斥体),与相比,互斥体可以跨进程使用。 ```csharp Mutex mutex = new Mutex(); mutex.WaitOne(); // 获取互斥体 try { // 访问共享资源的代码 } finally { mutex.ReleaseMutex(); // 释放互斥体 } ``` 这些是一些常见使用策略,根据具体的场景和需求选择适合的策略可以提高多线程程序的性能和可靠性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

随风的浪

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值