5. 一不小心就死锁了,怎么办?- 理论基础

1. 问题的由来

上一篇用Account.class作为互斥锁,但是所有账户的转账都是串行化,性能太差。比如A给B转账,C给D转账同时进行,这是现实允许的,但是现在都串行化了。

2. 用两把锁解决并发

在这里插入图片描述

class Account {
	private int balance;

	// 转账
	void transfer(Account target, int amt) {
		// 锁定转出账户
		synchronized (this) {
			// 锁定转入账户
			synchronized (target) {
				if (this.balance > amt) {
					this.balance -= amt;
					target.balance += amt;
				}
			}
		}
	}
}

以上可以解决A给B转账,C给D转账的并发问题。

3. 使用两把锁带来的问题

使用细粒度锁提高性能的同时,有可能带来死锁问题。
死锁:一组互相竞争资源因互相等待,导致永久阻塞的现象。

死锁是如何发生的:

  • 假设线程 T1 执行账户 A 转账户 B 的操作,账户 A.transfer(账户 B);同时线程 T2 执行账户 B 转账户 A 的操作,账户 B.transfer(账户 A)。
  • 当 T1 和 T2 同时执行完①处的代码时,T1 获得了账户 A 的锁(对于 T1,this 是账户 A),而 T2 获得了账户 B 的锁(对于 T2,this 是账户 B)。
  • 之后 T1 和 T2 在执行②处的代码时,T1 试图获取账户 B 的锁时,发现账户 B 已经被锁定(被 T2 锁定),所以 T1 开始等待;T2 则试图获取账户 A 的锁时,发现账户 A 已经被锁定(被 T1 锁定),所以 T2 也开始等待。于是 T1 和 T2 会无期限地等待下去,也就是所说的死锁了。
class Account {
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    // 锁定转出账户
    synchronized(this){// 锁定转入账户
      synchronized(target){if (this.balance > amt) {
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

资源分配图:
在这里插入图片描述

4. 如何预防死锁

Coffman总结的四个条件都发生才会出现死锁:

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

只要破坏其中一个就可以避免死锁。

解决办法

  1. 互斥无法破坏,因为锁就是互斥的;
  2. 对于占有且等待,一次性申请所有资源;
  3. 对于不可抢占,占有资源的线程申请其他资源时,申请不到,就主动释放占有的资源;
  4. 对于循环等待,可以按序申请资源预防,比如先申请资源序号小的,再申请资源序号大的。

4.1 破坏占有且等待条件

建一个单例类来管理锁,所有的转账都向这个单例申请锁. 如以下单例类有两个重要方法:同时申请资源 apply() 和同时释放资源 free()。

class Allocator {
	private List<Object> als = new ArrayList<>();

	// 一次性申请所有资源
	synchronized boolean apply(Object from, Object to) {
		if (als.contains(from) || als.contains(to)) {
			return false;
		} else {
			als.add(from);
			als.add(to);
		}
		return true;
	}

	// 归还资源
	synchronized void free(Object from, Object to) {
		als.remove(from);
		als.remove(to);
	}
}

class Account {
	// actr 应该为单例
	private Allocator actr;
	private int balance;

	// 转账
	void transfer(Account target, int amt) {
		// 一次性申请转出账户和转入账户,直到成功
		while (!actr.apply(this, target))
			;
		try {
			// 锁定转出账户
			synchronized (this) {
				// 锁定转入账户
				synchronized (target) {
					if (this.balance > amt) {
						this.balance -= amt;
						target.balance += amt;
					}
				}
			}
		} finally {
			actr.free(this, target);
		}
	}
}

4.2 破坏不可抢占的条件

核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。

java在语言层面没有解决问题,在SDK层面解决,在java.util.concurrent的lock解决。

4.3 破坏循环等待条件

破坏这个条件,需要对资源进行排序,然后按序申请资源。

假设账户有个id,可以按照id排序,锁定资源的时候按照顺序来。
①~⑥处的代码对转出账户(this)和转入账户(target)排序,然后按照序号从小到大的顺序锁定账户。这样就不存在“循环”等待了。

class Account {
  private int id;
  private int balance;
  // 转账
  void transfer(Account target, int amt){
    Account left = thisAccount right = target;if (this.id > target.id) { ③
      left = target;           ④
      right = this;}// 锁定序号小的账户
    synchronized(left){
      // 锁定序号大的账户
      synchronized(right){ 
        if (this.balance > amt){
          this.balance -= amt;
          target.balance += amt;
        }
      }
    }
  } 
}

5. 总结

遇到并发问题可以模拟现实的世界来理解和解决,不过要注意现实世界和计算机领域的不同,容易忽略一些细节。

预防死锁的三中方法中,也需要评估哪种成本最低。

6. 思考

我们上面提到:破坏占用且等待条件,我们也是锁了所有的账户,而且还是用了死循环 while(!actr.apply(this, target));

这个方法,那它比 synchronized(Account.class) 有没有性能优势呢?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值