java并发系列之:死锁

参考文章:https://segmentfault.com/a/1190000043810547

前言

在本文中,我们将探讨死锁的概念及其产生的原因,并通过示例代码来说明死锁的出现情况。我们还将介绍如何通过破坏死锁的不同条件来解决死锁问题,通过深入了解死锁及其解决方法,我们可以更好地应对并解决系统中可能出现的死锁情况。

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


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

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

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

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

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

2. 死锁示例及解决方案

出现死锁时,只需要破坏四个条件中的任意一个即可。但是,互斥条件是不能破坏的,因为只有互斥才能解决线程安全问题,所以只需要破坏其它三个条件中的任意一个即可。

2.1 死锁示例

以下代码模拟了两个人相互转账的场景。转账操作需要按照一定的顺序对账户进行加锁,以确保转账过程的安全性。然而,由于两个线程转账方向不同,导致加锁的顺序也不同。最终,两个线程相互等待对方释放锁资源,导致出现死锁现象。

账户类代码:

// 账户类
@Data
public 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. 总结

本文简单描述了死锁的概念、产生死锁需要满足的四个条件、破坏死锁不同条件的思路和示例。在实战中如何预防死锁、排查死锁,将在后面的文章中补充。若文章中有描述不清或者有误的地方,欢迎评论区讨论与指正!

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值