我们先来看下到底都有哪些原因造成了死锁,Coffman大佬总结了只有以下这四个条件都发生时才会出现死锁:
互斥,共享资源 X 和 Y 只能被一个线程占用;
占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
知道了死锁产生的必要条件后,如何去避免呢?
对于第一条,互斥这个条件我们没有办法破坏,因为我们用锁为的就是互斥;不过其他三个条件都是有办法破坏掉的:
对于“占用且等待”这个条件,我们可以一次性申请所有的资源,这样就不存在等待了。
对于“不可抢占”这个条件,占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源,这样不可抢占这个条件就破坏掉了。
对于“循环等待”这个条件,可以靠按序申请资源来预防。所谓按序申请,是指资源是有线性顺序的,申请的时候可以先申请资源序号小的,再申请资源序号大的,这样线性化后自然就不存在循环了。
理论上我们已经知道怎么避免死锁了,接下来通过代码来加深一下印象
1. 破坏占用且等待条件
对于转账例子来说,它需要的资源有两个,一个是转出账户,另一个是转入账户。如何一次性申请所有的资源(转出、转入账户呢)?
我们需要引入一个管理员(Manager)的角色,统一管理这两个资源。他的作用是当执行转账操作时,首先去管理员这里申请全部的资源,成功后再锁定这两个资源,当转账完成之后,通知管理员立刻释放资源。
public class Account {
//账号
private String accountName;
// 余额
private int balance;
public Account(String accountName,int balance){
this.accountName = accountName;
this.balance = balance;
}
// 省略get/set
}
/**
* <p>必须是单例的,只能有一个人分配资源</p>
*
* @version 1.0
* @date : 2021/8/14 17:40
*/
public class Manager {
private volatile static Manager manger;
private Manager(){}
public static Manager getInstance() {
if (manger == null) {
synchronized (Manager.class) {
if (manger == null) {
manger = new Manager();
}
}
}
return manger;
}
private Set<Object> lockResources = new HashSet<Object>();
public synchronized boolean apply(Object... objs) {
for (Object obj : objs) {
if (lockResources.contains(obj)) {
return false;
}
}
for (Object obj : objs) {
lockResources.add(obj);
}
return true;
}
public synchronized void free(Object... objs) {
for (Object obj : objs) {
if (lockResources.contains(obj)) {
lockResources.remove(obj);
}
}
}
}
public class AccountUnLock implements Runnable {
//转出账户
public Account fromAccount;
//转入账户
public Account toAccount;
//转出金额
public int amount;
public AccountUnLock(Account fromAccount, Account toAccount, int amount){
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run(){
while(true) {
//1.申请资源时,先去管理员获取到所有资源。
Manager instance = Manager.getInstance();
try {
while(!instance.apply(fromAccount, toAccount));
// 2.给资源加锁
synchronized (fromAccount) {
synchronized (toAccount) {
//转账进行的条件:判断转出账户的余额是否大于0
if(fromAccount.getBalance() <= 0){
System.out.println(fromAccount.getAccountName() + "账户余额不足!");
return;
}else{
//更新转出账户的余额:
fromAccount.setBalance(fromAccount.getBalance() - amount);
//更新转入账户的余额:+
toAccount.setBalance(toAccount.getBalance() + amount);
}
System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
}
}// 3.释放锁
} finally {
// 4.通知管理员释放资源.
instance.free(fromAccount, toAccount);
}
}
}
public static void main(String[] args) {
Account fromAccount = new Account("张三",100000);
Account toAccount = new Account("李四",100000);
Thread a = new Thread(new AccountUnLock(fromAccount,toAccount,1));
Thread b = new Thread(new AccountUnLock(toAccount,fromAccount,1));
a.start();
b.start();
}
}
2.破坏不可抢占条件
破坏不可抢占核心是要能够主动释放它占有的资源,这一点 synchronized 是做不到的。原因是 synchronized 申请资源的时候,如果申请不到,线程直接进入阻塞状态了,而线程进入阻塞状态,啥都干不了,也释放不了线程已经占有的资源。java.util.concurrent 这个包下面提供的 Lock 是可以轻松解决这个问题的。
public class AccountUnLockReentrant implements Runnable {
//转出账户
public Account fromAccount;
//转入账户
public Account toAccount;
//转出金额
public int amount;
private Lock lock = new ReentrantLock();
public AccountUnLockReentrant(Account fromAccount, Account toAccount, int amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run() {
while (true) {
try {
// 锁【lock.lock】必须紧跟try代码块,且unlock要放到finally第一行
lock.lock();
//转账进行的条件:判断转出账户的余额是否大于0
if (fromAccount.getBalance() <= 0) {
System.out.println(fromAccount.getAccountName() + "账户余额不足!");
return;
} else {
//更新转出账户的余额:
fromAccount.setBalance(fromAccount.getBalance() - amount);
//更新转入账户的余额:+
toAccount.setBalance(toAccount.getBalance() + amount);
}
System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
System.out.println("转入用户:" + toAccount.getAccountName() + "余额:" + toAccount.getBalance());
} finally {
lock.unlock();
}
}
}
public static void main(String[] args) {
Account fromAccount = new Account("张三", 100000);
Account toAccount = new Account("李四", 100000);
Thread a = new Thread(new AccountUnLockReentrant(fromAccount, toAccount, 1));
Thread b = new Thread(new AccountUnLockReentrant(toAccount, fromAccount, 1));
a.start();
b.start();
}
}
3.破坏循环等待条件
破坏这个条件,需要对资源进行排序,然后按序申请资源。申请的时候,我们可以按照从小到大的顺序来申请。这样就不存在“循环”等待了。
public class AccountMain implements Runnable {
//转出账户
public Account fromAccount;
//转入账户
public Account toAccount;
//转出金额
public int amount;
public AccountMain(Account fromAccount, Account toAccount, int amount){
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run(){
Account left =null;
Account right = null;
if (fromAccount.hashCode() > toAccount.hashCode()){
left = toAccount;
right =fromAccount;
}
while(true){
synchronized (left) {
synchronized (right) {
//转账进行的条件:判断转出账户的余额是否大于0
if(fromAccount.getBalance() <= 0){
System.out.println(fromAccount.getAccountName() + "账户余额不足,无法进行转账");
return;
}else{
//更新转出账户的余额:
fromAccount.setBalance(fromAccount.getBalance() - amount);
//更新转入账户的余额:+
toAccount.setBalance(toAccount.getBalance() + amount);
}
}
}
System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
}
}
public static void main(String[] args) {
Account fromAccount = new Account("张三",100000);
Account toAccount = new Account("李四",200000);
Thread a = new Thread(new AccountMain(fromAccount,toAccount,1));
Thread b = new Thread(new AccountMain(toAccount,fromAccount,1));
a.start();
b.start();
}
}
4.优化循环等待
while(!instance.apply(fromAccount, toAccount));
在破坏占用且等待条件代码中,使用while自旋的方式调用apply方法来统一管理资源,如果 apply() 操作耗时非常短,而且并发冲突量也不大时,这个方案还挺不错的,因为这种场景下,循环上几次或者几十次就能一次性获取转出账户和转入账户了。但是如果 apply() 操作耗时长,或者并发冲突量大的时候,循环等待这种方案就不适用了,因为在这种场景下,可能要循环上万次才能获取到锁,太消耗 CPU 了。
我们用等待-通知来优化一下。当线程要求的条件不满足,则线程阻塞自己,进入等待状态;当线程要求的条件满足后,通知等待的线程重新执行。
public class Allocator {
private volatile static Allocator manger;
private Allocator(){}
public static Allocator getInstance() {
if (manger == null) {
synchronized (Allocator.class) {
if (manger == null) {
manger = new Allocator();
}
}
}
return manger;
}
private Set<Object> lockResources = new HashSet<Object>();
public synchronized void apply(Object... objs) {
for (Object obj : objs) {
while (lockResources.contains(obj)) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
for (Object obj : objs) {
lockResources.add(obj);
}
}
public synchronized void free(Object... objs) {
for (Object obj : objs) {
if (lockResources.contains(obj)) {
lockResources.remove(obj);
}
}
notifyAll();
}
}
public class AccountUnLock2 implements Runnable {
//转出账户
public Account fromAccount;
//转入账户
public Account toAccount;
//转出金额
public int amount;
public AccountUnLock2(Account fromAccount, Account toAccount, int amount){
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run(){
while(true) {
//1.申请资源时,先去管理员获取到所有资源。
Allocator instance = Allocator.getInstance();
try {
instance.apply(fromAccount, toAccount);
//转账进行的条件:判断转出账户的余额是否大于0
if(fromAccount.getBalance() <= 0){
System.out.println(fromAccount.getAccountName() + "账户余额不足!");
return;
}else{
//更新转出账户的余额:
fromAccount.setBalance(fromAccount.getBalance() - amount);
//更新转入账户的余额:+
toAccount.setBalance(toAccount.getBalance() + amount);
}
System.out.println("转出用户:" + fromAccount.getAccountName() + "余额:" + fromAccount.getBalance());
System.out.println("转入用户:" +toAccount.getAccountName() + "余额:" + toAccount.getBalance());
} finally {
// 4.通知管理员释放资源.
instance.free(fromAccount, toAccount);
}
}
}
public static void main(String[] args) {
Account fromAccount = new Account("张三",100000);
Account toAccount = new Account("李四",100000);
Thread a = new Thread(new AccountUnLock2(fromAccount,toAccount,1));
Thread b = new Thread(new AccountUnLock2(toAccount,fromAccount,1));
a.start();
b.start();
}
}
总结
在这一节中介绍了产生死锁的四个原因,以及如何避免死锁。通过等待-通知的方式优化循环机制。