1.什么是死锁
2.如何解决死锁
死锁总结:并发下线程因为相互等待对方资源 导致永久 阻塞的现象
一、转账引发的血案:当两个账户互相较劲
让我们通过一个真实的转账场景,看看死锁是怎么悄悄发生的:
// 张三的银行账户
class Account {
private int balance;
void transfer(Account target, int amount) {
synchronized(this) { // 先锁住自己
Thread.sleep(2000); // 模拟耗时操作
synchronized(target) { // 再锁对方
this.balance -= amount;
target.balance += amount;
}
}
}
}
// 两个线程同时转账
new Thread(() -> zhangsan.transfer(lisi, 50)).start();
new Thread(() -> lisi.transfer(zhangsan, 50)).start();
当张三和李四同时给对方转账时,就像两个人在狭窄的走廊相遇——张三拿着自己的锁想要李四的锁,李四攥着自己的锁等着张三的锁。结果就是两个线程永远卡在synchronized代码块里大眼瞪小眼。
二、线上死锁急救包:三步定位问题
当监控报警响起,按照这个流程快速响应:
查心跳:用
jps -l
快速找到卡死的Java进程ID拍快照:执行
jstack 进程ID > thread_dump.log
导出线程快照看诊断:在日志中搜索"deadlock",你会看到类似这样的关键信息:
Found one Java-level deadlock:
"Thread-1":
waiting to lock monitor 0x00007f3a48005e00 (object 0x000000076b7d9c78, a Account),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f3a48006c00 (object 0x000000076b7d9c98, a Account),
which is held by "Thread-1"
这段诊断报告就像医生的X光片,清晰显示出两个线程如何互相掐住对方的脖子。根据提示的类名和行号,能快速定位到问题代码。
三、根治死锁四板斧:从根源解决问题
第一招:破除资源独占(互斥条件)
改用原子操作:对于简单数值操作,用
AtomicInteger
代替同步锁
private AtomicInteger balance = new AtomicInteger();
void safeTransfer(Account target, int amount) {
balance.getAndAdd(-amount);
target.balance.getAndAdd(amount);
}
第二招:强制资源打包(占有且等待)
银行金库法:建立全局资源管理器,一次性申请所有需要的资源
class ResourceManager {
private Set<Account> lockedAccounts = new HashSet<>();
synchronized boolean lockAccounts(Account... accounts) {
if(Collections.disjoint(lockedAccounts, Arrays.asList(accounts))) {
lockedAccounts.addAll(Arrays.asList(accounts));
return true;
}
return false;
}
synchronized void unlockAccounts(Account... accounts) {
lockedAccounts.removeAll(Arrays.asList(accounts));
}
}
第三招:设置等待时限(不可抢占)
给爱情加个期限:用
ReentrantLock
代替synchronized,设置抢锁超时
private Lock lock = new ReentrantLock();
void tryTransfer(Account target, int amount) {
try {
if(lock.tryLock(2, TimeUnit.SECONDS)) {
if(target.lock.tryLock(2, TimeUnit.SECONDS)) {
// 转账操作
}
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
target.lock.unlock();
lock.unlock();
}
}
第四招:统一资源顺序(循环等待)
学食堂排队:按照账户ID顺序加锁
void orderedTransfer(Account target, int amount) {
Account first = this.id < target.id ? this : target;
Account second = this.id < target.id ? target : this;
synchronized(first) {
synchronized(second) {
// 转账操作
}
}
}
四、防死锁的智慧:取舍的艺术
在实际开发中,安全与性能就像天平的两端。
下次当你设计转账功能时,不妨多问自己几个问题:
真的需要同时锁住两个账户吗?
能否改用版本号乐观锁?
事务超时时间设置合理吗?
有没有考虑网络抖动导致锁滞留?
评论区