多线程死锁

什么是死锁

  • 死锁:不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁
  • 死锁现象:出现死锁后,不会出现异常,不会出现提示,只是所有的线程都处于阻塞状态,无法继续执行
  • 生活中例子:一个房门上面有两个锁,钥匙分别在A、B两个人手中,想要进入房间必须将两把锁都打开,但是A和B都不愿意交出自己手中的钥匙,两个人僵持不下,此时就可以看成是死锁

同步机制中的锁

synchronized的锁是什么

  • 任意对象都可以作为同步锁。所有对象都自动含有单一的锁(监视器)
  • 同步方法的锁:静态方法(类名.class)、非静态方法(this)
  • 同步代码块:自己指定,很多时候也是指定为this或类名.class’
  • 注意:
  1. 必须确保使用同一个资源的多个线程共用一把锁,这个非常重要,否则就无法保证共享资源的安全
  2. 一个线程类中的所有静态方法共用同一把锁(类名.class),所有非静态方法共用同一把锁(this),同步代码块(指定需谨慎)

释放锁的操作

  • 当前线程的同步方法、同步代码块执行结束
  • 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行
  • 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致异常结束
  • 当前线程在同步代码块、同步方法中执行了线程对象的wait()方法,当前线程暂停,并释放锁

不会释放锁的操作

  • 线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行
  • 线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该线程挂起,该线程不会释放锁(同步监视器),应尽量避免使用suspend()和resume()来控制线程

死锁产生的原因

系统资源的竞争

系统中拥有某个不可剥夺的资源,但是数量不足以满足多个线程运行的需要,使得线程在运行过程中,会因争夺资源而陷入僵局,如打印机。只有对不可剥夺资源的竞争才可能产生死锁,可剥夺资源的竞争是不会引起死锁的

非法的进程推进顺序

线程程在运行过程中,请求和释放资源的顺序不当时,也会导致死锁。比如线程A等待线程B释放线程A需要的锁,线程B又在等待线程A释放线程B需要的锁,可以看出线程A和B不是因为竞争同一资源,而是在等待对方的资源导致死锁

死锁产生的必要条件

互斥条件

  • 线程要求对所分配的资源进行排他性控制,即在一段时间内某 资源仅为一个线程所占有。此时若有其他线程请求该资源,那么请求线程只能等待

不剥夺条件

  • 线程所获得的资源在未使用完毕之前,不能被其他线程强行夺走,只能由获得该资源的线程自己释放

请求和保持条件

  • 线程已经持有一个资源,但又提出了新的资源请求,而该资源已被其他线程占有,此时请求线程被阻塞,但对自己已获得的资源保持不放

循环等待条件

  • 存在一种线程资源的循环等待链,链中每一个线程已获得的资源同时被链中下一个进程所请求。即存在一个处于等待状态的线程集合{Pl, P2, …, pn},其中Pi等待的资源被P(n+1)占有(i=0, 1, …, n-1),Pn等待的资源被P0占有
    在这里插入图片描述
  • 直观上看,循环等待条件似乎和死锁定义一样,其实不然:按死锁定义构成等待环要求的条件更严,它要求Pi等待的资源必须由P(i+1)来满足,而循环等待条件则无此限制.例如:系统中有两台输出设备,P0占有一台,PK占有另一台,且K不属于集合{0, 1, …,
    n}。Pn等待一台输出设备,它可以从P0获得,也可能从PK获得。因此,虽然Pn、P0和其他
    一些进程形成了循环等待圈,但PK不在圈内,若PK释放了输出设备,则可打破循环等待,因此循环等待只是死锁的必要条件

总结死锁发生时的条件

  1. 互斥条件:一个资源每次只能被一个线程使用;如独木桥每次只能通过一个人
  2. 不剥夺条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放;如乙不退出桥面,甲也不退出桥面
  3. 请求和保持条件:线程已获得的资源,在未使用完之前,不能强行剥夺;如甲不能强制乙退出桥面,乙也不能强制甲退出桥面
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系;如果乙不退出桥面,甲不能通过,甲不退出桥面,乙不能通过

死锁例子1

package com.star.test;

/**
 * threadA先运行,这个时候flag==true,先锁定OBJA,然后睡眠1秒钟
 * 而threadA在睡眠的时候,另一个线程threadB启动,flag==false,先锁定OBJB,然后也睡眠1秒钟
 * threadA睡眠结束后需要锁定OBJB才能继续执行,而此时OBJB已被threadB锁定
 * threadB睡眠结束后需要锁定OBJA才能继续执行,而此时OBJA已被threadA锁定
 * threadA、threadB相互等待,都需要得到对方锁定的资源才能继续执行,从而死锁。
 */
class DeadLock implements Runnable {

    private boolean flag;
    private static final Object OBJ1 = new Object();
    private static final Object OBJ2 = new Object();

    public DeadLock(boolean flag) {
        this.flag = flag;
    }

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "运行");
        if (flag) {
            synchronized (OBJ1) {
                System.out.println(Thread.currentThread().getName() + "已经锁住OBJA");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (OBJ2) {
                    // 执行不到这里
                    System.out.println("1秒钟后," + Thread.currentThread().getName() + "锁住OBJB");
                }
            }
        } else {
            synchronized (OBJ2) {
                System.out.println(Thread.currentThread().getName() + "已经锁住OBJB");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (OBJ1) {
                    // 执行不到这里
                    System.out.println("1秒钟后," + Thread.currentThread().getName() + "锁住OBJA");
                }
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        Thread threadA = new Thread(new DeadLock(true), "线程A");
        Thread threadB = new Thread(new DeadLock(false), "线程B");
        threadA.start();
        threadB.start();
    }
}

运行结果:
在这里插入图片描述

  • 线程A锁住了OBJA(甲占有桥的一部分资源),线程B锁住了OBJB(乙占有桥的一部分资源),线程A企图锁住OBJB(甲让乙退出桥面,乙不从),进入阻塞,线程B企图锁住OBJA(乙让甲退出桥面,甲不从),进入阻塞,此时发生死锁
  • 从这个例子可以看出:死锁是因为多线程访问共享资源,由于访问的顺序不当所造成的,通常是一个线程锁定了一个资源A,而又想去锁定资源B;在另一个线程中,锁定了资源B,而又想去锁定资源A时,两个线程都想得到对方的资源,而不愿释放自己的资源,造成两个线程都在等待,从而发生死锁

死锁例子2

package com.star.test;

/**
 * 线程A先获取第一个对象锁objLock1,但是当线程A请求第二个对象锁objLock2之前,线程A就会进入睡眠状态,
 *     此时线程B运行,获取对象锁objLock2,当线程B请求第二个对象锁objLock3之前,线程B就会进入睡眠状态,
 *     此时线程C运行,获取对象锁objLock3,当线程B请求第二个对象锁objLock1之前,线程C就会进入睡眠状态,
 *     此时线程A睡眠时间结束时,因为第二个对象锁已经被线程B锁住了,线程A等待线程B释放objLock2,
 *     此时线程B睡眠时间结束时,因为第二个对象锁已经被线程C锁住了,线程B等待线程C释放objLock3,
 *     此时线程C睡眠时间结束时,因为第二个对象锁已经被线程A锁住了,线程C等待线程A释放objLock1,
 *     从而导致了死锁。在线程引起死锁的过程中,就形成了一个依赖于资源的循环
 */
class MyThread implements Runnable {
    private final Object lock1;
    private final Object lock2;
    private final Object lock3;

    MyThread(Object objLock1, Object objLock2, Object objLock3) {
        this.lock1 = objLock1;
        this.lock2 = objLock2;
        this.lock3 = objLock3;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        synchronized (lock1) {
            System.out.println(name + " 获得锁 " + lock1);
            work();
            synchronized (lock2) {
                System.out.println(name + " 获得锁 " + lock2);
                work();
                synchronized (lock3) {
                    System.out.println(name + " 获得锁 " + lock3);
                    work();
                }
                System.out.println(name + " 释放锁 " + lock3);
            }
            System.out.println(name + " 释放锁 " + lock2);
        }
        System.out.println(name + " 释放锁 " + lock1);
        System.out.println(name + " 完成执行");
    }

    private void work() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Object objLock1 = new Object();
        Object objLock2 = new Object();
        Object objLock3 = new Object();

        //A---B---C
        Thread threadA = new Thread(new MyThread(objLock1, objLock2, objLock3), "线程A");
        //B---C---A
        Thread threadB = new Thread(new MyThread(objLock2, objLock3, objLock1), "线程B");
        //C---A---B
        Thread threadC = new Thread(new MyThread(objLock3, objLock1, objLock2), "线程C");

        threadA.start();
        Thread.sleep(1000);
        threadB.start();
        Thread.sleep(1000);
        threadC.start();
    }
}

运行结果:
在这里插入图片描述
这个例子中,形成了一个锁依赖的环路:

  • 线程A先获取第一个对象锁objLock1,但是当线程A请求第二个对象锁objLock2之前,线程A就会进入睡眠状态
  • 此时线程B运行,获取对象锁objLock2,当线程B请求第二个对象锁objLock3之前,线程B就会进入睡眠状态
  • 此时线程C运行,获取对象锁objLock3,当线程B请求第二个对象锁objLock1之前,线程C就会进入睡眠状态
  • 此时线程A睡眠时间结束时,因为第二个对象锁已经被线程B锁住了,线程A等待线程B释放objLock2
  • 此时线程B睡眠时间结束时,因为第二个对象锁已经被线程C锁住了,线程B等待线程C释放objLock3
  • 此时线程C睡眠时间结束时,因为第二个对象锁已经被线程A锁住了,线程C等待线程A释放objLock1,从而导致了死锁
  • 在线程引起死锁的过程中,就形成了一个依赖于资源的循环

如何避免死锁

加锁顺序(线程按照一定的顺序加锁)

  • 当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生

改造例子1

Thread threadB = new Thread(new DeadLock(false), "线程B");

改为

Thread threadB = new Thread(new DeadLock(true), "线程B");

运行结果:
在这里插入图片描述

  • 因为线程A和线程B都是先对OBJA加锁,然后再对OBJ2加锁,当threadA启动后,锁住了OBJA,而threadB也启动后,只有当threadA释放了OBJA后threadB才会执行,从而有效的避免了死锁

改造例子2

//A---B---C
Thread threadA = new Thread(new MyThread(objLock1, objLock2, objLock3), "线程A");
//B---C---A
Thread threadB = new Thread(new MyThread(objLock2, objLock3, objLock1), "线程B");
//C---A---B
Thread threadC = new Thread(new MyThread(objLock3, objLock1, objLock2), "线程C");

改为

//A---B---C
Thread threadA = new Thread(new MyThread(objLock1, objLock2, objLock3), "线程A");
//B---C---A
Thread threadB = new Thread(new MyThread(objLock1, objLock2, objLock3), "线程B");
//C---A---B
Thread threadC = new Thread(new MyThread(objLock1, objLock2, objLock3), "线程C");

运行结果:
在这里插入图片描述

避免嵌套加锁

  • 嵌套加锁是死锁最主要的原因,假如已经有一个资源时,就要避免封锁另一个资源。如果运行时只有一个对象加锁,那几乎不可能出现死锁

改造例子1

@Override
public void run() {
    System.out.println(Thread.currentThread().getName() + "运行");
    if (flag) {
        synchronized (OBJ1) {
            System.out.println(Thread.currentThread().getName() + "已经锁住OBJA");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (OBJ2) {
                // 执行不到这里
                System.out.println("1秒钟后," + Thread.currentThread().getName() + "锁住OBJB");
            }
        }
    } else {
        synchronized (OBJ2) {
            System.out.println(Thread.currentThread().getName() + "已经锁住OBJB");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (OBJ1) {
                // 执行不到这里
                System.out.println("1秒钟后," + Thread.currentThread().getName() + "锁住OBJA");
            }
        }
    }
}

改为

@Override
public void run() {
    System.out.println(Thread.currentThread().getName() + "运行");
    if (flag) {
        synchronized (OBJ1) {
            System.out.println(Thread.currentThread().getName() + "已经锁住OBJA");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (OBJ2) {
            // 执行不到这里
            System.out.println("1秒钟后," + Thread.currentThread().getName() + "锁住OBJB");
        }
    } else {
        synchronized (OBJ2) {
            System.out.println(Thread.currentThread().getName() + "已经锁住OBJB");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        synchronized (OBJ1) {
            // 执行不到这里
            System.out.println("1秒钟后," + Thread.currentThread().getName() + "锁住OBJA");
        }
    }
}

运行结果:
在这里插入图片描述

改造例子2

@Override
public void run() {
    String name = Thread.currentThread().getName();
    synchronized (lock1) {
        System.out.println(name + " 获得锁 " + lock1);
        work();

    }
    System.out.println(name + " 释放锁 " + lock1);
    synchronized (lock2) {
        System.out.println(name + " 获得锁 " + lock2);
        work();

    }
    System.out.println(name + " 释放锁 " + lock2);
    synchronized (lock3) {
        System.out.println(name + " 获得锁 " + lock3);
        work();
    }
    System.out.println(name + " 释放锁 " + lock3);
    System.out.println(name + " 完成执行");
}

改为

@Override
public void run() {
    String name = Thread.currentThread().getName();
    synchronized (lock1) {
        System.out.println(name + " 获得锁 " + lock1);
        work();

    }
    System.out.println(name + " 释放锁 " + lock1);
    synchronized (lock2) {
        System.out.println(name + " 获得锁 " + lock2);
        work();

    }
    System.out.println(name + " 释放锁 " + lock2);
    synchronized (lock3) {
        System.out.println(name + " 获得锁 " + lock3);
        work();
    }
    System.out.println(name + " 释放锁 " + lock3);
    System.out.println(name + " 完成执行");
}

运行结果:
在这里插入图片描述

加锁时限

  • 在尝试获取锁的时候加一个超时时间,在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。
    如图:在这里插入图片描述
  • 线程1获得lockA,线程2获得lockB,此时线程B试图获得lockB但被阻塞,线程2试图获得lockA但被阻塞,此时发生死锁。线程1对lockB尝试超时,备份并释放lockB,并在重试之前随机等待(例如200毫秒)。线程2对lockA尝试超时,也备份并释放lockB,并在重试之前随机等待(例如50毫秒)。线程2比线程1早150毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)
  • 由于存在锁的超时,所以不能认为这种场景一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任务。另外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不一定了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。(超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试,导致新一轮的竞争,带来了新的问题)
  • 这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。所以需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具

死锁检测

  • 死锁检测是一个更好的死锁预防机制,主要是针对不可能实现按序加锁并且锁超时也不可行的场景
  • 每当一个线程获得了锁,会在线程和锁相关的数据结构中(如map)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图看看是否有死锁发生。比如如,线程1请求锁D,但是锁D这个时候被线程2持有,这时线程1就可以检查一下线程2是否已经请求了线程1当前所持有的锁。如果线程2确实有这样的请求,那么就是发生了死锁(线程1拥有锁C,请求锁D;线程2拥有锁D,请求锁C)
  • 死锁一般要比两个线程互相持有对方的锁要复杂的多。线程1等待线程2,线程2等待线程3,线程3等待线程4,线程4又在等待线程5。此时,线程1为了检测死锁,它需要递进地检测所有被线程2请求的锁。从线程2所请求的锁开始,线程1找到了线程3,然后又找到了线程4,接着又找到线程5,发现线程5请求的锁被线程1自己持有,这时就知道发生了死锁,如图:
    在这里插入图片描述
  • 当出现这种环形锁时,我们应该释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(不能从根本上减轻竞争)。
  • 一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为了避免这个问题,可以在死锁发生的时候设置随机的优先级。
  • 可以使用ReentrantLock.tryLock()方法,在一个循环中,如果tryLock()返回失败,那么就释放持有的锁,并睡眠一小段时间,这样就打破了死锁的闭环。比如:线程1持有锁A并且申请获得锁B,而线程2持有锁B并且申请获得锁C,而线程3持有锁C并且申请获得锁A。此时如果线程3申请锁A失败,那么线程3释放锁C,并进行睡眠。那么线程2就可以获得锁C,然后线程2执行完之后释放锁B、C,所以线程1也可以获得锁B,执行完然后释放锁A、B,然后线程3睡眠醒来,也可以获得锁C、A。此时就打破了死锁的闭环

总结避免死锁的方式

  1. 让程序每次只能获得一个锁,但是在多线程大部分环境下并不现实
  2. 考虑清楚锁的顺序,尽量减少嵌在的加锁交互数量
  3. 死锁的产生是两个线程无限等待对方持有的锁,只要等待时间有个上限就不会出现死锁了。但是synchronized不具备这个功能,可以使用Lock类中的tryLock方法去尝试获取锁,此方法可以指定一个超时时限,在等待超过该时限之后便会返回失败信息
  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Golden-Star

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值