Java 程序发生死锁后,进程会结束吗?答案是“不会”,占据资源、却再也不会提供服务。 初次接触 “死锁” ,在大学《操作系统原理》课程上,笔者对经典的 “哲学家进餐” 问题至今印象深刻,当时想象一些人围成一个圈等着拿筷子的场景,还觉得挺好笑的。回到本文,什么是死锁呢?这个问题相比大家都不陌生,摘录 “百度百科” 的解释是这样的: 死锁是指两个或两个以上的线程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。 操作系统中,锁是进程间通信的重要技术手段,它能解决进程间因共享资源可能出现的错误。如果使用不当或者过度使用,就可能会发生 “死锁” 问题 ,比如“哲学家饿死”这种 。 Java 应用发生死锁时,程序可能就此阻塞,程序僵而不死,服务无法正常使用,这就是死锁最直接的后果。 死锁产生的原因很多,锁顺序就是一种。它指一个线程同时需要多把锁时,由于锁的请求顺序不同,而导致的死锁问题。一起来看一下《Java 并发编程实践》一书中,锁顺序死锁的案例。 LeftRightDeadLock 类定义了两个锁成员变量和两个方法,分别以相反的顺序请求两把锁: 死锁风险分析:当一个线程调用 leftRight 方法获取了 left 锁后,另一个线程同时调用了 rightLeft 方法获取了 right 锁。此时他们都会因等待另一个锁而阻塞,程序僵死。 两个线程的操作交错进行时,对某一锁的请求顺序也是交错的,就容易因锁顺序而引发死锁问题。 破解方法:如果方法需要同时请求多把锁,应该统一这些锁的请求顺序,保证顺序一致。 上述加锁是对针对固定成员变量的,控制加锁顺序比较容易。还一种场景,在方法中以参数作为请求锁,参数不同、锁也不同,顺序也无法预测,这就是动态的锁顺序死锁的诱因。 《Java 并发编程实践》一书中给出的代码片段,它描述的场景是将资金从一个账户转入另一个账户,在开始转账之前,先要同时获取这两个账户对象的锁,确保通过原子操作来更新两个账户中的余额。 抽象一个代表金额的 Amount 类,一个 Account 账户类,一个转账方法,设计类图。 按该类图结构编写代码如下: 子类 DallarAmount代码: Account 类,提供账户的借、贷方法。 AccountHelper 转账功能提供者,在执行转账之前,先获取两个账户的锁: 上述代码看似没有问题,却存在死锁的风险。代码中先对 from 加锁,再对 to 账户加锁,貌似顺序固定,但事实上锁的顺序是动态的,它取决于参数传递的顺序,而参数的顺序又由外部输入决定。 编写测试代码,同时启动四个线程交替执行 transferMoney 方法,参数顺序相反。 反复执行该方法,前三次恰巧能够正常运行,在第四次执行的时候,终于遭遇了死锁,只有两个线程顺利结束,另外两个线程陷入死锁状态,这个程序一直结束不了。 因无法控制参数的顺序,所以必须定义一种固定的锁顺序,保证程序的锁顺序不受参数的影响。书中给出的解决办法是:比较对象的 hashCode,以此固定锁顺序,当两个对象拥有相同的哈希值时,再使用加时赛锁固定加锁的顺序。 修正 AccountHelper 代码如下: 上述例子清晰地解释了过度使用锁的风险,例子可能有些极端。笔者不禁在想,真正开发过程中,谁会写这样的代码呢?为什么要同时获取多个锁呢? 此外,在解决动态锁顺序死锁问题中,额外使用加时赛锁,完全可以只用这一个锁来保证转账操作的原子性呀。不过,笔者不得不承认,这段代码扩宽了个人的眼界和编程思维:原来还存在这样的问题,还可以用这样的解决办法。 验证代码遭遇死锁时,Eclipse 的控制台一直显示红色运行状态,笔者起初觉得挺好玩的,随即又意识到了死锁对应用程序的威胁。如果我们投产的应用遭遇这种情况怎么办呢?而且死锁是随机发生的,第二个例子要运行多次才能出现死锁,这也增加了问题排查的难度。 这个章节看完,那个卡住的测试程序就是 “死锁” 最直观的解释。引言
锁顺序死锁概述
案例回顾
public class LeftRightDeadLock {
private final Object left = new Object();
private final Object right = new Object();
public void leftRight(){
synchronized (left) {
synchronized (right) {
//doSomethoing();
}
}
}
public void rightLeft(){
synchronized (right) {
synchronized (left) {
//doSomethoing();
}
}
}
}
死锁分析
动态的锁顺序死锁
抽象测试类
编写实现代码
/**
* 顶层金额抽象类:包含币种和余额两个属性
* @author bh
*/
public abstract class Amount implements Comparable<Amount>{
public abstract BigDecimal getBalance();
public abstract void setBalance(BigDecimal balance);
public abstract Currency getCurrency();
@Override
public int compareTo(Amount o) {
if(o==null){
throw new NullPointerException("null arg.");
}
if(this.getBalance()==null||o.getBalance()==null){
throw new NullPointerException("null arg.");
}
return this.getBalance().compareTo(o.getBalance());
}
}
/**
* 美元类
* @author bh
*/
public class DollarAmount extends Amount{
private BigDecimal balance;
private Currency currency = Currency.getInstance(Locale.US);
public DollarAmount(BigDecimal balance){
this.balance = balance;
}
public BigDecimal getBalance() {
return balance;
}
public void setAmount(BigDecimal amount) {
this.balance = amount;
}
public Currency getCurrency() {
return currency;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
/**
* 账户类:包括金额,借贷及获取余额方法
* @author bh
*/
public class Account {
private String id;
private Amount balance;
public Account(Amount amount,String id){
this.balance = amount;
this.id = id;
}
public Amount getBalance() {
return balance;
}
public void setBalance(Amount balance) {
this.balance = balance;
}
public void debit(Amount amount){
if(this.balance==null||amount==null||amount.getBalance()==null){
return;
}
System.out.println(id+" 支出金额"+amount.getBalance());
//修正账户余额:本账户减去借方金额
BigDecimal current = this.balance.getBalance();
BigDecimal now = current.subtract(amount.getBalance());
this.balance.setBalance(now);
}
public void credit(Amount amount){
if(this.balance==null||amount==null||amount.getBalance()==null){
return;
}
System.out.println(id+" 收入金额"+amount.getBalance());
//修正账户余额:本账户加贷方金额
BigDecimal current = this.balance.getBalance();
BigDecimal now = current.add(amount.getBalance());
this.balance.setBalance(now);
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
}
/**
* 同时获取两个账户的锁
* @author bh
*/
public class AccountHelper {
public void transferMoney(final Account fromAcct,
final Account toAcct,
final Amount amount){
//参数校验
if(fromAcct==null||toAcct==null||amount==null){
throw new IllegalArgumentException("null arg.");
}
//余额校验
if(fromAcct.getBalance().compareTo(amount)<0){
throw new IllegalArgumentException(fromAcct.getId()+"账户余额不足");
}
synchronized (fromAcct) {
synchronized (toAcct) {
this.transfer(fromAcct, toAcct, amount);
}
}
}
//transfer对两个账户的操作必须是原子的完成
private void transfer(final Account fromAcct,
final Account toAcct,
final Amount amount){
System.out.println("Thread "+Thread.currentThread().getName()+" do transfer.");
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
编写测试类
代码如下:public class MainTest {
public static void main(String[] args) {
Amount amFromAcc = new DollarAmount(new BigDecimal(2000));
Amount amToAcc = new DollarAmount(new BigDecimal(1000));
final AccountHelper h = new AccountHelper();
final Account fromAcc = new Account(amFromAcc,"zhang_3");
final Account toAcc = new Account(amToAcc,"wang_5");
final Amount amToTran = new DollarAmount(new BigDecimal(1));
Thread t1 = new Thread(new Runnable(){
@Override
public void run() {
h.transferMoney(fromAcc, toAcc, amToTran);
}
});
Thread t4 = new Thread(new Runnable(){
@Override
public void run() {
h.transferMoney(fromAcc, toAcc, amToTran);
}
});
Thread t2 = new Thread(new Runnable(){
@Override
public void run() {
h.transferMoney(toAcc, fromAcc, amToTran);
}
});
Thread t3 = new Thread(new Runnable(){
@Override
public void run() {
h.transferMoney(toAcc, fromAcc, amToTran);
}
});
t1.start();
t2.start();
t3.start();
t4.start();
}
}
结果如下:破解之道
/**
* 根据某种规则设置锁的获取顺序,避免锁顺序死锁问题
* @author bh
*
*/
public class AccountHelper {
//加时赛锁
private final Object tieLock = new Object();
public void transferMoney(final Account fromAcct,
final Account toAcct,
final Amount amount){
//参数校验
if(fromAcct==null||toAcct==null||amount==null){
throw new IllegalArgumentException("null arg.");
}
//余额校验
if(fromAcct.getBalance().compareTo(amount)<0){
throw new IllegalArgumentException(fromAcct.getId()+"账户余额不足");
}
//根据对象的hash值随机设置加锁顺序
int fromHash = System.identityHashCode(fromAcct);
int toHash = System.identityHashCode(toAcct);
if(fromHash<toHash){
synchronized (fromAcct) {
synchronized (toAcct) {
this.transfer(fromAcct, toAcct, amount);
}
}
}else if(fromHash>toHash){
synchronized (toAcct) {
synchronized (fromAcct) {
this.transfer(fromAcct, toAcct, amount);
}
}
}else{
//碰巧相对时,先获取加时赛锁
synchronized (tieLock) {
synchronized (fromAcct) {
synchronized (toAcct) {
this.transfer(fromAcct, toAcct, amount);
}
}
}
}
}
//transfer对两个账户的操作必须是原子的完成
private void transfer(final Account fromAcct,
final Account toAcct,
final Amount amount){
System.out.println("Thread "+Thread.currentThread().getName()+" do transfer.");
fromAcct.debit(amount);
toAcct.credit(amount);
}
}
实践启示录