首页 > 编程笔记 > Java笔记 阅读:15

Java中的线程死锁问题(附带实例)

所谓死锁,是指多个线程因竞争资源而造成的一种僵局(互相等待),如果无外力作用,那么这些进程都将无法向前推进。

线程死锁的示意图如下图所示:

图 1 线程死锁的示意图

产生死锁的原因

死锁主要是由以下 4 个因素造成的:
  1. 互斥条件:是指线程对已经获取到的资源进行排他性使用,即该资源同时只由一个线程占用。如果此时还有其他线程请求获取该资源,那么请求者只能等待,直到占用资源的线程释放该资源。
  2. 不可被剥夺条件:是指线程获取到的资源在自己使用完之前不能被其他线程占用,只有在自己使用完毕才由自己释放该资源。
  3. 请求并持有条件:是指一个线程已经占用了至少一个资源,但又提出了新的资源请求,而新的资源已被其他线程占用,所以当前线程会被阻塞,但阻塞的同时并不释放自己已经获取的资源。
  4. 环路等待条件:是指在发生死锁时,必然存在一个(线程—资源)环形链,即线程集合 {T0,T1,T2,…,Tn} 中的 T0 正在等待 T1 占用的资源,T1 正在等待 T2 占用的资源,依次类推,Tn 正在等待 T0 占用的资源。

环路等待的示意图如下图所示:

图 2 环路等待的示意图

当上述 4 个条件都成立时,便形成死锁。当然,在发生死锁的情况下,如果打破上述任何一个条件,就可以让死锁消失。

【实例】一个简单的产生死锁的应用。
public class Example {
    public static void main(String[] args) {
        DeadDemo td1 = new DeadDemo();
        DeadDemo td2 = new DeadDemo();
        td1.flag = 1;
        td2.flag = 0;
        new Thread(td1, "td1").start();
        new Thread(td2, "td2").start();
    }
}

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

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + ": flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                System.out.println(threadName + ": 取得 o1 锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName + ": 申请 o2 锁");
                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                System.out.println(threadName + ": 取得 o2 锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName + ": 申请 o1 锁");
                synchronized (o1) {
                    System.out.println("0");
                }
            }
        }
    }
}
运行结果为:

td2: flag=0
td1: flag=1
td2: 取得o2锁
td1: 取得o1锁
td1: 申请o2锁
td2: 申请o1锁

当 DeadDemo 类的对象(td1)flag==1 时,先锁定 o1 锁,睡眠 500 毫秒。而 td1 在睡眠的时候另一个对象(td2)flag==0 的线程启动,先锁定 o2 锁,睡眠 500 毫秒:

解决死锁的方法

要想解决死锁,只需要破坏至少一个产生死锁的必要条件即可。

在操作系统中,互斥条件和不可剥夺条件是操作系统规定的,没有办法人为更改,并且这两个条件明显是一个标准的程序应该具备的特性。

所以,目前只有请求并持有条件和环路等待条件是可以被破坏的。

产生死锁的原因其实和申请资源的顺序有很大的关系。使用资源申请的有序性原则就可以避免产生死锁。因此,解决死锁的方法有两种:
【实例】解决上面实例中产生的死锁。
public class Example {
    public static void main(String[] args) {
        UnDeadDemo td1 = new UnDeadDemo();
        UnDeadDemo td2 = new UnDeadDemo();
        td1.flag = 1;
        td2.flag = 0;
        new Thread(td1, "td1").start();
        new Thread(td2, "td2").start();
    }
}

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

    @Override
    public void run() {
        String threadName = Thread.currentThread().getName();
        System.out.println(threadName + ": flag = " + flag);
        if (flag == 1) {
            synchronized (o1) {
                System.out.println(threadName + ": 取得 o1 锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName + ": 申请 o2 锁");
                synchronized (o2) {
                    System.out.println("1");
                }
            }
        }
        if (flag == 0) {
            synchronized (o2) {
                System.out.println(threadName + ": 取得 o2 锁");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(threadName + ": 申请 o1 锁");
                synchronized (o1) {
                    System.out.println("0");
                }
            }
        }
    }
}
运行结果为:

td2: flag=0
td1: flag=1
td2: 取得o2锁
td2: 申请o1锁
0
td1: 取得o1锁
td1: 申请o2锁
1

程序对前面实例的 td2 代码进行修改,使在 td2 中获取资源的顺序和在 td1 中获取资源的顺序保持一致,这样就可以有效地避免产生死锁。

td1 和 td2 同时执行了 synchronized(o1),只有一个线程可以获取到 o1 锁上的监听器锁,假如 td1 获取到了,那么 td2 就会被阻塞而不会再获取 o2 锁,td1 获取到 o1 锁的监听器锁之后会申请 o2 锁的监听器锁,这时 td1 是可以获取到的,td1 获取到 o2 锁并使用后先释放 o2 锁,再释放 o1 锁,释放 o1 锁之后 td2 才会从阻塞状态变为运行状态。所以,资源的有序性破坏了资源的请求并持有条件和环路等待条件,由此可以避免产生死锁。

相关文章