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

PostgreSql死锁问题处理 并发场景下,大批量消费mq消息更新到数据库时,没有对数据进行统一排序导致的死锁问题。通过报错日志定位到了具体的业务代码以及导致死锁的sql。即执行sql发生了如下的情况:事务A更新数据1、2,事务B更新数据2、1,出现了循环等待锁的情况Session_A执行:update table_name set column = xxx WHERE id = 1;Session_B执行:update table_name set column = xxx WHERE id = 2; 阅读详情

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

死锁是多线程编程或者说是并发编程中的一个经典问题,也是我们在实际工作中很可能会碰到的问题。相信大部分读者对“死锁”这个词都是略有耳闻的,但从我对后端开发岗位的面试情况来看很多同学往往对死锁都还没有系统的了解。虽然“死锁”听起来很高深,但是实际上已经被研究得比较透彻,大部分的解决方法都非常成熟和清晰,所以大家完全不用担心这篇文章的难度。

虽然本文是一篇介绍死锁及其解决方式的文章,但是对于多线程程序中的非死锁问题我们也应该有所了解,这样才能写出正确且高效的多线程程序。多线程程序中的非死锁问题主要分为两类:

  • 违反原子性问题

    • 一些语句在底层会被分为多个底层指令运行,所以在多个线程之间这些指令就可能会存在穿插,这样程序的行为就可能会与预期不符造成bug。
  • 违反执行顺序问题

    • 一些程序语句可能会因为子线程立即启动早于父线程中的后续代码,或者是多个线程并发执行等情况,造成程序运行顺序和期望不符导致产生bug。

接下来就让我们开始消灭死锁吧!

初识死锁

什么是死锁?

死锁,顾名思义就是导致线程卡死的锁冲突,例如下面的这种情况:

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

可以看出,上面的两个线程已经互相卡死了,线程t1在等待线程t2释放锁B,而线程t2在等待线程t1释放锁A。两个线程互不相让也就没有一个线程可以继续往下执行了。这种情况下就发生了死锁。

死锁的四个必要条件

上面的情况只是死锁的一个例子,我们可以用更精确的方式描述死锁出现的条件:

  • 互斥。资源被竞争性地访问,这里的资源可以理解为锁;
  • 持有并等待。线程持有已经分配给他们的资源,同时等待其他的资源;
  • 不抢占。线程已经获取到的资源不会被其他线程强制抢占;
  • 环路等待。线程之间存在资源的环形依赖链,每个线程都依赖于链条中的下一个线程释放必要的资源,而链条的末尾又依赖了链条头部的线程,进入了一个循环等待的状态。

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

阻止死锁的发生

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

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

破坏环路等待条件

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

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

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

但是这种方案也存在它的缺点,比如在大型系统当中,不同模块直接解耦和隔离得非常彻底,不同模块的研发同学之间都不清楚具体的实现细节,在这样的情况下就很难做到整个系统层面的全局锁排序了。在这种情况下,我们可以对方案进行扩充,例如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双端队列类就是一个无锁的并发安全链表实现,有兴趣的读者可以了解一下。

这种方法同样可以用在数据库操作上,当我们执行update语句时可以在where子句中添加上一些字段的旧值作为条件,比如update t_xxxx set value = <newValue>, version = version + 1 where id = xxx and version = 10,这样我们就可以通过update语句返回的影响行数是不是0来判断更新操作有没有成功了,这是不是和CAS很相似?

其他解决死锁的方法 —— 探测并恢复

有时,我们并不需要完全阻止死锁的发生,而是可以通过其他的手段来控制死锁的影响。就像如果新的治疗手段可以使×××病人继续活七八十年,那么×××也就没有那么可怕了。

还有一种解决死锁的方法就是让死锁发生,之后再解决它,就像电脑死机以后直接重启一样。使用这种方法我们可以这么做:如果多个线程出现了死锁的情况,那么我们就杀死足够多的线程使系统恢复到可运行状态。在我们常用的关系型数据库中使用的就是这种方法,数据库会周期性地使用探测器创建资源图,然后检查其中是否存在循环。如果探测到了循环(死锁),那么数据库就会根据估算的执行成本高低杀死可以解决死锁问题的尽可能成本最小的线程。

数据库在被外部应用调用的过程中是没办法获知外部应用的逻辑细节的,所以自然也就没办法用之前说的种种方法来解决死锁问题,只能通过事后检测并恢复来对死锁问题做最低限度的保障。但是我们可以在我们的应用程序中应用更多的解决方案,从更上层解决死锁问题。

总结

在这篇文章中,我们从死锁的概念出发,首先介绍了死锁是什么和死锁发生的四个必要条件。然后通过破坏任意一个必要条件产生了四种不同的阻止死锁的解决方案,最后介绍了另外一种死锁解决方法——在死锁发生后再探测并恢复系统运行。相信大家可以在不同的场景中都能找到适合该场景的解决方案,但是锁本质上是容易引入问题的,所以如果不是确有必要,最好不要贸然用锁来进行处理。

【操作系统】死锁问题---死锁的消除方法 解释死锁的消除方法 阅读详情

相关推荐

Java中线程死锁问题的解决方法

锁的粒度控制避免嵌套锁,优先使用细粒度锁(如)替代。锁的持有时间尽量减少锁的持有时间,避免在锁内执行耗时操作(如I/O、网络请求)。锁的释放使用确保锁释放,或通过tryLock()的try-catch处理超时。数据库事务锁事务中按固定顺序获取表锁(如先products后orders),避免交叉锁竞争。监控与日志定期采集线程转储,结合监控平台(如Prometheus+Grafana)检测异常线程状态。Java线程死锁的核心在于打破循环等待条件。

shejizuopin的博客 1211

什么是死锁死锁如何解决

1、死锁是什么? 死锁是指两个或多个事务在同一资源上相互占用,并请求锁定对方的资源,从而导致恶性循环的现象。 当多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进,这种情况就是死锁。 很显然,如果没有外力的作用,那么死锁涉及到的各个进程都将永远处于封锁状态。 2、死锁产生的四个必要条件 (1)互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。 (2)不剥夺条件:进程所获得的

qq_44290077的博客 2万+

【线程安全】死锁问题及解决方案

本期主要讲解: 1.什么是死锁 2.死锁的三个典型情况 2.1—个线程─把锁 2.2两个线程两把锁 2.3多个线程多把锁 3.如何避免死锁 3.1产生死锁的四个必要条件 3.2打破循环等待

程序猿教你打篮球的博客 7742

死锁问题及解决方案

死锁问题出现的经典场景以及避免死锁出现的解决方案

m0_73730252的博客 2297

解决死锁100方法

死锁是多线程编程或者说是并发编程中的一个经典问题,也是我们在实际工作中很可能会碰到的问题。相信大部分读者对“死锁”这个词都是略有耳闻的,但从我对后端开发岗位的面试情况来看很多同学往往对死锁都还没有系统的了解。虽然“死锁”听起来很高深,但是实际上已经被研究得比较透彻,大部分的解决方法都非常成熟和清晰,所以大家完全不用担心这篇文章的难度。 虽然本文是一篇介绍死锁及其解决方式的文章,但是对于多线程程序中...

weixin_34409741的博客 674

Java死锁的原因例子及解决方法

Java发生死锁的根本原因是:在申请锁时发生了交叉闭环申请。即线程在获得了锁A并且没有释放的情况下去申请锁B,这时,另一个线程已经获得了锁B,在释放锁B之前又要先获得锁A,因此闭环发生,陷入死锁循环。 死锁发生的例子1: public class DeadLockA extends Thread { @Override public void run() { t...

川峰的博客 2万+

Java死锁及如何解决死锁

死锁:是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象, 若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁。 1、普通死锁 1.1 死锁代码 下面通过代码演示: public class NormalDeadLock { //定义两个对象锁 private static Object valueFirst...

LINBE_blazers的博客 5766

死锁问题及解决方法

假设你是银行的管理者,你有100元的现金,三个客户都想借钱,每个人都提前告知你他们的最大借款需求。现在,进程A需要资源Y,而进程B需要资源X,然而它们都不愿意释放自己已经占有的资源(互斥条件),并且相互等待对方释放资源(循环等待条件)。例如,A在等待B的瓶子,B在等待C的瓶子,C又在等待A的瓶子,形成一个“死循环”。它的基本思想是,在分配资源时,系统不会直接分配,而是根据每个进程可能的最大资源需求来判断是否会导致死锁。这样,进程在请求新的资源时,必须先释放自己已经占有的资源,避免形成死锁

m0_73895001的博客 1143

死锁以及如何解决死锁

转自https://www.cnblogs.com/aflyun/p/9194104.html,真的很详细。包括我之前总结的cpu100%如何解决也有。 死锁4大要素:互斥,持有并请求,不可剥夺,持续等待 那么如何避免死锁?破坏其中一个条件 说是这么说,代码撸起来。写一个死锁: public class TheBackLock { public static String ob...

大鸡腿的博客 4166

MySQL死锁原因及解决方法

MySQL 死锁(Deadlock)是指两个或多个事务在执行过程中,因争夺资源而陷入相互等待的状态,导致事务无法继续执行的现象。当死锁发生时,MySQL 会自动检测并选择一个事务作为“牺牲者”(通常是较小的事务)进行回滚,另一个事务则继续执行。通过统一资源访问顺序、优化索引、拆分事务等方法,可以有效减少死锁发生。下面通过一个具体的例子说明 MySQL 死锁的产生和解决方法。假设有两个事务(事务A和事务B),操作两张表。所有事务按固定顺序操作表或行。

走过冬季 1630

利用工具JStack排查:死锁问题和CPU100%问题

无论是再面试过程中还是再实际项目开发当中我们都有可能遇到这两个问题。我之前有同学面试这两个问题都有问道过。哈哈哈。所以我绝对把他们了解下并利用博客记录。 JStack可以显示Java应用程序中每个线程的堆栈跟踪,帮助开发人员诊断线程相关的问题,比如死锁和性能问题。通过使用JStack,开发人员可以查看线程的状态、锁定信息以及线程调用堆栈,从而更好地理解应用程序的运行状况。命令介绍:jstack -F [-m] [-l] :这个命令会在Java进程响应时生成线程转储。-m选项会同时显示

qq_64680177的博客 3609

掌握死锁解决技巧与预防方法

在多线程和多进程的并发环境中,死锁是计算机科学领域中经常遇到的一个问题。它发生在系统中的多个进程或线程在执行过程中,因争夺资源而造成的一种僵局。在这个僵局中,相关进程或线程互相等待对方释放资源,从而无法向前推进,导致程序无法完成预期的任务。死锁现象的表现形式多种多样,但其共同点在于系统资源被无效率地占用,导致程序的响应速度大幅下降甚至完全无响应。在一些极端情况下,死锁还可能导致系统崩溃或重启。理解死锁,首先需要认识它的定义和表现现象。

weixin_42466723的博客 977

操作系统学习记录之九:死锁的产生及解决死锁方法

死锁的产生: 允许多个进程并发执行共享系统资源时,系统必须提供同步机制和进程通信机制,然而,对这种机制使用不当的话,可能会出现进程永远被阻塞的现象; 例如,两个进程分别等待对方占有的一个资源,于是两者都不能执行而处于永远等待,这种现象称为“死锁”;   死锁的定义: 一组进程处于死锁状态是指:每一个进程都在等待被另一个进程所占有的、不能抢占的资源。 例如, 存在n个进程P1, P2, …, ...

小小柴的博客 5109
上一篇: 详解Spring的AOP切面编程
下一篇: 面试京东Java架构师,竟然被问这些题,学到了!
大G哥
博客等级 码龄8年 5294粉丝 1408原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值