1、简介
在遇到线程安全问题的时候,我们会使用加锁机制来确保线程安全,但如果过度地使用加锁,则可能导致锁顺序死锁(Lock-Ordering Deadlock)。或者有的场景我们使用线程池和信号量来限制资源的使用,但这些被限制的行为可能会导致资源死锁(Resource DeadLock)。这是来自Java并发必读佳作 Java Concurrency in Practice 关于活跃性危险中的描述。
我们知道Java应用程序不像数据库服务器,能够检测一组事务中死锁的发生,进而选择一个事务去执行;在Java程序中如果遇到死锁将会是一个非常严重的问题,它轻则导致程序响应时间变长,系统吞吐量变小;重则导致应用中的某一个功能直接失去响应能力无法提供服务,这些后果都是不堪设想的。因此我们应该及时发现和规避这些问题。
2、死锁产生的条件
死锁的产生有四个必要的条件
- 互斥使用,即当资源被一个线程占用时,别的线程不能使用
- 不可抢占,资源请求者不能强制从资源占有者手中抢夺资源,资源只能由占有者主动释放
- 请求和保持,当资源请求者在请求其他资源的同时保持对原因资源的占有
- 循环等待,多个线程存在环路的锁依赖关系而永远等待下去,例如T1占有T2的资源,T2占有T3的资源,T3占有T1的资源,这种情况可能会形成一个等待环路
对于死锁产生的四个条件只要能破坏其中一条即可让死锁消失,但是条件一是基础,不能被破坏。
3、各种死锁的介绍
3.1 锁顺序死锁
先举一个顺序死锁的例子。
构建一个LeftRightDeadLock类,这个类中有两个共享资源right,left我们通过对这两个共享资源加锁的方式来控制程序的执行流程,但是这个示例在高并发的场景下存在顺序死锁的风险。
如下示意图存在死锁风险
LeftRightDeadLock示例代码:
package com.liziba.dl;
/**
* <p>
* 顺序死锁
* </p>
*
* @Author: Liziba
*/
public class LeftRightDeadLock {
private final Object right = new Object();
private final Object left = new Object();
/**
* 加锁顺序从left -> right
*/
public void leftToRight() {
synchronized (left) {
synchronized (right) {
System.out.println(Thread.currentThread().getName() + " left -> right lock.");
}
}
}
/**
* 加锁顺序right -> left
*/
public void rightToLeft() {
synchronized (right) {
synchronized (left) {
System.out.println(Thread.currentThread().getName() + " right -> left lock.");
}
}
}
}
测试代码,通过创建多个线程,并发执行上面的LeftRightDeadLock
public static void main(String[] args) {
LeftRightDeadLock lrDeadLock = new LeftRightDeadLock();
for (int i = 0; i < 10; i++) {
new Thread(() -> {
// 为了更好的演示死锁,将两个方法的调用放置到同一个线程中执行
lrDeadLock.leftToRight();
lrDeadLock.rightToLeft();
}, "ThreadA-"+i).start();
}
}
可以看到如下的运行结果,程序并未结束,但是也无法继续运行。
产生这种情况的原因,是不同的线程通过不同顺序去获取相同的锁;比如线程1获取锁的顺序是left -> right,而线程2获取锁的顺序是right -> left,在某种情况下会发生死锁。拿上面的案例分析,我们通过Java自带的jps和jstack工具查看java进程ID和线程相关信息。
jps查看LeftRightDeadLock的进程id为17968
jstack查看进程中的线程信息,线程信息比较多,我把重要的复制出来,如下的图中能很明显的看到产生了死锁。
这里省略了很多线程当前状态信息
解决顺序死锁的办法其实就是保证所有线程以相同的顺序获取锁就行。
3.2 动态锁顺序死锁
3.2.1 动态锁顺序死锁的产生与示例
动态锁顺序死锁与上面的锁顺序死锁其实最本质的区别,就在于动态锁顺序死锁锁住的资源无法确定或者会发生改变。
比如说银行转账业务中,账户A向账户B转账,账户B也可以向账户A转账,这种情况下如果加锁的方式不正确就会发生死锁,比如如下代码:
定义简单的账户类Account
package com.liziba.dl;
import java.math.BigDecimal;
/**
* <p>
* 账户类
* </p>
*
* @Author: Liziba
*/
public class Account {
/** 账户 */
public String number;
/** 余额 */
public BigDecimal balance;
public Account(String number, BigDecimal balance) {
this.number = number;
this.balance = balance;
}
public void setNumber(String number) {
this.number = number;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
}
定义转账类TransferMoney,其中有transferMoney()方法用于accountFrom账户向accountTo转账金额amt:
package com.liziba.dl;
import java.math.BigDecimal;
/**
* <p>
* 转账类
* </p>
*
* @Author: Liziba
*/
public class TransferMoney {
/**
* 转账方法
*
* @param accountFrom 转账方
* @param accountTo 接收方
* @param amt 转账金额
* @throws Exception
*/
public static void transferMoney(Account accountFrom,
Account accountTo,