综述

在遇到线程安全问题的时候,我们会使用加锁机制来确保线程安全,但如果过度地使用加锁,则可能导致锁顺序死锁(Lock-Ordering Deadlock)。或者有的场景我们使用线程池和信号量来限制资源的使用,但这些被限制的行为可能会导致资源死锁(Resource DeadLock)。
我们知道Java应用程序不像数据库服务器,能够检测一组事务中死锁的发生,进而选择一个事务去执行;在Java程序中如果遇到死锁将会是一个非常严重的问题,它轻则导致程序响应时间变长,系统吞吐量变小;重则导致应用中的某一个功能直接失去响应能力无法提供服务,这些后果都是不堪设想的。因此我们应该及时发现和规避这些问题。

死锁产生的条件

死锁的产生有四个必要的条件

  • 互斥使用,即当资源被一个线程占用时,别的线程不能使用
  • 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
  • 请求和保持,当资源请求者在请求其他资源的同时保持对原有资源的占有
  • 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路

对于死锁产生的四个条件只要能破坏其中一条即可让死锁消失,但是条件一是基础,不能被破坏。

各种死锁

锁顺序死锁

死锁 当多个线程同时需要同一个锁,但是以不同的方式获取它们。

例如,如果线程1持有锁A,然后请求锁B,线程2已经持有锁B,然后请求锁A,这样一个死锁就发生了。线程1永远也得不到锁B,线程2永远也得不到锁A。它们永远也不知道这种情况。

public class TreeNode {TreeNode parent   = null;  List     children = new ArrayList();
​public synchronized void addChild(TreeNode child){if(!this.children.contains(child)) {this.children.add(child);child.setParentOnly(this);}}public synchronized void addChildOnly(TreeNode child){if(!this.children.contains(child){this.children.add(child);}}public synchronized void setParent(TreeNode parent){this.parent = parent;parent.addChildOnly(this);}
​public synchronized void setParentOnly(TreeNode parent){this.parent = parent;}
}

如果一个线程(1)调用parent.addChild(child)的同时其他线程(2)在同一个parent和child实例上调用child.setParent(parent)方法,就会发生死锁。 下面是说明这个问题的一些伪代码:

Thread 1: parent.addChild(child); //locks parent--> child.setParentOnly(parent);
​
Thread 2: child.setParent(parent); //locks child--> parent.addChildOnly()

首先,线程1调用parent.addChild(child),因为addChild()是同步的,所以线程1会锁住parent对象,防止其他线程获得。

然后,线程2调用child.setParent(parent),因为setParent()是同步的,所有线程2会锁住child对象,防止其他线程获得。

现在,parent和child对象被这两个不同的线程锁住。接下来,线程1尝试调用child.setParentOnly()方法,但是child对象被线程2锁住了,因此这个调用就会阻塞在那。线程2也尝试调用parent.addChildOnly()方法,但是parent对象被线程1锁住了。线程2也会阻塞在这个方法的调用上。现在两个线程都在等待获取被其他线程持有的锁。

线程确实需要同时获得锁。例如,如果线程1早线程2一点点,获得了锁A和B,然后,线程2就会在尝试获取锁B时,阻塞在那。这样就不会有死锁发生。由于,线程调度是不确定的,所以,我们无法准确预测什么时候会发生死锁。

@Slf4j
public class DeadLock implements Runnable {
    public int flag = 1;
    //静态对象是类的所有对象共享的
    private static Object o1 = new Object(), o2 = new Object();

@Override
    public void run() {
        log.info("flag:{}", flag);
        if (flag == 1) {
            synchronized (o1) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o2) {
                    log.info("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                try {
                    Thread.sleep(500);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                synchronized (o1) {
                    log.info("0");
                }
            }
        }
    }

public static void main(String[] args) {
        DeadLock td1 = new DeadLock();
        DeadLock td2 = new DeadLock();
        td1.flag = 1;
        td2.flag = 0;
        //td1,td2都处于可执行状态,但JVM线程调度先执行哪个线程是不确定的。
        //td2的run()可能在td1的run()之前运行
        new Thread(td1).start();
        new Thread(td2).start();
    }
}

更复杂的死锁

Thread 1  locks A, waits for B
Thread 2  locks B, waits for C
Thread 3  locks C, waits for D
Thread 4  locks D, waits for A

线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4等待线程1.

常见的数据库死锁

一个更复杂的死锁发生场景,就是数据库事务。一个数据库可能包含许多SQL更新请求。在一个事务中,要更新一条记录,但这条记录被来自其它事务的更新请求锁住了,知道第一个事务完成。在数据库中,同一个事务内的每条更新请求可能都会锁住一些记录。

如果多个事务同时运行,并且更新相同的记录。这就会有发生死锁的风险。

例如:

Transaction 1, request 1, locks record 1 for update
Transaction 2, request 1, locks record 2 for update
Transaction 1, request 2, tries to lock record 2 for update.
Transaction 2, request 2, tries to lock record 1 for update.

一个事务事先并不知道所有的它将要锁住的记录,所有在数据库中检测和预防死锁变得更加困难。

重入锁死

重入锁死是一种类似于死锁和嵌套管程失败的情景。

如果一个线程重入获得了一个非重入的锁,读写锁或者一些其他的同步器就会发生重入锁死。重入意味着一个线程已经持有了一个锁可以再次持有它。Java的同步块是可以冲入的。因此,下面这段代码执行将不会出现问题。

public class Reentrant{public synchronized outer(){inner();}
​public synchronized inner(){//do something}
}

outerinner方法都被声明为synchronized,这等同于一个synchronized(this)块。如果一个线程在outer()方法里面调用inner()方法将不会出现问题,因为这两个方法都被同步在同一个管程对象"this"上。如果一个线程已经持有了一个管程对象上的锁,它就可以访问同一个管程对象上所有的同步块。这被称作可重入

下面Lock的实现是不可重入的:

public class Lock{  private boolean isLocked = false;
​public synchronized void lock()throws InterruptedException{while(isLocked){wait();}isLocked = true;}
​public synchronized void unlock(){isLocked = false;notify();}
}

如果一个线程两次调用lock()方法而在两次调用之间没有调用unlock(),第二次调用lock()将会阻塞。一个重入锁死就发生了。

要避免重入锁死你有两种选择:

  • 编写代码避免获取已经持有的锁

  • 使用可重入锁

使用哪种方法更适合于你的程序取决于具体的情景。可重入锁的性能常常不如非重入锁,而且更难实现,可重入锁通常没有不可重入锁那么好的表现,而且实现起来复杂,但这些情况在你的项目中也许算不上什么问题。无论你的项目用锁来实现方便还是不用锁方便,可重入特性都需要根据具体问题具体分析。

如何预防死锁 呢?

锁排序 当多个线程获取同一个锁但是以不同的顺序,就会发生死锁。

如果你确保所有的锁一直以相同的顺序被其他线程获取,死锁就不会发生。看下面这例子:

Thread 1:
​lock A lock B
Thread 2:
​wait for Alock C (when A locked)
Thread 3:
​wait for Await for Bwait for C

如果一个线程,像线程3,需要几个锁,就必须规定其获得锁的顺序。在它获得序列中靠前的锁之前不能够获得靠后的锁。

比如,线程2或者线程3首先要获得锁A,才能够获得锁C。因为,线程A持有锁A,线程2或者线程3首先必须等待直到锁A被释放。然后,在它们能够获得锁B或者C之前,必须成功获得锁A。

锁排序是一个简单但很有效的预防死锁的机制。但是,它仅适用于你事先知道所有的锁的情况下。它并不适用于所有的场景。

锁超时

另一个预防死锁的机制是在请求锁时设置超时时长,也就说一个线程在设置的超时时长内如果没有获得锁就会放弃。如果一个线程在给定时长内没有成功获取所有必要的锁,它将会回退,释放所有的锁请求,随机等待一段时间,然后重试。随机等待的过程中给了其他线程获取这个锁的一个机会,因此,这也可以让程序在没有锁的情况下继续运行。

Thread 1 locks A
Thread 2 locks B
​
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
​
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis) before retrying.
​
Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis) before retrying.

在上面的例子中,线程2将会在线程之前大约200毫秒重试去获得锁,所以,大体上将会获得所有的锁。已经在等待的线程A一直在尝试获取锁A。当线程2完成时,线程1也将会获得所有的锁。

我们需要记住一个问题,上面提到的仅仅是因为一个锁超时了,而不是说线程发生;了死锁,这也仅仅是说这个线程获取这个锁花费了多少时间去完成任务。

另外,如果线程足够多,尽管设置了超时和重试,也是会有发生死锁的风险。2个线程各自在重试前等待0~500毫秒也许不会发生死锁,但如果10或者20个线程情况就不同了。这种情况发生死锁的概率要比两个线程的情况要高得多。

锁超时机制存在的一个问题是在Java中在进入一个同步代码块时设置时长是不可能的。你不得不创建一个自定义的锁相关的类或者使用在Java5中java.util.concurrency包中的并发结构之一。

死锁检测

死锁检测是一个重量级的死锁预防机制,主要用于在锁排序和锁超时都不可用的场景中。

当一个线程请求一个锁当时请求被禁止时,这个线程可以遍历锁图(lock graph)检查是否发生了死锁。例如,如果一个线程A请求锁7,但是锁7被线程B持有,然后,线程A可以检测线程B是否有请求任何线程A持有的锁。如果有,就会发生一个死锁。

当然,一个死锁场景可能比两个对象分别持有对方的锁要复杂的锁。线程A可能等待线程B,线程B等待线程C,线程C等待线程D,线程D等待线程A。为了检测死锁,线程A必须一次测试所有的被线程B请求的锁。从线程B的锁请求线程A到达线程C,然后又到达线程D,从上面的检测中,线程A找到线程A自身持有的一个锁。这样,线程A就会知道发生了死锁。

下面是一个被四个线程持有和请求锁的图。类似于这样的一个数据结构可以用来检测死锁。

那么,如果检测到一个死锁,这些线程可以做些什么?

一个可能的做法就是释放所有的锁,回退,随机等待一段时间然后重试。这种做法与锁超时机制非常相似除了只有发生死锁时线程才会回退(backup)。而不仅仅是因为锁请求超时。然而,如果大量的线程去请求同一个锁,可能重复的发生死锁,尽管存在回退和等待机制。

一个更好的做法就是为这些线程设置优先级,这样一来,就会只有一个或者一些线程在遇到死锁时发生回退。剩下的线程继续请求锁假如没有死锁再发生。如果赋予线程的优先级是固定的,同样的线程总是拥有更高的优先级。为了避免这种情况,我们可以在发生死锁时,随机的为线程设置优先级。

多线程之死锁介绍及预防相关推荐

  1. Java多线程之死锁编码及定位分析

    Java多线程之死锁编码及定位分析 目录 死锁是什么 代码实现 死锁解决办法 1. 死锁是什么 死锁是指两个或两个以上的进程在执行过程中因争夺资而造成的一种互相等待的现象,若无外力干涉那它们都将无法推 ...

  2. JAVA的多线程、死锁、线程间通信、如何规避死锁、线程安全的单例模式

    主要内容: 多线程 线程和进程间的关系 Java中的线程理论 Java中线程类的实现方式 Java中线程的常用方法 线程安全性问题 线程间通信 线程的死锁 如何规避死锁 线程安全的单例模式 多线程 线 ...

  3. 多线程产生死锁的四个必要条件

    多线程产生死锁的四个必要条件 1.互斥条件:任意时刻一个资源只能给一个进程使用,其他进程若申请一个资源,而该资源被另一进程占有时,则申请 者等待直到资源被占有者释放. 2.不可剥夺条件:进程所获得的资 ...

  4. Python 多线程中死锁了怎么办?

    一.死锁 在线程间共享多个资源的时候,如果两个线程分别占有一部分资源并且同时等待对方的资源,就会造成死锁. 就好像在现实社会中,男女双方在闹别扭之后,都在等待对方先道歉. 如果双方都这样固执地等待对方 ...

  5. iOS多线程各种安全锁介绍 - 线程同步

    一.atomic介绍 github对应Demo:https://github.com/Master-fd/LockDemo 在iOS中,@property 新增属性时,可以增加atomic选项,ato ...

  6. Java多线程探究-死锁原因

    进程死锁及解决办法 一.要点提示 (1) 掌握死锁的概念和产生死锁的根本原因. (2) 理解产生死锁的必要条件--以下四个条件同时具备:互斥条件.不可抢占条件.占有且申请条件.循环等待条件. (3)  ...

  7. delphi多线程TThread类介绍

    Delphi中有一个线程类TThread是用来实现多线程编程的,这个绝大多数Delphi书藉都有说到,但基本上都是对TThread类的几个成员作一简单介绍,再说明一下Execute的实现和Synchr ...

  8. 调试多线程 查死锁的bug gcore命令 gdb对多线程的调试 gcore pstack 调试常用命令...

    gdb thread apply all bt 如果你发现有那么几个栈停在 pthread_wait 或者类似调用上,大致就可以得出结论:就是它们几个儿女情长,耽误了整个进程. 注意gdb的版本要高于 ...

  9. mysql死锁介绍以及解决

    什么是死锁 死锁是2+个线程在执行过程中, 因争夺资源而造成的相互等待的现象,若无外力作用,它们将无法推进下去. 死锁产生的4个必要条件 互斥条件 指进程对所分配的资源进行排他性使用,即一段时间内某资 ...

最新文章

  1. cocos2d-x温故(三)!
  2. Hsiaoyang: Google与站点地图Sitemap
  3. C语言quick sort快速排序的算法(附完整源码)
  4. 第七十九期:阿里程序员感慨:码农们过去暴富有多轻松,现在赚钱就有多辛苦
  5. 解决python2.7.9以下版本requests访问https的问题
  6. apache通过AD验证
  7. 如何在苹果Mac上删除APFS卷?
  8. 《四 spring源码》spring的事务注解@Transactional 原理分析
  9. layim mysql_ichat系统说明 · ThinkPHP5+workerman+layIM打造聊天系统 · 看云
  10. mac及idea常用快捷键
  11. scheme 中文教程
  12. IDM安装使用 解决下载限速
  13. 日系插画学习笔记(二):结构与透视
  14. C# dataGridView中插入excel表格
  15. keyshot渲染图文教程_KeyShot中渲染汽车教程
  16. java注释【单行注释,多行注释,文档注释】
  17. cocos2d-x 艺术字
  18. matlab 图像 局部极值,[转载]matlab 图像局部求极值
  19. 汇编语言相关指令介绍(一)
  20. 分布式数据访问层(DAL)

热门文章

  1. 前端切图要选择png和jpg呢?
  2. html百度地图获取城镇街道,百度地图定位得到当前位置(省、市、区县、街道、门派号码)...
  3. 一张图告诉你,如何攻击Java Web应用
  4. OpenStack私有云安配置
  5. 健身俱乐部管理系统的设计与实现
  6. matlab数组做运算,6.2 MATLAB数组的运算
  7. 听云重磅发布 [2014中国移动应用性能管理白皮书]
  8. 企业办公入门之选 用ThinkCentre E95更划算!
  9. 数据库连接池设置多大才合适?
  10. java神秘岛_我的世界1.4.2喵喵苍炎冒险整合包1.1含服务端(含IC,BC,暮色,神秘岛,,PAM世界等MOD)...