本文主要讲述死锁的一个经典案例—银行转账问题,并对该问题进行定位、修复。
1. 问题说明
当账户A对账户B进行转账时,
- 首先需要获取到
两把锁
:账户A和账户B的锁。 - 获取两把锁成功,且余额大于0,则扣除转出人的余额,并增加收款人的余额,而且这些操作都是在一个
原子操作
中 - 获取锁的
顺序相反
导致死锁,即线程1获取到账户A的锁,然后请求账户B的锁,线程2已经获取到账户B的锁,然后请求A的锁,结果两者互相等待对方的锁,造成死锁。
2. 代码演示
public class TransferMoney implements Runnable {
Integer flag = 1;
static Account a = new Account(1000);
static Account b = new Account(1000);
//主函数
public static void main(String[] args) throws InterruptedException {
TransferMoney t1 = new TransferMoney();
TransferMoney t2 = new TransferMoney();
t1.flag = 1;
t1.flag = 0;
Thread thread1 = new Thread(t1);
Thread thread2 = new Thread(t2);
thread1.start();
thread2.start();
thread1.join();
thread1.join();
System.out.println("a的余额为:"+a.balance);
System.out.println("a的余额为:"+b.balance);
}
@Override
public void run() {
if (flag == 1) {
transferMoney(a, b, 500);
}
if (flag == 0) {
transferMoney(b, a, 500);
}
}
// 转账
private void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
System.out.println(Thread.currentThread().getName()+"获取到第一把锁");
//加入线程睡眠500ms
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (to) {
System.out.println(Thread.currentThread().getName()+"获取到第二把锁");
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账" + i + "元");
}
}
}
static class Account {
//余额
int balance;
public Account(int balance) {
this.balance = balance;
}
}
}
打印结果:
启动程序,发现打印结果输出几句之后就不再输出,而且程序也未停止
,这就发生了死锁。
- 当线程 thread1 执行transferMoney()方法的时候,他拿到from锁,也就是里面的类成员变量a;
- 经过 500ms,这个期间线程thread2进来执行transferMoney()方法,拿到from锁,也就是类成员变量b
- 接下来500ms之后线程thread1继续执行,但是他要拿到to锁,也就是他的成员变量b,但是已经被线程thread1拿过去作为他的from锁了
- 线程thread2接下来拿他的to锁,也就是成员变量a,但是他已经被线程thread1拿着了,因为成员变量a是线程1的from锁;
所以就进入了死锁的情况。
下面模拟多人转账,多个线程陷入死锁:
public class MultiTransferMoney {
private static final int NUM_ACCOUNTS = 5000;//账号数
private static final int NUM_MONEY = 1000;//余额
private static final int NUM_ITERATIONS = 10000000;//转账次数
private static final int NUM_THREADS = 20;//转账人数
public static void main(String[] args) {
Random random = new Random();
TransferMoney.Account[] accounts = new TransferMoney.Account[NUM_ACCOUNTS];
//初始化
for (int i = 0; i < accounts.length; i++) {
accounts[i] = new TransferMoney.Account(NUM_MONEY);
}
//转账类
class TransferThread extends Thread{
@Override
public void run() {
for (int i = 0; i < NUM_ITERATIONS; i++) {
//随机下标
int fromAcct = random.nextInt(NUM_ACCOUNTS);
int toAcct = random.nextInt(NUM_ACCOUNTS);
int amount = random.nextInt(NUM_ACCOUNTS);
transferMoney(accounts[fromAcct], accounts[toAcct],amount);
}
System.out.println("程序结束!!!!");
}
private void transferMoney(Account from, Account to, int amount) {
synchronized (from) {
synchronized (to) {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance += amount;
System.out.println("成功转账" + i + "元");
}
}
}
}
//线程数
for (int i = 0; i < NUM_THREADS; i++) {
new TransferThread().start();
}
}
}
打印结果:
在多人同时转账的情况下,虽然很人数很多,发生死锁的概率变小,但是只要发生死锁的风险存在,随着时间的推移,就一定会导致程序陷入死锁(墨菲定律)。
3. 如何定位死锁的位置(以两人转账的代码为例)
(1)jstack 命令
通过使用java自带的jstack命令
,来查找我们项目中的死锁问题
## 需要首先获取程序的进程 pid
jps
## 然后在 终端界面执行如下命令
jstack 8359 #javahome下的jastack命令 进程的pid
执行结果图:
可以清晰地看到 Thread-1 拿到了锁 <0x000000076adae688> ,正在等待 <0x000000076adae678>,而 Thread-0 拿到了锁 <0x000000076adae678>,正在等待 <0x000000076adae688>,于是两者互相等待,造成死锁。
(2)ThreadMXBean代码
/**
* 通过 ThreadMXBean 检测死锁
*/
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
public class ThreadMXBeanDetection implements Runnable{
int flag = 1;//标记位
static Object lock1 = new Object();
static Object lock2 = new Object();
public static void main(String[] args) throws InterruptedException {
ThreadMXBeanDetection r1 = new ThreadMXBeanDetection();
ThreadMXBeanDetection r2 = new ThreadMXBeanDetection();
r1.flag=1;
r2.flag=0;
Thread thread1 = new Thread(r1);
Thread thread2 = new Thread(r2);
thread1.start();
thread2.start();
Thread.sleep(1000);
//得到实例
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
//发现死锁
if (deadlockedThreads != null && deadlockedThreads.length>0){
//迭代
for (long item : deadlockedThreads) {
//获取线程信息
ThreadInfo threadInfo = threadMXBean.getThreadInfo(item);
//获取死锁线程的名字
System.out.println("发现死锁:"+threadInfo.getThreadName());
}
}
}
@Override
public void run() {
System.out.println("flag= " + flag);
if (flag == 1) {
synchronized (lock1){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2){
System.out.println(flag);
}
}
}
if (flag == 0) {
synchronized (lock2){
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1){
System.out.println(flag);
}
}
}
}
}
打印结果:
如上图所示,ThreadMXBean 可以检测死锁,如果我们检测到了之后,就可以编写对应的逻辑,比如重启线程、通知告警系统、发消息提醒运维人员等。
4. 死锁修复(以两人转账的代码为例)
本公众号的《死锁细究》这篇文章提到多种死锁修复的方案。
本文使用死锁避免策略
:把获取两把锁的规则改一下,原来的规则是先获取转出人的锁,再获取收款人的锁,这就会造成两个转出人都在等对方释放锁的情况。
现在我们把规则改成:所有的交易都先获取hash
值更小的锁,获取到了hash
小的锁才能获取hash
大的锁,这就避免了环形的死锁,假如说这两个锁的大小一样,这时候就需要一把额外的锁来进行交易流程的控制,相当于一场“加时赛”。
在实际业务开发中可以使用主键,因为主键是唯一的,可以用主键来决定获取锁的顺序。
代码展示:
public class TransferMoney implements Runnable {
private int flag = 1;
private static Account a = new Account(500);
private static Account b = new Account(500);
private static Object lock = new Object();
public static void main(String[] args) throws InterruptedException {
TransferMoney r1 = new TransferMoney();
TransferMoney r2 = new TransferMoney();
r1.flag = 1;
r2.flag = 0;
//定义两个线程,flag = 1和0分别模拟a和b
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("a的余额" + a.balance);
System.out.println("b的余额" + b.balance);
}
@Override
public void run() {
if (flag == 1) {
//a转账给b
transferMoney(a, b, 200);
}
if (flag == 0) {
//b转账给a
transferMoney(b, a, 200);
}
}
/**
* 转账方法
*
* @param from 转出人
* @param to 收款人
* @param amount 转账金额
*/
public static void transferMoney(Account from, Account to, int amount) {
/**
* 辅助类
*/
class Helper {
public void transfer() {
if (from.balance - amount < 0) {
System.out.println("余额不足,转账失败。");
return;
}
from.balance -= amount;
to.balance = to.balance + amount;
System.out.println("成功转账" + amount + "元");
}
}
//使用类的hash值来帮助排序
int fromHash = System.identityHashCode(from);
int toHash = System.identityHashCode(to);
// 通过System.identityHashCode(XXX)去获取对象的hash值,并进行比较他们的hash值来进行比较来决定拿锁的顺序
if (fromHash < toHash) {
synchronized (from) {
System.out.println(Thread.currentThread().getName() + "获取到第一把锁");
synchronized (to) {
System.out.println(Thread.currentThread().getName() + "获取到第二把锁");
new Helper().transfer();
}
}
} else if (fromHash > toHash) {
synchronized (to) {
System.out.println(Thread.currentThread().getName() + "获取到第一把锁");
synchronized (from) {
System.out.println(Thread.currentThread().getName() + "获取到第二把锁");
new Helper().transfer();
}
}
} else {
//发生hash碰撞时,可以增加第三把锁来进行控制,类似“加时赛”
synchronized (lock) {
synchronized (to) {
System.out.println(Thread.currentThread().getName() + "获取到第一把锁");
synchronized (from) {
System.out.println(Thread.currentThread().getName() + "获取到第二把锁");
new Helper().transfer();
}
}
}
}
}
/**
* 收款账户
*/
static class Account {
public Account(int balance) {
this.balance = balance;
}
int balance;
}
}
打印结果:
程序不再死锁!!!