前言
本例子不是实际的生产模型,只是为了更好研究死锁而举的例子
需求
james 转账给 jay 20 元,需要保证转账过程原子性操作
能加锁的前提
每个对象都有一个monitor,用于维护自身的锁状态,换句话说,所有对象都可以作为一把锁
每个用户的monitor就可以作为一把锁,并且这把锁是互斥的,暂且称作读写锁
用加锁实现原子性
- 锁规则
每个用户都持有账户的一把读写锁,可以自己持有,也可以被别人拿到 - 转账中的加锁
转出用户需要扣住自己账户的读写锁,并且拿到转入用户的读写锁,也就是转出方需要拿到两把锁 - 加锁的目的
同一笔交易,只能同时成功或者同时失败
james 扣去的钱 必须等于 jay 增加的钱
多个交易互相独立
代码演示
public class DeadLockByTransferMoney implements Runnable{
private static class User {
String username;
Integer money;
public User(String username, Integer money) {
this.username = username;
this.money = money;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public void transfer(User user, Integer money) {
try{
this.money = this.money - money;
user.money = user.money + money;
} catch (自定义异常) {
// 回滚逻辑
}
System.out.println("转账发生");
}
}
// 转账发起者
private final User fromLock;
// 转账接收者
private final User toLock;
public DeadLockByTransferMoney(User fromLock, User toLock) {
this.fromLock = fromLock;
this.toLock = toLock;
}
/**
* 保证原子性的锁操作
* 也证明了锁的可重入性质
*/
@Override
public void run() {
synchronized (fromLock) {
// 模拟网络开销 , 一旦有网络开销, 发生死锁的概率极高
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (toLock) {
toLock.transfer(toLock, 20);
}
}
}
public static void main(String[] args) throws InterruptedException {
User jay = new User("jay", 500);
User james = new User("james", 500);
DeadLockByTransferMoney r1 = new DeadLockByTransferMoney(jay, james);
DeadLockByTransferMoney r2 = new DeadLockByTransferMoney(james, jay);
Thread t1 = new Thread(r1);
Thread t2 = new Thread(r2);
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(jay.getMoney() + ":::::" + james.getMoney());
}
}
死锁分析
两个线程都扣住了自己的锁,为了建立交易的连接,互相等待对方释放锁,造成死锁
发生死锁的总结
- 加锁是实现原子性的途径之一
- 锁的操作顺序是导致死锁的原因之一
- synchronize锁是可重入的
检测死锁
方案一
-
jdk 自带命令行工具jstack
jdk安装目录/bin/jstack.exe 文件可以打印出栈信息
-
获取当前线程的系统pid
-
cmd 命令,键入/bin 目录下
jstack -F (资源管理器重的pid)
-
查看输出
- 死锁信息
- 死锁信息
方案二
- 工具类获取死锁信息
// 获取检测死锁的工具类
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int k = 0; k < deadlockedThreads.length; k++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[k]);
System.out.println("发生线程死锁了, 相关线程" + threadInfo.getThreadName());
isHasDeadLock = true;
}
}
if (isHasDeadLock) {
break;
}
- 拓展 ,模拟多用户同时转账,也能检测死锁
/**
* @Author james
* @Description
*
* 工具类及时反馈是否有死锁
* @see ManagementFactory#getThreadMXBean 获取工具类
* @see ThreadMXBean#findDeadlockedThreads() 获取所有死锁的线程id
* @see ThreadMXBean#getThreadInfo 根据 id 获取线程信息
*
* 调整加锁的顺序就能避免死锁的发生
*
* @Date 2019/11/22
*/
public class CheckDeadLockAndFix implements Runnable{
private final static int NUM_USER_COUNT = 500;
private final static int NUM_THREAD_COUNT = 100000;
private final static int MONEY_PER_TRANSFER = 1;
private static class User {
String username;
Integer money;
public User(String username, Integer money) {
this.username = username;
this.money = money;
}
public User(Integer money) {
this.money = money;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getMoney() {
return money;
}
public void setMoney(Integer money) {
this.money = money;
}
public void transfer(User user, Integer money) {
this.money = this.money - money;
user.money = user.money + money;
// System.out.println("转账发生, 线程: " + Thread.currentThread().getName());
}
}
// 转账发起者
private final User fromLock;
// 转账接收者
private final User toLock;
public CheckDeadLockAndFix(User fromLock, User toLock) {
this.fromLock = fromLock;
this.toLock = toLock;
}
/**
* 保证原子性的锁操作
* 也证明了锁的可重入性质
*/
@Override
public void run() {
int fromLockHash = System.identityHashCode(fromLock);
int toLockHash = System.identityHashCode(toLock);
if (fromLockHash < toLockHash) {
synchronized (fromLock) {
// 网络开销极低的情况
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (toLock) {
toLock.transfer(toLock, MONEY_PER_TRANSFER);
}
}
} else {
synchronized (toLock) {
// 网络开销极低的情况
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (fromLock) {
toLock.transfer(toLock, MONEY_PER_TRANSFER);
}
}
}
}
public static void main(String[] args) throws InterruptedException {
Random random = new Random();
User[] users = new User[NUM_USER_COUNT];
for (int i = 0; i < NUM_USER_COUNT; i++) {
users[i]= new User(500);
}
boolean isHasDeadLock = false;
for (int i = 0; i < NUM_THREAD_COUNT; i++) {
CheckDeadLockAndFix r = new CheckDeadLockAndFix(users[random.nextInt(NUM_USER_COUNT)], users[random.nextInt(NUM_USER_COUNT)]);
Thread thread = new Thread(r);
thread.start();
// 获取检测死锁的工具类
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();
if (deadlockedThreads != null && deadlockedThreads.length > 0) {
for (int k = 0; k < deadlockedThreads.length; k++) {
ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[k]);
System.out.println("发生线程死锁了, 相关线程" + threadInfo.getThreadName());
isHasDeadLock = true;
}
}
if (isHasDeadLock) {
break;
}
}
}
}
代码层面解决死锁问题
思路
避免锁的互相等待
拆分思路
-
按顺序拿锁
让同时获得两个锁的线程按相同的顺序去竞争锁,
也就是 线程 A 和 B 只能首先竞争 锁1,
若线程 A 先拿到锁1, B 只能等待 A 线程释放锁才能继续执行,而不是去拿锁2 -
确定顺序 – 使用哈希值
System.identityHashCode(lock);
每个锁的哈希值是唯一的,大小关系也就确定了,完整的代码:if (fromLockHash < toLockHash) { synchronized (fromLock) { // 网络开销极低的情况 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (toLock) { toLock.transfer(toLock, MONEY_PER_TRANSFER); } } } else if (fromLockHash > toLockHash){ synchronized (toLock) { // 网络开销极低的情况 try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (fromLock) { toLock.transfer(toLock, MONEY_PER_TRANSFER); } } }
-
解决哈希冲突 – 方案一:加多一把锁
private final static Object lock = new Object();
else { // 哈希冲突的情况下,相同哈希值的线程串行完成即可,先竞争到锁的先执行 synchronized (lock) { synchronized (fromLock) { synchronized (toLock) { toLock.transfer(toLock, MONEY_PER_TRANSFER); } } } }
-
解决哈希冲突 – 方案二:使用数据库数据的主键来完成