前言

在本文中,咱们将探讨死锁的概念及其产生的起因,并通过示例代码来阐明死锁的呈现状况。咱们还将介绍如何通过毁坏死锁的不同条件来解决死锁问题,通过深刻理解死锁及其解决办法,咱们能够更好地应答并解决零碎中可能呈现的死锁状况。

1. 死锁的概念与产生死锁的条件


死锁指的是,一组相互竞争资源的线程,它们之间互相期待而导致永恒阻塞的景象。
当上面四个条件同时满足时,就会产生死锁:

1.互斥:一个共享资源同一时间只能被一个线程占用;

2.占有且期待:线程1曾经取得共享资源X,在期待获取共享资源Y的时候,它不会开释曾经占有的共享资源X;

3.不可抢占:线程不能抢占其它线程曾经占有的共享资源;

4.循环期待:线程1期待线程2占有的资源,线程2期待线程1占有的资源,这就是循环期待。

2. 死锁示例及解决方案

呈现死锁时,只须要毁坏四个条件中的任意一个即可。然而,互斥条件是不能毁坏的,因为只有互斥能力解决线程平安问题,所以只须要毁坏其它三个条件中的任意一个即可。

2.1 死锁示例

以下代码模仿了两个人互相转账的场景。转账操作须要依照肯定的程序对账户进行加锁,以确保转账过程的安全性。然而,因为两个线程转账方向不同,导致加锁的程序也不同。最终,两个线程互相期待对方开释锁资源,导致呈现死锁景象。

账户类代码:

// 账户类@Datapublic class Account {    private String accountName; // 账户名    private int balance; // 账户余额    public Account(String accountName, int balance) {        this.accountName = accountName;        this.balance = balance;    }    // 转出操作,缩小转出方的余额    public void debit(int count) {        this.balance -= count;    }    // 转入操作,减少转入方的余额    public void credit(int count) {        this.balance += count;    }}

转账示例代码:

// 呈现死锁的示例public class TransferAccount implements Runnable {    private Account formAccount; // 转出账户    private Account toAccount; // 转入账户    private int amount; // 转账金额    public TransferAccount(Account formAccount, Account toAccount, int amount) {        this.formAccount = formAccount;        this.toAccount = toAccount;        this.amount = amount;    }    @Override    public void run() {        while(true) {            // 爱护转入转出账户,其余线程在转账期间无奈操作,保障线程平安            synchronized (formAccount) {                synchronized (toAccount) {                    // 转出方余额足够                    if (formAccount.getBalance() >= amount) {                        formAccount.debit(amount);                        toAccount.credit(amount);                    }                }            }            System.out.println("fromAccount:" + formAccount.getAccountName() + ":" + formAccount.getBalance());            System.out.println("toAccount:" + toAccount.getAccountName() + ":" + toAccount.getBalance());        }    }    public static void main(String[] args) {        Account zhangsan = new Account("张三", 1000);        Account lisi = new Account("李四", 2000);        // 因为上面持有锁的程序是相同的,所以可能会呈现持有并期待的状况而导致死锁        // 张三向李四转10块钱        Thread t1 = new Thread(new TransferAccount(zhangsan, lisi, 10));        // 李四向张三转20块钱        Thread t2 = new Thread(new TransferAccount(lisi, zhangsan, 20));        t1.start();        t2.start();    }}

2.2 解决方案一:毁坏占有且期待条件

如果在进行操作之前,一次性获取所有的共享资源,那么就不会存在占有局部资源,期待另一部分资源开释的状况了,毁坏的是占有且期待条件。这种形式相当于把锁的范畴扩充了,尽管能解决死锁问题,然而会导致性能升高。

咱们定义一个资源管理器,应用一个汇合对立治理共享资源来实现上述操作。资源管理器中定义一个互斥的资源申请办法,只有所有资源都没被占用的状况下,能力同时获取所有资源。

// 一次性获取资源的资源管理器,解决死锁的持有并期待问题public class Allocator {    private List<Object> list = new ArrayList<>(); // 通过汇合对立治理申请的资源    // 一次性申请所有资源    synchronized boolean apply(Object form, Object to) {        // 如果任意一个资源被占用了,就无奈一次性获取全副资源,返回false        if (list.contains(form) || list.contains(to)) {            return false;        }        list.add(form);        list.add(to);        return true;    }    // 开释资源    synchronized void free(Object form, Object to) {        list.remove(form);        list.remove(to);    }}

在转账操作中,先通过资源管理器获取资源,获取胜利的状况下才执行转账操作。转账操作执行结束后,开释资源。

// 通过Allocator对立申请资源public class TransferAccount01 implements Runnable {    private Account formAccount; // 转出账户    private Account toAccount; // 转入账户    private int count; // 转入金额    private Allocator allocator; // 对立资源管理器    public TransferAccount01(Account formAccount, Account toAccount, int count, Allocator allocator) {        this.formAccount = formAccount;        this.toAccount = toAccount;        this.count = count;        this.allocator = allocator;    }    @Override    public void run() {        while (true) {            // 一次性申请所有资源            if (allocator.apply(formAccount, toAccount)) {                try {                    if (formAccount.getBalance() >= count) {                        formAccount.debit(count);                        toAccount.credit(count);                    }                } finally {                    // 开释资源                    allocator.free(formAccount, toAccount);                }            }            System.out.println("fromAccount:" + formAccount.getAccountName() + ":" + formAccount.getBalance());            System.out.println("toAccount:" + toAccount.getAccountName() + ":" + toAccount.getBalance());        }    }    public static void main(String[] args) {        Account zhangsan = new Account("张三", 1000);        Account lisi = new Account("李四", 2000);        Allocator allocator = new Allocator();        // 因为上面持有锁的程序是相同的,所以可能会呈现持有并期待的状况        // 张三向李四转10块钱        Thread t1 = new Thread(new TransferAccount01(zhangsan, lisi, 10, allocator));        // 李四向张三转20块钱        Thread t2 = new Thread(new TransferAccount01(lisi, zhangsan, 20, allocator));        t1.start();        t2.start();    }}

2.3 解决方案二:毁坏不可抢占条件

在后面死锁的代码示例里,加锁形式用的是synchronized,如果线程获取不到锁,就会始终阻塞在那里。咱们是否让线程在获取其它锁失败的状况下,被动开释本人持有的锁,从而解决死锁呢?

咱们能够通过非阻塞的ReentrantLock的tryLock()办法获取锁,如果没有获取到锁,它会立刻返回,而不是阻塞线程。获取不到其它锁,那就调用ReentrantLock的unlock()办法来开释本人持有的锁,从而毁坏不可抢占条件

// 毁坏不可抢占条件public class TransferAccount02 implements Runnable {    private Account formAccount; // 转出账户    private Account toAccount; // 转入账户    private int count; // 转入金额    ReentrantLock formLock = new ReentrantLock();    ReentrantLock toLock = new ReentrantLock();    public TransferAccount02(Account formAccount, Account toAccount, int count) {        this.formAccount = formAccount;        this.toAccount = toAccount;        this.count = count;    }    @Override    public void run() {        while(true) {            // 非阻塞期待,没有取得锁就不会进入上面这段代码            if (formLock.tryLock()) {                try {                    // 假如获取了下面的锁,然而上面的锁没获取到                    if (toLock.tryLock()) {                        try {                            // 转出方余额足够                            if (formAccount.getBalance() >= count) {                                formAccount.debit(count);                                toAccount.credit(count);                            }                        } finally {                            toLock.unlock(); // 开释toLock锁                        }                    }                } finally {                    formLock.unlock(); // 获取toLock失败时,开释持有的formLock                }            }            System.out.println("fromAccount:" + formAccount.getAccountName() + ":" + formAccount.getBalance());            System.out.println("toAccount:" + toAccount.getAccountName() + ":" + toAccount.getBalance());        }    }    public static void main(String[] args) {        Account zhangsan = new Account("张三", 1000);        Account lisi = new Account("李四", 2000);        // 张三向李四转10块钱        Thread t1 = new Thread(new TransferAccount02(zhangsan, lisi, 10));        // 李四向张三转20块钱        Thread t2 = new Thread(new TransferAccount02(lisi, zhangsan, 20));                t1.start();        t2.start();    }}

2.4 解决方案三:毁坏循环期待条件

下面死锁案例中,导致死锁的根本原因是因为多线程加锁的程序不统一。如果张三和李四都遵循雷同的加锁和解锁程序,就能够毁坏循环期待条件,从而解决死锁问题。
在本实例中,按雷同的程序加锁解锁会毁坏原有的业务逻辑,这里仅仅是演示解决死锁的一种思路。在实在业务场景中,须要结合实际状况进行判断和取舍。

// 毁坏循环期待条件public class TransferAccount03 implements Runnable {    private Account formAccount; // 转出账户    private Account toAccount; // 转入账户    private int count; // 转入金额    public TransferAccount03(Account formAccount, Account toAccount, int count) {        this.formAccount = formAccount;        this.toAccount = toAccount;        this.count = count;    }    @Override    public void run() {        // 不论内部调用程序如何,外部保障加锁程序统一        // 顺序调用的时候可能不走这段逻辑,然而逆序调用的时候会走这段逻辑,保障程序和逆序调用时候的加锁程序统一        Account left = null;        Account right = null;        if (formAccount.hashCode() > toAccount.hashCode()) {            left = toAccount;            right = formAccount;        }        while(true) {            // 爱护转入转出账户,其余线程在转账期间无奈操作            synchronized (left) {                synchronized (right) {                    // 转出方余额足够                    if (formAccount.getBalance() >= count) {                        formAccount.debit(count);                        toAccount.credit(count);                    }                }            }            System.out.println("fromAccount:" + formAccount.getAccountName() + ":" + formAccount.getBalance());            System.out.println("toAccount:" + toAccount.getAccountName() + ":" + toAccount.getBalance());        }    }    public static void main(String[] args) {        Account zhangsan = new Account("张三", 1000);        Account lisi = new Account("李四", 2000);        // 张三向李四转10块钱        Thread t1 = new Thread(new TransferAccount03(zhangsan, lisi, 10));        // 李四向张三转20块钱        Thread t2 = new Thread(new TransferAccount03(lisi, zhangsan, 20));                t1.start();        t2.start();    }}

3. 总结

本文简略形容了死锁的概念、产生死锁须要满足的四个条件、毁坏死锁不同条件的思路和示例。在实战中如何预防死锁、排查死锁,将在前面的文章中补充。若文章中有形容不清或者有误的中央,欢送评论区探讨与斧正!