造成死锁的原因:
1.在一个代码块中同时获得多个锁,导致多个线程同时执行代码时,获取锁之间相互依赖,从而导致锁“抱死”。例如,t1线程首先获得A锁,再获得B锁,t2线程先获得B锁,再获得A锁,当t1获得A锁的同时,t2获得了B锁的使用权,此时t1无法获取B锁,t2也无法获得A锁,线程一直等待,这就叫锁“抱死”。
2.在同步代码块中调用了外部的同步方法(常见)
下面我将结合实际情况分析账户转账时,发生死锁的代码示例。
首先建一个Account类,包括账户ID,账户名称,账户余额。
public class Account {
private int id;
private String accName;
private double balance = 100;
public Account(String name) {
this.accName = name;
}
public Account(int id,String name) {
this.id = id;
this.accName = name;
}
public int compareTo(double money){
if(balance > money){
return 1;
}else if(balance == money){
return 0;
}else{
return -1;
}
}
public void in(double money){
balance += money;
System.out.println(accName+"中的余额:"+balance);
}
public void out(double money){
balance -= money;
System.out.println(accName+"中的余额:"+balance);
}
/**
* @return the accName
*/
public String getAccName() {
return accName;
}
/**
* @param accName the accName to set
*/
public void setAccName(String accName) {
this.accName = accName;
}
/**
* @return the balance
*/
public double getBalance() {
return balance;
}
/**
* @param balance the balance to set
*/
public void setBalance(double balance) {
this.balance = balance;
}
/**
* @return the id
*/
public int getId() {
return id;
}
/**
* @param id the id to set
*/
public void setId(int id) {
this.id = id;
}
}
实现转账的方法:
/**
* 造成死锁的示例:通过synchronized同步代码块
* @param A
* @param B
* @param money
* @throws Exception
*/
public static void transferMoney(Account A,Account B,double money) throws Exception{
if(A.compareTo(money) < 0){
throw new Exception("余额不足!");
}
synchronized(A){
System.out.println(Thread.currentThread().getName()+"获得了"+ A.getAccName() +"的锁");
Thread.sleep(2000);//模拟2个线程同时进行,等待获得B锁,这样就造成了死锁
synchronized (B) {
System.out.println("获得了"+ B.getAccName() +"的锁");
A.out(money);
B.in(money);
}
}
}
测试方法:
public static void main(String[] args) {
final Account[] accounts = new Account[2];
accounts[0] = new Account(1,"zhangsan");
accounts[1] = new Account(2,"lisi");
new Thread(new Runnable() {
@Override
public void run() {
try {
transferMoney(accounts[0], accounts[1], 10);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
transferMoney(accounts[1], accounts[0], 10);
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
测试结果:
问题的原因开头已经提到了,zhangsan账户向lis账户转账首先获取了zhangsan的对象锁,lis同时向zhangsan转账获得了lisi的对象锁,此时zhangsan无法获得zhangsan的对象锁,lisi无法获取到zhangsan的对象锁,从而导致死锁。那么问题原因知道了,如何解决呢?
通过hashcode给对象锁排序,使得每次获取锁的顺序是一致的。这里对象的hashcode一般情况下是不一样的,如果为了保险起见可以使用对象的唯一ID进行排序也是可以的。
/**方法一
* 解决死锁的方法:根据hashcode给锁排序,按照一个指定的顺序加锁
* @param A
* @param B
* @param money
* @throws Exception
*/
public static void solveDeadLock1(Account A,Account B,double money) throws Exception{
if(A.compareTo(money) < 0){
throw new Exception("余额不足!");
}
if(A.hashCode()
执行结果:
转账方法修改为同步方法:
public synchronized void in(double money){
balance += money;
System.out.println(accName+"中的余额:"+balance);
}
/**
* 造成死锁的示例:通过synchronized同步方法
* @param A
* @param B
* @param money
* @throws Exception
*/
public static void transferMoney1(Account A,Account B,double money) throws Exception{
if(A.compareTo(money) < 0){
throw new Exception("余额不足!");
}
synchronized(A){
System.out.println(Thread.currentThread().getName()+"获得了"+ A.getAccName() +"的锁");
Thread.sleep(2000);//模拟2个线程同时进行,
A.out(money);
B.in(money);
}
}
测试结果:
原因和第一种是一样的,只是将同步代码块换成了同步方法,我们会发现zhangsan和lisi的账户中都少了10元,所以请记住一点:避免在同步代码块中调用外部的同步方法。
解决方法是尽量让原子操作的范围最小化。
实际上在转账过程中变量就是余额balance,而且balance是在in和out方法里计算的,所以将转入in方法和转出out方法都写为同步方法。
public synchronized void in(double money){
balance += money;
System.out.println(accName+"中的余额:"+balance);
}
public synchronized void out(double money){
balance -= money;
System.out.println(accName+"中的余额:"+balance);
}
/**
* 解决死锁的方法:将原子操作的范围最小化,避免在同步代码块中调用外部的同步方法
* @param A
* @param B
* @param money
* @throws Exception
*/
public static void solveDeadLock3(Account A,Account B,double money) throws Exception{
if(A.compareTo(money) < 0){
throw new Exception("余额不足!");
}
A.out(money);
B.in(money);
}
测试结果:
总结
1.避免在同步代码块中调用外部的同步方法。
2.在嵌套多层synchronized同块中,对锁进行排序,使得每次获取锁的顺序是一致的。