常见锁策略

目录

一 . 悲观锁 与 乐观锁

二 . 读写锁 与 普通互斥锁

三 . 重量级锁 与 轻量级锁

四 .  挂起等待锁 与 自旋锁

五 . 公平锁 与 非公平锁

六 . 可重入锁 与 不可重入锁

七 . 关于死锁


站在锁实现者的角度来思考 : 预测接下来的锁冲突的概率大 , 还是不大 , 根据冲突的概率来决定接下来怎么做.

一 . 悲观锁 与 乐观锁

悲观锁 : 预期锁冲突的概率很高.

乐观锁 : 预期锁冲突的概率很低.

通常来讲 : 乐观锁做的工作更少 , 付出的成本更低 , 更高效

                 悲观锁做的工作更多 , 付出的成本更高 , 更低效

二 . 读写锁 与 普通互斥锁

互斥锁 : 只有加锁和解锁两个操作,进入代码块加锁,出了代码块解锁 (例如 synchronized)

读写锁 : 能够把读和写区分开 , 分为三个操作 : 加读锁 , 加写锁 , 解锁 .

加读锁 : 如果代码只是进行读操作 , 就加读锁 .

加写锁 : 如果代码中进行了修改操作 , 就加写锁.

多个线程同时读同一个变量 , 不会有线程安全问题!

因此在读写锁中约定1. 读锁个读锁之间 , 不会有锁竞争 , 不会产生阻塞等待

                                    2. 写锁和读锁之间 , 有锁竞争.

                                    3. 写锁和写锁之间 , 有锁竞争. 

读写锁适用于读操作多 , 写操作少的情况下 (一写多读的情况)

三 . 重量级锁 与 轻量级锁

重量级锁 : 加锁解锁, 过程更慢 , 更低效 . 就是做了更多的事情 , 开销更大

轻量级锁 : 加锁解锁 , 过程更快 , 更高效 , 做的事情更少, 开销更小.

与悲观锁乐观锁有一定的重叠 , 也可以认为悲观锁一般都是重量级锁 , 乐观锁一般都是轻量级锁.  (但是不绝对)

在使用的锁中 , 如果锁是基于内核的一些功能来实现的(比如调用了操作系统提供的mutex接口),此时一般认为这时重量级锁. 如果锁是纯用户态实现的 , 此时一般认为这时轻量级锁(用户态的代码更可控 , 也更高效)

四 .  挂起等待锁 与 自旋锁

自旋锁是轻量级锁的一种典型实现.

挂起等待锁是重量级锁的一种典型实现.

对于自旋锁来说 : 它是纯用户态的代码 , 不需要经过内核态(时间相对更短) , 往往较轻 , 特点是一旦锁被释放,就能第一时间拿到锁 (速度会更快). (忙等,会消耗cpu的资源)

对于挂起等待锁来说 : 它是通过内核的机制来实现挂起等待(时间更长) , 往往较重.

五 . 公平锁 与 非公平锁

约定 : 遵守先来后到

公平锁 : 多个线程在等待一把锁时 , 谁是先来的, 谁就能先获取到这个锁.

非公平锁 : 多个线程在等待一把锁时 , 每个等待的线程获取到锁的概率都是均等的.

对于操作系统来说 . 本身线程之间的调度就是随机的(机会均等的) . 操作系统提供的mutex这个锁 , 就是属于非公平锁.

六 . 可重入锁 与 不可重入锁

一个线程 , 针对一把锁 , 连续加锁两次 , 出现死锁了 , 就是不可重入锁 , 没有出现死锁 , 就是可重入锁.

对于synchronized 它就是一个可重入锁. 在上述这种场景下 , 是不会死锁的.

(加锁的时候会判定一下,看当前尝试申请锁的线程是不是已经就是锁的拥有者了 , 如果是 , 那么程序继续往下执行)

七 . 关于死锁

1 . 一个线程 , 一把锁(就是上述的情况) , 可重入锁没事 , 不可重入锁会死锁.

2. 两个线程两把锁 , 既是可重入锁 , 也会死锁.

举个栗子 : 

在戴口罩期间 , 西安的一码通(就是类似于健康码)崩溃了.

程序猿 : 大爷, 让我上去修个bug.

保安大爷 : 你需要出示一码通 , 我才能放你进去.

程序猿 : 我进去把一码通的bug修复了 , 我哪能给你出示一码通.

保安大爷 : 不行 , 你先给我看你的一码通.

................................

对于上述这个栗子 , 它就是一个死锁状态.

用代码演示 : 

3. N个线程 , M 把锁

因为线程数量和锁数量更多了 , 就更容易出现死锁了!!!!!

对此 , 就有一个经典的问题 , 哲学家就餐问题.

假设这五个哲学家 , 同时拿起左手边的筷子 , 那就死锁了!

死锁的四个必要条件 :

1. 互斥使用 , 一个线程拿到一把锁之后 , 另一个线程不能使用 (锁的基本特点)

2. 不可抢占 , 一个线程拿到锁 , 只能自己主动的释放 , 不能是被其他线程强行占有 (基本特点)

3. 请求和保持  "吃着碗里的 看着锅里的" , 例如 , 上面的两个线程两把锁 , t1 拿到 locker1 之后 , 紧接着会尝试获取locker2 的锁权限 , 同时它不会释放locker1.

4. 循环等待     t1线程拿不到locker2这把锁 , 它就一直会等待.

如何避免死锁?

一个简单有效的方法 , 破解循环等待这个条件.

针对锁进行编号 , 如果需要同时获取多把锁 , 约定加锁顺序 , 务必是先对小的编号加锁 , 后对大的编号加锁.

 

 总结 : 

综上 , synchronized 既是悲观锁 , 也是乐观锁 , 取决于锁竞争的激烈程度 , 自适应!!

既是轻量级锁 , 也是重量级锁 , 轻量级锁的部分由自旋锁实现 , 重量级锁部分基于挂起等待锁实现.    如果锁冲突不激烈 , 以轻量级锁/乐观锁的状态运行 , 如果锁冲突激烈 , 则以重量级锁/悲观锁的状态运行.

synchronized 不是读写锁.

synchronized 是非公平锁.

synchronized 是可重入锁.

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
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(); // 释放互斥体 } ``` 这些是一些常见使用策略,根据具体的场景和需求选择适合的策略可以提高多线程程序的性能和可靠性。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值