多线程基础学习篇之死锁

讲死锁之前先讲一个转账的例子

@Data
@AllArgsConstructor
@NoArgsConstructor
public class Account {
    // 余额
    private int balance;
    // 转账
    public void transfer(Account target, int money) {
        if (this.balance > money) {
            balance -= money;
            target.setBalance(target.getBalance() + money);
        }
    }

}

这是一个账户类它有一个转账操作,假如账户A需要给账户B操作,在多线程环境下进行此操作,第一想法是在transfer()方法前加个synchronized关键字

public synchronized void transfer(Account target, int money) {
        if (this.balance > money) {  
            balance -= money;
            target.setBalance(target.getBalance() + money);
        }
    }

但仔细想想这个方法其实不太靠谱

假设当前有账户A,B,C ,余额各为200元。 需求是 A 转账 B 100元,B 转账 C 100元
如果此时线程 t1 执行账户A转给账户B ,线程 t2 执行账户B 转给账户C。
我们的预期结果是 A :100元,B:200元,C:300元
这样的话就会产生问题了。因为当前 synchronized 锁定的是this对象,所以 线程t1 和线程t2 其实还是可以同时进入transfer()中 。
这时就会发生:当线程t1读到账户B的余额还没有对其增加时,线程t2 也读到了账户B的余额。
此时就会产生与预期不一样的2种结果

  1. 如果线程t1 先于线程t2执行完操作,线程t2对账户B的操作会覆盖掉线程t1对账户B的操作,此时账户B余额为 100元。
  2. 如果线程t2 先于线程t1执行完操作,线程t1对账户B的操作会覆盖掉线程t2对账户B的操作,此时账户B余额为 300元。

那么该怎么解决这个问题呢?

上个例子中问题发生的原因为 2个线程同时进入了transfer()方法。因为synchronized关键字修饰的是实例方法
他代表锁定的对象为this,而账户A 和 账户B 为2个不同的 Account 对象。

我们会想到只要将锁的范围改为当前类的class对象就可以解决此问题

  1. transfer()方法 用static 关键字 修饰

  2. 将方法实现部分 用 synchronized(Account.class)包起来。

这2种方法都可以将锁定对象变为class对象 从而解决了多线程之间转账并发问题

可是朋友们!这样却带来了重大的弊端。
我们试想一下 如果现实生活种这样的话还得了,同一时刻只能一个账户转给另一个账户
这种办法大大影响了执行性能,因为2个线程中操作的账户 如果是不相关的 ,那么完全可以实现并发执行。

还有更好的解决方法吗?
当然有,我们可以这样做,新的transfer()方法实现如下:

// 转账
public void transfer(Account01 target, int money)  {
     synchronized (this) {
         synchronized (target) {
             if (this.balance > money) {
                 balance -= money;
                 target.setBalance(target.getBalance() + money);
             }
         }
     }
 }

现在我们模拟上个例子中发生的操作 看看会不会发生转账并发问题。

  1. 线程 t1 获取this锁 即账户A对象的锁, 线程 t2 获取this锁 即账户B对象的锁
  2. 此时线程A会被阻塞 因为获取不了target锁 即账户B对象锁
  3. 线程t1 等待线程 t2获取完账户C的锁执行完转账 并释放账户B锁之后才会继续向下执行转账操作

很明显 这样就保证了 A 转账B, B转账C,同一时刻只会有一个执行。从而保证了2个线程 对 有关联账户转账的线程问题。并且!也不会有class对象锁的 性能问题,不关联账户操作,完全可以实现并发。
这个行为叫做锁细 粒化。使用细粒度锁可以提高并行度,是性能优化的一个重要手段。

但是!!!!!!!!
这却产生了另一个问题 也就是今天的主题 死锁!

假设现在的需求是账户A 转账 给转户B,账户B也需要转账给账户A
此时:

  1. 线程t1 获取账户A对象锁,线程t2获取账户B对象锁
  2. 线程 t1 和 t2 都互相等待 对方已经占用的锁

导致程序不能正常运行 一直阻塞 这就是死锁

死锁发生的4个条件

  1. 互斥,共享资源 X 和 Y 只能被一个线程占用;
  2. 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
  3. 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
  4. 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等 待。

我们只需要破坏其中的一个条件就可以 破解死锁

  1. 破坏互斥:因为我们本来就是为了保证只有一个线程占用 所以 不能破坏该条件

  2. 破坏占有且等待:要破坏这个条件可以一次性获得所有资源 我们通过增加一个Allocator类来实现

    Allocator有两个重要功能 :同时申请 资源 apply() 和同时释放资源 free()。
    账户 Account 类里面持有一个 Allocator 的单例(必须是 单例,只能由一个人来分配资源)。
    当账户 Account 在执行转账操作的时候,首先向 Allocator 同时申请转出账户和转入账户这两个资源,成功后 再锁定这两个资源;当转账操作执行完,释放 锁之后我们需通知 Allocator 同时释放转出账户和转入账户这两个资源。具体的代码实现如 下。

    class Allocator {
        private List<Object> als = new ArrayList<>();
    	// 同时申请2个资源 不满足条件就return false
        public synchronized boolean apply(Object form, Object to) {
            if (als.contains(form) || als.contains(to)) {
                return false;
            }
            als.add(form);
            als.add(to);
            return true;
        }
    	// 同时释放资源
        public synchronized void free(Object form, Object to) {
            als.remove(form);
            als.remove(to);
        }
    
    }
    
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class Account01 {
        private int balance;
        private int id;
        private Allocator allocator;
    
        public void transfer(Account01 target, int money) throws InterruptedException {
           // 循环申请2个资源
            while (!allocator.apply(this, target)) ;
            try {
                synchronized (this) {
                    Thread.sleep(200);
                    synchronized (target) {
                        if (this.balance > money) {
                            balance -= money;
                            target.setBalance(target.getBalance() + money);
                        }
                    }
                }
            } finally {
            	// 转账完释放资源
                allocator.free(this, target);
            }
        }
    
    }
    
  3. 破坏循环等待条件:破坏这个条件我们需要按照顺序加锁。按照什么顺序呢?
    我们可以给每个账户加一个id属性。转账方法中我们先对id小的账户加锁
    注意:如果id相等需要再加一个锁

    @Data
    @NoArgsConstructor
    public class Account {
        private int balance;
        private int id;
        // 用来解决id相等需要在外面再加个锁
        private static final Object object = new Object();
    
        public Account(int balance, int id) {
            this.balance = balance;
            this.id = id;
        }
    
        public void transfer(Account target, int money) throws InterruptedException {
            Account left = this;
            Account right = target;
            if (left.getId() > right.getId()) {
                left = target;
                right = this;
            } else if (left.getId() == right.getId()) {
                synchronized (object) {
                    synchronized (left) {
                        Thread.sleep(200);
                        synchronized (right) {
                            if (this.balance > money) {
                                balance -= money;
                                target.setBalance(target.getBalance() + money);
                            }
                        }
                    }
                }
                return;
            }
            synchronized (left) {
                Thread.sleep(200);
                synchronized (right) {
                    if (this.balance > money) {
                        balance -= money;
                        target.setBalance(target.getBalance() + money);
                    }
                }
            }
        }
    
    }
    

    这种方法中如果2个账户的id相等的话 还需要再加一个锁

    4.破坏不可抢占条件:破坏不可抢占条件看上去很简单,核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态 了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。

总结
当我们用细粒度锁 锁定多个资源时 需要注意死锁的问题

破坏占用且等待条件的成本比破 坏循环等待条件的成本高,破坏占用且等待条件,我们也是锁了所有的账户,而且还是用了死循 环 while(!actr.apply(this, target));方法,不过好在 apply() 这个方法基本不耗时。 但是还有比使用apply()更好的办法。那就是利用等待/通知机制

参考:极客时间:java并发编程实战 王宝令

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值