Java线程死锁

线程死锁

什么是死锁

线程1持有资源A,线程2持有资源B。这时如果线程1去请求资源B,线程2去请求资源A。由于资源A和资源B都已经被其它线程所持有,导致线程1和线程2一直无法获取到想要的资源,陷入无限等待状态。
Java线程死锁

一个必然死锁的程序

下面通过一段代码来演示下:

/**
 * 必然死锁的例子
 */
public class MustDeadLock implements Runnable {

    int state = 1;
    static Object lock1 = new Object();
    static Object lock2 = new Object();

    public static void main(String[] args) {
        MustDeadLock mustDeadLock = new MustDeadLock();
        MustDeadLock mustDeadLock1 = new MustDeadLock();
        mustDeadLock.state = 0;
        new Thread(mustDeadLock).start();
        new Thread(mustDeadLock1).start();
    }

    @Override
    public void run() {
        try {
            if (state == 1) {
                synchronized (lock1) {
                    System.out.println(Thread.currentThread().getName()+"获取到lock1");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+"准备获取lock2");
                    synchronized (lock2) {
                        System.out.println(Thread.currentThread().getName()+"获取到lock2");
                    }
                }
            }
            if (state == 0) {
                synchronized (lock2) {
                    System.out.println(Thread.currentThread().getName()+"获取到lock2");
                    Thread.sleep(500);
                    System.out.println(Thread.currentThread().getName()+"准备获取lock1");
                    synchronized (lock1) {
                        System.out.println(Thread.currentThread().getName()+"获取到lock1");
                    }
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

执行结果:

Thread-0获取到lock2
Thread-1获取到lock1
Thread-1准备获取lock2
Thread-0准备获取lock1

Thread-0获取到lock2,Thread-1获取到lock1。这时当Thread-1准备获取lock2时,由lock2被Thread-0持有,所以Thread-1就陷入了无限等待状态。Thread-0也一样一直等待获取lock1。

死锁发生的四个必要条件

通过以上例子,我们分析下死锁发生的四个必要条件。
1:互斥条件。无论时lock1 还是lock2,都只能同时被一个线程所持有。
2:持有并请求。Thread-0持有了一个资源lock2,这时它又去请求另外一个资源。
3:不剥夺条件。Thread-0持有的资源lock2只能由它自己去释放,线程不能释放不是由自己持有的资源。
4:环路等待。Thread-0获取到lock2,去请求lock1,Thread-1获取到lock1,去请求lock2。Thread-0在等待Thread-1持有的资源,Thread-1在等待Thread-0持有的资源。Thread-0 --》Thread-1 --》Thread-0。
如果发生死锁,以上这四个条件缺一不可。

实际生产中死锁问题

银行转账问题

有两个用户张三和李四,张三的账户余额有500元,李四的账户余额有500元。在同一时间张三向李四的账户转200元,李四向张三的账户转200元,互相转了100次。以下是具体实现。

/**
 * 模拟两个人转账导致线程死锁问题
 */
public class TransferMoney implements Runnable {

    Acount a;
    Acount b;
    int amount;

    public TransferMoney(Acount a, Acount b, int amount) {
        this.a = a;
        this.b = b;
        this.amount = amount;
    }

    public static void main(String[] args) {
        Acount acount = new Acount(500);
        Acount acount1 = new Acount(500);
        TransferMoney transferMoney = new TransferMoney(acount, acount1, 200);
        TransferMoney transferMoney2 = new TransferMoney(acount1, acount, 200);
        for (int i = 0; i < 100; i++) {
            new Thread(transferMoney).start();
            new Thread(transferMoney2).start();
        }


    }

    @Override
    public void run() {
        try {
            transfer(a, b, amount);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void transfer(Acount a, Acount b, int amount) throws InterruptedException {
        synchronized (a) {
            synchronized (b) {
                if (a.money < amount) {
                    System.out.println("账户余额不足,转账失败");
                }
                a.money -= amount;
                b.money += amount;
                System.out.println("转账成功");
            }
        }
    }

    static class Acount {
        int money;

        public Acount(int money) {
            this.money = money;
        }
    }
}

执行结果:
Java线程死锁
互相转账100次,总共应该打印200次转账结果。但只执行了两次程序就没有执行下去了。下面分析下导致这个问题的原因。
张三向李四的转账,必须先获取到张三账户和李四账户两把锁,以防止转账期间其它线程对账户进行操作。如果发生这种情况:一个线程持有了张三账户,另外一个线程持有了李四账户,这时两个线程去请求另一个账户锁的时候就会请求不到。两个线程陷入无限等待状态,同时其它想要获取这两个账户锁的线程也会一直等待。

使获取锁的顺序一致解决银行转账问题

我们可以使获取锁的顺序一致来解决上面银行转账问题。上面问题导致的原因之一是,不同的线程获取锁的顺序不同。张三向李四转账,先获取张三账户锁,再获取李四账户锁;李四向张三转账,先获取李四账户锁,再获取张三账户锁,这样就会形成一个环路:线程1(张三)-》线程2(李四)-》线程1(张三)。所以我们只要破坏这个环路,无论是张三向李四转账还是李四向张三转账,它们获取锁的顺序都是一致的,就可以解决死锁问题。下面是具体实现。

/**
 * 改变获取锁的顺序解决银行转账死锁问题。
 */
public class ChangeOrderDealTransferMoney implements Runnable {

    TransferMoney.Acount a;
    TransferMoney.Acount b;
    int amount;
    Object lock = new Object();

    public ChangeOrderDealTransferMoney(TransferMoney.Acount a, TransferMoney.Acount b, int amount) {
        this.a = a;
        this.b = b;
        this.amount = amount;
    }

    public static void main(String[] args) {
        TransferMoney.Acount acount = new TransferMoney.Acount(500);
        TransferMoney.Acount acount1 = new TransferMoney.Acount(500);
        ChangeOrderDealTransferMoney transferMoney = new ChangeOrderDealTransferMoney(acount, acount1, 200);
        ChangeOrderDealTransferMoney transferMoney2 = new ChangeOrderDealTransferMoney(acount1, acount, 200);
        for (int i = 0; i < 1000; i++) {
            new Thread(transferMoney).start();
            new Thread(transferMoney2).start();
        }
    }

    @Override
    public void run() {
        // 根据hash值来判断获取锁的顺序,如果有hash冲突,加一个竞争锁。
        if (a.hashCode() > b.hashCode()) {
            synchronized (a) {
                synchronized (b) {
                    transfer(a, b, amount);
                }
            }
        }
        if (a.hashCode() < b.hashCode()) {
            synchronized (b) {
                synchronized (a) {
                    transfer(a, b, amount);
                }
            }
        } else {
            synchronized (lock) {
                transfer(a, b, amount);
            }
        }
    }

    public static void transfer(TransferMoney.Acount a, TransferMoney.Acount b, int amount) {
        if (a.money < amount) {
            System.out.println("账户余额不足,转账失败");
        }
        a.money -= amount;
        b.money += amount;
        System.out.println("转账成功");
    }
}

执行结果:
Java线程死锁
通过账户的hash值来判断获取锁的顺序,这样不论是张三向李四转账还是李四向张三转账,获取锁的顺序都一致。如果有hash冲突的情况,可以加一个竞争锁来保证线程安全。在实际生产中可以根据索引来判断获取锁的顺序。

哲学家就餐问题以及对应的解决方法

有五位哲学家在一起就餐,每位哲学家左右两边只有一个刀叉。只有拿起刀叉的时候才可以就餐。假设每个哲学家先拿左手边的餐具,再拿右手边的餐具,就完餐后就思考,思考完后又就餐,会发生什么情况。下面用代码演示一下:

/**
 * 哲学家就餐问题演示。
 */
public class DiningPhilosopher implements Runnable {

    Object knife;
    Object cross;

    public DiningPhilosopher(Object knife, Object cross) {
        this.knife = knife;
        this.cross = cross;
    }

    public static void main(String[] args) {
        Object[] tableware = new Object[5];
        for (int i = 0; i < tableware.length; i++) {
            tableware[i] = new Object();
        }
        DiningPhilosopher[] diningPhilosophers = new DiningPhilosopher[5];
        for (int i = 0; i < diningPhilosophers.length; i++) {
            diningPhilosophers[i] = new DiningPhilosopher(tableware[i % 4], tableware[(i + 1) % 4]);
            new Thread(diningPhilosophers[i]).start();
        }
    }

    @Override
    public void run() {
        while (true) {
            doSomething("thinking");
            synchronized (knife) {
                synchronized (cross) {
                    doSomething("eating");
                    doSomething("put down cross");
                }
                doSomething("put down knife");
            }
        }
    }

    private void doSomething(String str) {
        System.out.println(Thread.currentThread().getName() + " " + str);
    }
}

执行结果:
Java线程死锁
可以看到线程陷入了死锁。

解决哲学家就餐问题思路

解决哲学家就餐问题的方法有很多,从破坏死锁的四个必要条件去考虑。
1:从互斥条件考虑。餐具只能被一个人持有,所以这个条件是无法破坏的。
2:持有并请求。哲学家就餐是先拿左手边的餐具,再去拿右手边的餐具。可以让则学家同时去拿左手边的餐具和右手边的餐具。
3:不可剥夺。拿到餐具后,只有自己就餐后才放下。如果有一个服务员,可以提醒哲学家在还未就餐时放下餐具,也可以解决此问题。
4:环路等待。破坏环路等待有两种方式,一是改变一个哲学家拿餐具的顺序。第二:发餐牌,五个人发四张餐牌,只有拿到餐牌的哲学家才能就餐。
以上就是解决则学家就餐问题的一些思路。

其它线程活跃性问题

活锁

线程一直做一些无意义的事情

饥饿

线程一直得不到CPU的调度

上一篇:Dapr Pub/Sub 集成 RabbitMQ 、Golang、Java、DotNet Core


下一篇:动态代理