c# 死锁和活锁的发生及避免
作者:精致码农 • 王亮 发布时间:2023-05-28 00:45:49
避免多线程同时读写共享数据
在实际开发中,难免会遇到多线程读写共享数据的需求。比如在某个业务处理时,先获取共享数据(比如是一个计数),再利用共享数据进行某些计算和业务处理,最后把共享数据修改为一个新的值。由于是多个线程同时操作,某个线程取得共享数据后,紧接着共享数据可能又被其它线程修改了,那么这个线程取得的数据就是错误的旧数据。我们来看一个具体代码示例:
static int count { get; set; }
static void Main(string[] args)
{
for (int i = 1; i <= 2; i++)
{
var thread = new Thread(ThreadMethod);
thread.Start(i);
Thread.Sleep(500);
}
}
static void ThreadMethod(object threadNo)
{
while (true)
{
var temp = count;
Console.WriteLine("线程 " + threadNo + " 读取计数");
Thread.Sleep(1000); // 模拟耗时工作
count = temp + 1;
Console.WriteLine("线程 " + threadNo + " 已将计数增加至: " + count);
Thread.Sleep(1000);
}
}
示例中开启了两个独立的线程开始工作并计数,假使当 ThreadMethod
被执行第 4 次的时候(即此刻 count
值应为 4),count
值的变化过程应该是:1、2、3、4,而实际运行时计数的的变化却是:1、1、2、2...。也就是说,除了第一次,后面每次,两个线程读取到的计数都是旧的错误数据,这个错误数据我们把它叫作脏数据。
因此,对共享数据进行读写时,应视其为独占资源,进行排它访问,避免同时读写。在一个线程对其进行读写时,其它线程必须等待。避免同时读写共享数据最简单的方法就是加锁。
修改一下示例,对 count
加锁:
static int count { get; set; }
static readonly object key = new object();
static void Main(string[] args)
{
...
}
static void ThreadMethod(object threadNumber)
{
while (true)
{
lock(key)
{
var temp = count;
...
count = temp + 1;
...
}
Thread.Sleep(1000);
}
}
这样就保证了同时只能有一个线程对共享数据进行读写,避免出现脏数据。
死锁的发生
上面为了解决多线程同时读写共享数据问题,引入了锁。但如果同一个线程需要在一个任务内占用多个独占资源,这又会带来新的问题:死锁。简单来说,当线程在请求独占资源得不到满足而等待时,又不释放已占有资源,就会出现死锁。死锁就是多个线程同时彼此循环等待,都等着另一方释放其占有的资源给自己用,你等我,我待你,你我永远都处在彼此等待的状态,陷入僵局。下面用示例演示死锁是如何发生的:
class Program
{
static void Main(string[] args)
{
var workers = new Workers();
workers.StartThreads();
var output = workers.GetResult();
Console.WriteLine(output);
}
}
class Workers
{
Thread thread1, thread2;
object resourceA = new object();
object resourceB = new object();
string output;
public void StartThreads()
{
thread1 = new Thread(Thread1DoWork);
thread2 = new Thread(Thread2DoWork);
thread1.Start();
thread2.Start();
}
public string GetResult()
{
thread1.Join();
thread2.Join();
return output;
}
public void Thread1DoWork()
{
lock (resourceA)
{
Thread.Sleep(100);
lock (resourceB)
{
output += "T1#";
}
}
}
public void Thread2DoWork()
{
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
}
示例运行后永远没有输出结果,发生了死锁。线程 1 工作时锁定了资源 A,期间需要锁定使用资源 B;但此时资源 B 被线程 2 独占,恰巧资线程 2 此时又在待资源 A 被释放;而资源 A 又被线程 1 占用......,如此,双方陷入了永远的循环等待中。
死锁的避免
针对以上出现死锁的情况,要避免死锁,可以使用 Monitor.TryEnter(obj, timeout)
方法来检查某个对象是否被占用。这个方法尝试获取指定对象的独占权限,如果 timeout
时间内依然不能获得该对象的访问权,则主动“屈服”,调用 Thread.Yield()
方法把该线程已占用的其它资源交还给 CUP,这样其它等待该资源的线程就可以继续执行了。即,线程在请求独占资源得不到满足时,主动作出让步,避免造成死锁。
把上面示例代码的 Workers
类的 Thread1DoWork
方法使用 Monitor.TryEnter
修改一下:
// ...(省略相同代码)
public void Thread1DoWork()
{
bool mustDoWork = true;
while (mustDoWork)
{
lock (resourceA)
{
Thread.Sleep(100);
if (Monitor.TryEnter(resourceB, 0))
{
output += "T1#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
public void Thread2DoWork()
{
lock (resourceB)
{
Thread.Sleep(100);
lock (resourceA)
{
output += "T2#";
}
}
}
再次运行示例,程序正常输出 T2#T1#
并正常结束,解决了死锁问题。
注意,这个解决方法依赖于线程 2 对其所需的独占资源的固执占有和线程 1 愿意“屈服”作出让步,让线程 2 总是优先执行。同时注意,线程 1 在锁定 resourceA
后,由于争夺不到 resourceB
,作出了让步,把已占有的 resourceA
释放掉后,就必须等线程 2 使用完 resourceA
重新锁定 resourceA
再重做工作。
正因为线程 2 总是优先,所以,如果线程 2 占用 resourceA
或 resourceB
的频率非常高(比如外面再嵌套一个类似 while(true) 的循环 ),那么就可能导致线程 1 一直无法获得所需要的资源,这种现象叫线程饥饿,是由高优先级线程吞噬低优先级线程 CPU 执行时间的原因造成的。线程饥饿除了这种的原因,还有可能是线程在等待一个本身也处于永久等待完成的任务。
我们可以继续开个脑洞,上面示例中,如果线程 2 也愿意让步,会出现什么情况呢?
活锁的发生和避免
我们把上面示例改造一下,使线程 2 也愿意让步:
public void Thread1DoWork()
{
bool mustDoWork = true;
Thread.Sleep(100);
while (mustDoWork)
{
lock (resourceA)
{
Console.WriteLine("T1 重做");
Thread.Sleep(1000);
if (Monitor.TryEnter(resourceB, 0))
{
output += "T1#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
public void Thread2DoWork()
{
bool mustDoWork = true;
Thread.Sleep(100);
while (mustDoWork)
{
lock (resourceB)
{
Console.WriteLine("T2 重做");
Thread.Sleep(1100);
if (Monitor.TryEnter(resourceA, 0))
{
output += "T2#";
mustDoWork = false;
Monitor.Exit(resourceB);
}
}
if (mustDoWork) Thread.Yield();
}
}
注意,为了使我要演示的效果更明显,我把两个线程的 Thread.Sleep 时间拉开了一点点。运行后的效果如下:
通过观察运行效果,我们发现线程 1 和线程 2 一直在相互让步,然后不断重新开始。两个线程都无法进入 Monitor.TryEnter
代码块,虽然都在运行,但却没有真正地干活。
我们把这种线程一直处于运行状态但其任务却一直无法进展的现象称为活锁。活锁和死锁的区别在于,处于活锁的线程是运行状态,而处于死锁的线程表现为等待;活锁有可能自行解开,死锁则不能。
要避免活锁,就要合理预估各线程对独占资源的占用时间,并合理安排任务调用时间间隔,要格外小心。现实中,这种业务场景很少见。示例中这种复杂的资源占用逻辑,很容易把人搞蒙,而且极不容易维护。推荐的做法是使用信号量机制代替锁,这是另外一个话题,后面单独写文章讲。
总结
我们应该避免多线程同时读写共享数据,避免的方式,最简单的就是加锁,把共享数据作为独占资源来进行排它使用。
多个线程在一次任务中需要对多个独占资源加锁时,就可能因相互循环等待而出现死锁。要避免死锁,就至少得有一个线程作出让步。即,在发现自己需要的资源得不到满足时,就要主动释放已占有的资源,以让别的线程可以顺利执行完成。
大部分情况安排一个线程让步便可避免死锁,但在复杂业务中可能会有多个线程互相让步的情况造成活锁。为了避免活锁,需要合理安排线程任务调用的时间间隔,而这会使得业务代码变得非常复杂。更好的做法是放弃使用锁,而换成使用信号量机制来实现对资源的独占访问。
作者:精致码农
出处:http://cnblogs.com/willick
来源:https://www.cnblogs.com/willick/p/13967346.html?utm_source=tuicool&utm_medium=referral
猜你喜欢
- Android ListView的Item点击效果的定制
- 前言之前写过一篇关于配置中心对配置内容加密解密的介绍:《Spring Cloud构建微服务架构:分布式配置中心(加密解密) 》。在这篇文章中
- 项目背景我们开发过程中会碰到这样一类问题,就是数据层或三方接口返回的Bean对象需要转换重新装换一下我们需要的对象。我们通常的做法就是通过g
- 十年前,Java 还是计算机科学的入门课程中的必学语言。如果你想学其他语言,比如 C、Python、PHP,你就得专门选那个语言的课程,或者
- 总之是用jdbc 的游标移动package com.sp.person.sql.util; import java.sql.Connecti
- 判断某字符串是否为空,为空的标准是str==null或str.length()==01.下面是StringUtils判断是否为空的示例:St
- 1.线程池Executors的简单使用1)创建一个线程的线程池。 Executors.newSingleThreadExecutor();
- Spring Boot1.为什么要使用 Spring Boot因为Spring, SpringMVC 需要使用的大量的配置文件 (xml文件
- 线程池示例在分析线程池之前,先看一个简单的线程池示例。import java.util.concurrent.Executors;impor
- 本文实例为大家分享了Java实现统计字符串出现次数的具体代码,供大家参考,具体内容如下需求:健盘录入一个字符串,要求统计字符串中每个字符串出
- 在项目开发中,经常碰到map转实体对象或者对象转map的场景,工作中,很多时候我们可能比较喜欢使用第三方jar包的API对他们进行转化,而且
- Spring 封装了 RedisTemplate 来操作 Redis,它支持所有的 Redis 原生的 API。在 Re
- 如下所示:# ===============================================================
- JLabel 对象可以显示文本、图像或同时显示二者。可以通过设置垂直和水平对齐方式,指定标签显示区中标签内容在何处对齐。默认情况下,标签在其
- 前言本文主要介绍其具体的实现思路(视频仅有代码输入,并无过程介绍等),同时,在原本实现的基础上,进行了多处修改和优化,具体参见下面的内容。优
- 在java的开发学习中,可能需要想了解class文件对应的代码内容是什么,如果我们使用IDE工具,可以通过IDE工具的各种反编译插件去进行反
- @RequestBody配合@Valid校验入参参数自定义一个Controllerimport com.example.demo.pojo.
- 不依赖任何外界包,maven如何生成可以执行的jar?pom中不包含任何引用的情况下,只需要在pom中添加 maven-jar-plugin
- 一、添加pom.xml依赖<parent> <groupId>org.springfram
- Java反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动