我来告诉你解决死锁的100种方法

本文介绍了死锁的概念及其四个必要条件,并探讨了如何通过破坏这些条件来避免死锁,如锁排序、一次性获取所有锁、使用CAS等技术。文章还提到在实际编程中的应用和解决方案的局限性,以及在特定场景下的适用性。
摘要由CSDN通过智能技术生成
  • 互斥。资源被竞争性地访问,这里的资源可以理解为锁;

  • 持有并等待。线程持有已经分配给他们的资源,同时等待其他的资源;

  • 不抢占。线程已经获取到的资源不会被其他线程强制抢占;

  • 环路等待。线程之间存在资源的环形依赖链,每个线程都依赖于链条中的下一个线程释放必要的资源,而链条的末尾又依赖了链条头部的线程,进入了一个循环等待的状态。

上面这四个都是死锁出现的必要条件,如果其中任何一个条件不满足都不会出现死锁。虽然这四个条件的定义看起来非常的理论和官方,但是在实际的编程实践中,我们正是在死锁的这四个必要条件基础上构建出解决方案的。所以这里不妨思考一下这四个条件各自的含义,想一想如果去掉其中的一个条件死锁是否还能发生,或者为什么不能发生。

阻止死锁的发生

=======

了解了死锁的概念和四个必要条件之后,我们下面就正式开始解决死锁问题了。对于死锁问题,我们最希望能够达到的当然是完全不发生死锁问题,也就是在死锁发生之前就阻止它。

那么想要阻止死锁的发生,我们自然是要让死锁无法成立,最直接的方法当然是破坏掉死锁出现的必要条件。只要有任何一个必要条件无法成立,那么死锁也就没办法发生了。

破坏环路等待条件


实践中最有效也是最常用的一种死锁阻止技术就是锁排序,通过对加锁的操作进行排序我们就能够破坏环路等待条件。例如当我们需要获取数组中某一个位置对应的锁来修改这个位置上保存的值时,如果需要同时获取多个位置对应的锁,那么我们就可以按位置在数组中的排列先后顺序统一从前往后加锁。

试想一下如果程序中所有需要加锁的代码都按照一个统一的固定顺序加锁,那么我们就可以想象锁被放在了一条不断向前延伸的直线上,而因为加锁的顺序一定是沿着这条线向下走的,所以每条线程都只能向前加锁,而不能再回头获取已经在后面的锁了。这样一来,线程只会向前单向等待锁释放,自然也就无法形成一个环路了。

其实大部分死锁解决方法不止可以用于多线程编程领域,还可以扩展到更多的并发场景下。比如在数据库操作中,如果我们要对某几行数据执行更新操作,那么就会获取这几行数据所对应的锁,我们同样可以通过对数据库更新语句进行排序来阻止在数据库层面发生的死锁。

但是这种方案也存在它的缺点,比如在大型系统当中,不同模块直接解耦和隔离得非常彻底,不同模块的研发同学之间都不清楚具体的实现细节,在这样的情况下就很难做到整个系统层面的全局锁排序了。在这种情况下,我们可以对方案进行扩充,例如Linux在内存映射代码就使用了一种锁分组排序的方式来解决这个问题。锁分组排序首先按模块将锁分为了不同的组,每个组之间定义了严格的加锁顺序,然后再在组内对具体的锁按规则进行排序,这样就保证了全局的加锁顺序一致。在Linux的对应的源码顶部,我们可以看到有非常详尽的注释定义了明确的锁排序规则。

这种解决方案如果规模过大的话即使可以实现也会非常的脆弱,只要有一个加锁操作没有遵守锁排序规则就有可能会引发死锁。不过在像微服务之类解耦比较充分的场景下,只要架构拆分合理,任务模块尽可能小且不会将加锁范围扩大到模块之外,那么锁排序将是一种非常实用和便捷的死锁阻止技术。

破坏持有并等待条件


想要破坏持有并等待条件,我们可以一次性原子性地获取所有需要的锁,比如通过一个专门的全局锁作为加锁令牌控制加锁操作,只有获取了这个锁才能对其他锁执行加锁操作。这样对于一个线程来说就相当于一次性获取到了所有需要的锁,且除非等待加锁令牌否则在获取其他锁的过程中不会发生锁等待。

这样的解决方案虽然简单粗暴,但这种简单粗暴也带来了一些问题:

  • 这种实现会降低系统的并发性,因为所有需要获取锁的线程都要去竞争同一个加锁令牌锁;

  • 并且因为要在程序的一开始就获取所有需要的锁,这就导致了线程持有锁的时间超出了实际需要,很多锁资源被长时间的持有所浪费,而其他线程只能等待之前的线程执行结束后统一释放所有锁;

  • 另一方面,现代程序设计理念要求我们提高程序的封装性,不同模块之间的细节要互相隐藏,这就使得在一个统一的位置一次性获取所有锁变得不再可能。

破坏不抢占条件


如果一个线程已经获取到了一些锁,那么在这个线程释放锁之前这些锁是不会被强制抢占的。但是为了防止死锁的发生,我们可以选择让线程在获取后续的锁失败时主动放弃自己已经持有的锁并在之后重试整个任务,这样其他等待这些锁的线程就可以继续执行了。

同样的,这个方案也会有自己的缺陷:

  • 虽然这种方式可以避免死锁,但是如果几个互相存在竞争的线程不断地放弃、重试、放弃,那么就会导致活锁问题(livelock)。在这种情况下,虽然线程没有因为锁冲突被卡死,但是仍然会被阻塞相当长的时间甚至一直处于重试当中。

  • 这个问题的一种解决方式是给任务重试添加一个随机的延迟时间,这样就能大大降低任务冲突的概率了。在一些接口请求框架中也使用了这种技巧来分散服务高峰期的请求重试操作,防止服务陷入阻塞、崩溃、阻塞的恶性循环。

  • 还是因为程序的封装性,在一个模块中难以释放其他模块中已经获取到的锁。

虽然每一个方案都有自己的缺陷,但是在适合它们的场景下,它们都能发挥出巨大的作用。

破坏互斥条件


在之前的文章中,我们已经了解了一种与锁完全不同的同步方式CAS。通过CAS提供的原子性支持,我们可以实现各种无锁数据结构,不仅避免了互斥锁所带来的开销和复杂性,也由此避开了我们一直在讨论的死锁问题。

AtomicInteger类中就大量使用了CAS操作来实现并发安全,例如incrementAndGet()方法就是用Unsafe类中基于CAS的原子累加方法getAndAddInt来实现的。下面是Unsafe类的getAndAddInt方法实现:

/**

  • 增加指定字段值并返回原值

  • @param obj 目标对象

  • @param valueOffset 目标字段的内存偏移量

  • @param increment 增加值

  • @return 字段原值

*/

public final int getAndAddInt(Object obj, long valueOffset, int increment) {

// 保存字段原值的变量

int oldValue;

do {

// 获取字段原值

oldValue = this.getIntVolatile(obj, valueOffset);

// obj和valueOffset唯一指定了目标字段所对应的内存区域

// while条件中不断调用CAS方法来对目标字段值进行增加,并保证字段的值没有被其他线程修改

// 如果在修改过程中其他线程修改了这个字段的值,那么CAS操作失败,循环语句会重试操作

} while(!this.compareAndSwapInt(obj, valueOffset, oldValue, oldValue + increment));

// 返回字段的原值

return oldValue;

}

上面代码中的compareAndSwapInt方法就是我们说的CAS操作(Compare And Swap),我们可以看到,CAS在每次执行时不一定会成功。如果执行CAS操作时目标字段的值已经被别的线程修改了,那么这次CAS操作就会失败,循环语句将会在CAS操作失败的情况下不断重试同样的操作。这种不断重试的方式就被称为自旋,在jvm当中对互斥锁的等待也会通过少量的自旋操作来进行优化。

不过如果一个变量同时被多个线程以CAS方式修改,那么就有可能导致出现活锁,多个线程将会一直不断重试CAS操作。所以CAS操作的成本和数据竞争的激烈程度密切相关,在一些竞争非常激烈的情况下,CAS操作的成本甚至会超过互斥锁。

除了累加整型值这样的简单场景之外,还有更多更复杂的无锁(lock-free)数据结构,例如java.util.concurrent包中的ConcurrentLinkedDeque双端队列类就是一个无锁的并发安全链表实现,有兴趣的读者可以了解一下。

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

1200页Java架构面试专题及答案

小编整理不易,对这份1200页Java架构面试专题及答案感兴趣劳烦帮忙转发/点赞

百度、字节、美团等大厂常见面试题

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
-2vNPerdD-1713691488281)]

百度、字节、美团等大厂常见面试题

[外链图片转存中…(img-PWTcIK4A-1713691488281)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值