同步(synchronized)可以保证共享资源的安全可靠性,但是同步使用不当,就会在多线程环境下产生死锁问题。死锁就是指两个线程都在等待彼此执行完毕,互相等待,造成了程序的停滞。

01、一个简单的示例认识Java死锁

一个线程thread1在持有锁lock1的情况下,再去申请获得锁lock2;另一个线程thread2在持有锁lock2的情况下,再去申请获得锁lock1;此时就会造成线程thread1和thread2的死锁。

因为Java中默认的锁申请操作是阻塞的,线程thread1持有了lock1锁,线程thread2持有了lock2锁,他们各自线程的操作都需要获取对方已经持有的锁,由于线程里synchronized锁住的代码全部执行完毕才会释放锁,这时其他线程才可能拿到被释放的锁,因此thread1线程在等待thread2线程释放lock2锁,thread2线程在等待thread1线程释放lock1锁,形成了互相等待,所以线程thread1和thread2永远被阻塞了,导致了死锁。

示例:代码演示上面的死锁过程

// 死锁演示
public class LockTask {
public static void main(String[] args) {
final Object lock1 = new Object(); // 锁lock1
final Object lock2 = new Object(); // 锁lock2


// 定义线程1
    Thread thread1 = new Thread(new Runnable() {
public void run() {
synchronized (lock1) { // 线程thread1持有锁lock1
try {
            System.out.println("---线程thread1持有锁lock1");
            Thread.sleep(50);// 模拟线程操作需要时间50ms
synchronized (lock2) { // 线程thread1申请获得锁lock1
              System.out.println("---线程thread1申请获得锁lock1成功");
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }
    });


// 定义线程2
    Thread thread2 = new Thread(new Runnable() {
public void run() {
synchronized (lock2) { // 线程thread2持有锁lock2
try {
            System.out.println("===线程thread2持有锁lock2");
            Thread.sleep(50); // 模拟线程操作需要时间50ms
synchronized (lock1) { // 线程thread2申请获得锁lock1
              System.out.println("===线程thread2申请获得锁lock1成功");
            }
          } catch (Exception e) {
            e.printStackTrace();
          }
        }
      }
    });


// 启动线程
    thread1.start();
    thread2.start();
  }
}


程序运行结果:
---线程thread1持有锁lock1
===线程thread2持有锁lock2
程序一直运行,不会退出,形成死锁

揭秘Java死锁:从根源到解决方案的实战分析_java

通过上面的程序,我们可以看出,使用synchronized同步锁不当,就会形成死锁。简单来说,程序需要同时锁住多个对象的时候,就可能导致死锁。

在上面的程序中,我们让线程thread1强制执行,就能正常输出结果了。

其他代码不变,只是修改以下部分:
// 启动线程
    thread1.start();
try {
      thread1.join(); // 让线程thread1强制执行
    } catch (InterruptedException e) {
      e.printStackTrace();
    }
    thread2.start();


程序运行结果:
---线程thread1持有锁lock1
---线程thread1申请获得锁lock1成功
===线程thread2持有锁lock2
===线程thread2申请获得锁lock1成功

揭秘Java死锁:从根源到解决方案的实战分析_死锁_02

以上的代码只是为了演示在没有发生死锁的情况下,程序的输出效果。在多线程环境下,一定要注意同步synchronized的使用情况,在synchronized嵌套使用的情况下,要注意防范死锁的发生。

02、银行转账进一步认识死锁

下面我们通过代码模拟银行的转账过程,分析转账过程形成的死锁,进一步的认识死锁。

示例1:模拟银行转账

银行账户Account类有账号和金额2个属性以及getter方法,有取款和存款2个业务方法;转账线程TransferMoney类通过transferMoney方法进行模拟转账;转账测试TransferMoneyTest类进行验证。

// 模拟银行账户
public class Account {


private int accountMoney;// 金额
private final int accountNumber; // 账号


private static final AtomicInteger SEQUENCE = new AtomicInteger();


// 创建账户的时候,初始化账号和金额
public Account() {
this.accountNumber = SEQUENCE.incrementAndGet(); // 每个账户的账号自增
this.accountMoney = 1000000; // 每个账户默认有1百万
  }


public int getAccountMoney() {
return accountMoney;
  }


public int getAccountNumber() {
return accountNumber;
  }


// 取款
public void debit(int money) throws InterruptedException {
    Thread.sleep(5);// 模拟操作时间
    accountMoney = accountMoney - money;
  }


// 存款
public void credit(int money) throws InterruptedException {
    Thread.sleep(5);// 模拟操作时间
    accountMoney = accountMoney + money;
  }
}


// 模拟转账线程
public class TransferMoney extends Thread {


private Account from; // 转出账户
private Account to; // 转入账户
private int money; // 转出的金额


public TransferMoney(Account from, Account to, int money) {
this.from = from;
this.to = to;
this.money = money;
  }


public void transferMoney() throws Exception {
    System.out
        .println("---账户 " + from.getAccountNumber() + "给账户"
            + to.getAccountNumber() + "转账" + money + "开始---转账前:账户 "
            + from.getAccountNumber() + "的金额"
            + from.getAccountMoney() + ",账户 "
            + to.getAccountNumber() + "的金额" + to.getAccountMoney());


// 银行转账的场景下,必须同时获得两个账户上的锁,才能进行转账操作
    synchronized (from) { // 转出账户加锁
      System.out.println("***账户" + from.getAccountNumber() + "获得锁");
      synchronized (to) { // 转入账户加锁
        System.out.println("****账户" + to.getAccountNumber() + "也获得锁");
if (from.getAccountMoney() >= money) { // 转出的账户里的金额够
from.debit(money);
          to.credit(money);
        } else {
          System.out.println("账户" + from.getAccountNumber()
              + "没有足够的金额可转出");
        }
      }
    }


    System.out
        .println("---账户 " + from.getAccountNumber() + "给账户"
            + to.getAccountNumber() + "转账" + money + "结束---转账后:账户 "
            + from.getAccountNumber() + "的金额"
            + from.getAccountMoney() + ",账户 "
            + to.getAccountNumber() + "的金额" + to.getAccountMoney());
  }


  @Override
public void run() {
try {
      transferMoney();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}


// 转账测试
public class TransferMoneyTest {
public static void main(String[] args) {
    Account account1 = new Account();
    Account account2 = new Account();
    Account account3 = new Account();
    Account account4 = new Account();
    Account account5 = new Account();


int money1 = 100000;
int money2 = 200000;
int money3 = 300000;
int money4 = 400000;


    TransferMoney t1 = new TransferMoney(account1, account2, money1);
    TransferMoney t2 = new TransferMoney(account2, account3, money2);
    TransferMoney t3 = new TransferMoney(account3, account4, money3);
    TransferMoney t4 = new TransferMoney(account4, account5, money4);


    t1.start();
    t2.start();
    t3.start();
    t4.start();
  }
}


程序运行结果:(运行结果有多种)
---账户 1给账户2转账100000开始---转账前:账户 1的金额1000000,账户 2的金额1000000
---账户 4给账户5转账400000开始---转账前:账户 4的金额1000000,账户 5的金额1000000
---账户 2给账户3转账200000开始---转账前:账户 2的金额1000000,账户 3的金额1000000
***账户2获得锁
---账户 3给账户4转账300000开始---转账前:账户 3的金额1000000,账户 4的金额1000000
****账户3也获得锁
***账户4获得锁
***账户1获得锁
****账户5也获得锁
***账户3获得锁
---账户 2给账户3转账200000结束---转账后:账户 2的金额800000,账户 3的金额1200000
---账户 4给账户5转账400000结束---转账后:账户 4的金额600000,账户 5的金额1400000
****账户2也获得锁
****账户4也获得锁
---账户 1给账户2转账100000结束---转账后:账户 1的金额900000,账户 2的金额900000
---账户 3给账户4转账300000结束---转账后:账户 3的金额900000,账户 4的金额900000
程序可以正常退出

从以上的程序运行结果来看,程序不会死锁,因为测试程序转账过程没有形成互相等待的情况,但是以上的程序有明细的模拟转账线程TransferMoney类有明细的问题,存在synchronized嵌套占有2个对象锁的情况,容易引起死锁。

示例2:修改转账测试TransferMoneyTest类,让其形成死锁

// 转账测试
public class TransferMoneyTest {
public static void main(String[] args) {
    Account account1 = new Account();
    Account account2 = new Account();


int money1 = 100000;
int money2 = 200000;


    TransferMoney t1 = new TransferMoney(account1, account2, money1);// 互相等待的情况发生,引发死锁
    TransferMoney t2 = new TransferMoney(account2, account1, money2);// 互相等待的情况发生,引发死锁


    t1.start();
    t2.start();
  }
}


程序运行结果:
---账户 1给账户2转账100000开始---转账前:账户 1的金额1000000,账户 2的金额1000000
---账户 2给账户1转账200000开始---转账前:账户 2的金额1000000,账户 1的金额1000000
***账户1获得锁
***账户2获得锁
程序形成死锁,僵死不退出

揭秘Java死锁:从根源到解决方案的实战分析_加锁_03

以上的程序可以看出,当存在synchronized嵌套占有2个对象锁的情况,多线程情况下就可能引起死锁,存在互相等待的情况,就会发生死锁。

03、如何避免死锁

从以上的程序可以看出,在synchronized嵌套执行的时候,已经对一个对象加锁的情况下,再去申请对另外一个对象加锁,就有可能引发死锁。

那么,解决以上死锁的思路就是,在synchronized嵌套执行的方法中,按照固定的顺序来申请对象锁,比如按照先申请账号大的账户锁,然后再申请账号小的账户锁,这样就可以避免账号对象锁的互相等待情况出现,也就避免了死锁。

因为两个账户转账,肯定要对两个账户加锁,但是我们在写程序的时候,可以按照固定的顺序来加锁,比如转账过程的最外层加锁先对账号大的加锁,再对账号小的加锁,就可以避免死锁的发生。

假设账号1给账号2转账,账号2也给账号1转账,按账号大小顺序加锁。

对于账号1给账号2转账线程1来说,先锁账号大的,那么就是首先锁住账号2,然后申请锁住账号1,然后开始执行转账。

对于账号2给账号1转账线程2来说,先锁账号大的,那么就是首先锁住账号2,然后申请锁住账号1,然后开始执行转账。在这个过程中如果线程1没有执行完毕,线程2申请锁住账号2的代码就不会执行,需要等待线程1执行完毕,释放账号2的锁对象后,线程2才可能获得账号2对象的锁去执行后续的代码。因此就避免了线程1和线程2之间形成死锁的情况。

示例:转账线程,按照固定的顺序来加锁

// 模拟转账
public class TransferMoney extends Thread {


private Account from; // 转出账户
private Account to; // 转入账户
private int money; // 转出的金额


public TransferMoney(Account from, Account to, int money) {
this.from = from;
this.to = to;
this.money = money;
  }


public void transferMoney() throws Exception {
    System.out
        .println("---账户 " + from.getAccountNumber() + "给账户"
            + to.getAccountNumber() + "转账" + money + "开始---转账前:账户 "
            + from.getAccountNumber() + "的金额"
            + from.getAccountMoney() + ",账户 "
            + to.getAccountNumber() + "的金额" + to.getAccountMoney());


// 银行转账的场景下,必须同时获得两个账户上的锁,才能进行转账操作
if (from.getAccountNumber() > to.getAccountNumber()) { // 按照账号大小上锁
      synchronized (from) {// 先锁账号大的
        System.out.println("***账户" + from.getAccountNumber() + "获得锁");
        synchronized (to) { // 再锁账号小的
          System.out.println("****账户" + to.getAccountNumber()
              + "也获得锁");
if (from.getAccountMoney() >= money) { // 转出的账户里的金额够
from.debit(money);
            to.credit(money);
          } else {
            System.out.println("账户" + from.getAccountNumber()
                + "没有足够的金额可转出");
          }
        }
      }
    } else {
      synchronized (to) { // 先锁账号大的
        System.out.println("###账户" + to.getAccountNumber() + "获得了锁");
        synchronized (from) { // 再锁账号小的
          System.out.println("####账户" + from.getAccountNumber()
              + "也获得了锁");
if (from.getAccountMoney() >= money) { // 转出的账户里的金额够
from.debit(money);
            to.credit(money);
          } else {
            System.out.println("账户" + from.getAccountNumber()
                + "没有足够的金额可转出");
          }
        }
      }
    }


    System.out
        .println("---账户 " + from.getAccountNumber() + "给账户"
            + to.getAccountNumber() + "转账" + money + "结束---转账后:账户 "
            + from.getAccountNumber() + "的金额"
            + from.getAccountMoney() + ",账户 "
            + to.getAccountNumber() + "的金额" + to.getAccountMoney());
  }


  @Override
public void run() {
try {
      transferMoney();
    } catch (Exception e) {
      e.printStackTrace();
    }
  }
}


// 转账测试
public class TransferMoneyTest {
public static void main(String[] args) {
    Account account1 = new Account();
    Account account2 = new Account();


int money1 = 100000;
int money2 = 200000;


    TransferMoney t1 = new TransferMoney(account1, account2, money1);// 互相等待的情况发生,引发死锁
    TransferMoney t2 = new TransferMoney(account2, account1, money2);// 互相等待的情况发生,引发死锁


    t1.start();
    t2.start();
  }
}


程序运行结果:
---账户 1给账户2转账100000开始---转账前:账户 1的金额1000000,账户 2的金额1000000
---账户 2给账户1转账200000开始---转账前:账户 2的金额1000000,账户 1的金额1000000
###账户2获得了锁
####账户1也获得了锁
***账户2获得锁
****账户1也获得锁
---账户 1给账户2转账100000结束---转账后:账户 1的金额900000,账户 2的金额1100000
---账户 2给账户1转账200000结束---转账后:账户 2的金额900000,账户 1的金额1100000

揭秘Java死锁:从根源到解决方案的实战分析_System_04

从以上的程序结果来看,在synchronized嵌套执行的方法中,按照固定的顺序来申请不同对象的锁是保证避免死锁的有效手段。

如果没有类似账号这样的属性,那么也可以使用对象的hashCode()的值来进行加锁顺序的判断。

if (from.hashCode() > to.hashCode()) { // 按照hashCode大小上锁
    synchronized (from) {// 先锁hashCode大的
        synchronized (to) { // 再锁hashCode小的
其他代码省略...

至此,Java中的死锁问题,我们就介绍完了,希望对你有用。