1.不小心死锁了怎么办?
1.1什么是死锁?
在使用线程的使用过程中,我们无法避免加锁的情况,那么加锁就可能会导致死锁。如果出现了死锁的情况,我们需要怎么去解决呢?首先,我们来看下什么是死锁,下面这张图很形象的说明了什么是死锁,路口的四辆车都分别等待对面车道的车让行,相互等待,形成了死锁的状态。在线程中,死锁是这样定义的,一组相互竞争相同资源的线程因为相互等待而导致永久阻塞,这种现象就叫做死锁。
1.2模拟死锁示例
用一个转账的示例来模拟死锁。
账户类:
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 amount) {
this.balance -= amount;
}
public void cribit(int amount) {
this.balance += amount;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public int getBalance() {
return balance;
}
public void setBalance(int balance) {
this.balance = balance;
}
}
转账类:
public class TransferAccount implements Runnable{
private Account fromAccount; // 转入账户
private Account toAccount; // 转出账户
private int amount;
public TransferAccount(Account fromAccount, Account toAccount, int amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run() {
while(true){
synchronized (fromAccount){
synchronized (toAccount){
if(fromAccount.getBalance()>=amount){
fromAccount.debit(amount);
toAccount.cribit(amount);
}
}
System.out.println(fromAccount.getAccountName()+"--------------"+fromAccount.getBalance());
System.out.println(toAccount.getAccountName()+"--------------"+toAccount.getBalance());
}
}
}
}
测试类:
public class Test {
public static void main(String[] args) {
Account fromAccount = new Account("zhangsan",100000);
Account toAccount = new Account("lisi",200000);
Thread a = new Thread(new TransferAccount(fromAccount,toAccount,1));
Thread b = new Thread(new TransferAccount(toAccount,fromAccount,1));
a.start();
b.start();
}
}
当我们运行测试类后,会发现当执行到某个时候,控制台会停止输出,这个时候就是发生了死锁。
1.3发生死锁的原因
发生死锁必需以下四个条件同时满足
- 互斥,共享资源 X 和 Y 只能被一个线程占用;
- 占有且等待,线程 T1 已经取得共享资源 X,在等待共享资源 Y 的时候,不释放共享资源 X;
- 不可抢占,其他线程不能强行抢占线程 T1 占有的资源;
- 循环等待,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源,就是循环等待。
1.4如何解决死锁问题
上面说到发生死锁必须同时满足四个条件,那么,我们破坏其中一个条件,就可以解决死锁的问题。如果已经发生了死锁,一般没有什么好的方法来解决,只能通过重启应用,所以如果要解决死锁问题,最好的方式就是提前规避。
首先,我们肯定不能去破坏第一个条件,锁的作用就是要互斥。所以,只能去破坏其他三个条件。
1.4.1 解决方法一
破坏第二个条件,占有且等待,那么我们可以一次性去申请所有资源,就不会存在等待的问题了。
对上面的示例进行改进:
新增类Allocator :
public class Allocator {
private List<Object> list = new ArrayList<>();
/**
* 申请资源的方法
* @param from
* @param to
* @return
*/
synchronized boolean apply(Object from,Object to){
if(list.contains(from) || list.contains(to))return false;
list.add(from);
list.add(to);
return true;
}
/**
* 释放资源的方法
* @param from
* @param to
* @return
*/
synchronized void free(Object from,Object to){
list.remove(from);
list.remove(to);
}
}
修改TransferAccount类:
public class TransferAccount01 implements Runnable{
private Account fromAccount; // 转入账户
private Account toAccount; // 转出账户
private int amount;
Allocator allocator;
public TransferAccount01(Account fromAccount, Account toAccount, int amount,Allocator allocator) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
this.allocator = allocator;
}
@Override
public void run() {
while(true){
if(allocator.apply(fromAccount,toAccount)){ // 都会在这个地方去获取资源
try {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.cribit(amount);
}
}
System.out.println(fromAccount.getAccountName() + "--------------" + fromAccount.getBalance());
System.out.println(toAccount.getAccountName() + "--------------" + toAccount.getBalance());
}
}finally {
allocator.free(fromAccount,toAccount);
}
}
}
}
}
测试类修改:
public class Test {
public static void main(String[] args) {
Account fromAccount = new Account("zhangsan",100000);
Account toAccount = new Account("lisi",200000);
Allocator allocator = new Allocator(); // 统一分配锁
Thread a = new Thread(new TransferAccount01(fromAccount,toAccount,1,allocator));
Thread b = new Thread(new TransferAccount01(toAccount,fromAccount,1,allocator));
a.start();
b.start();
}
}
经过这样处理之后就不会再出现死锁的问题了。
1.4.2 解决方法二
破坏不可抢占,可以让占用部分资源的线程进一步申请资源,如果申请不到,可以主动释放其他线程占用的资源,这样就可以把不可抢占的条件破坏掉。
修改TransferAccount类:
public class TransferAccount02 implements Runnable {
private Account fromAccount; // 转入账户
private Account toAccount; // 转出账户
private int amount;
Lock fromLock = new ReentrantLock();
Lock toLock = new ReentrantLock();
public TransferAccount02(Account fromAccount, Account toAccount, int amount) {
this.fromAccount = fromAccount;
this.toAccount = toAccount;
this.amount = amount;
}
@Override
public void run() {
while (true) {
if (fromLock.tryLock()) {
if (toLock.tryLock()) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.cribit(amount);
}
System.out.println(fromAccount.getAccountName() + "--------------" + fromAccount.getBalance());
System.out.println(toAccount.getAccountName() + "--------------" + toAccount.getBalance());
}
}
}
}
}
测试类修改:
public class Test {
public static void main(String[] args) {
Account fromAccount = new Account("zhangsan",100000);
Account toAccount = new Account("lisi",200000);
Thread a = new Thread(new TransferAccount02(fromAccount,toAccount,1));
Thread b = new Thread(new TransferAccount02(toAccount,fromAccount,1));
a.start();
b.start();
}
}
1.4.2 解决方法三
破坏循环等待,可以按照顺序来进行资源的处理。
这里仅对TransferAccount进行修改,对传进来的账户的hashcode进行排序以保证顺序。
public class TransferAccount03 implements Runnable{
private Account fromAccount; // 转入账户
private Account toAccount; // 转出账户
private int amount;
public TransferAccount03(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){
if(fromAccount.getBalance()>=amount){
fromAccount.debit(amount);
toAccount.cribit(amount);
}
}
System.out.println(fromAccount.getAccountName()+"--------------"+fromAccount.getBalance());
System.out.println(toAccount.getAccountName()+"--------------"+toAccount.getBalance());
}
}
}
}
但是在这个例子中会存在一个问题,最后变成总是从一个相同的账户转到另一个账号,最终有一个账户的余额为0.