并发转账时死锁问题的解决思路
引言:粗粒度锁的性能瓶颈
在多线程环境下处理银行转账业务时,最简单的方案是使用一把全局锁将所有账户的转账操作串行化。这种方式虽然能够保证数据一致性,但会带来明显的性能问题:所有转账操作都必须排队执行,即使是完全独立的账户之间的转账也无法并行处理。例如,账户A转账给账户B的操作,与账户C转账给账户D的操作,在现实场景中完全可以并行执行,却被强制变成了串行操作,严重降低了系统吞吐量。
细粒度锁的引入与死锁风险
为了提升并发性能,我们可以采用细粒度锁策略:在转账方法内部,先锁定转出账户,再锁定转入账户,只有两个锁都获取成功时才执行转账逻辑。这种方案允许多个不相关的转账操作并行执行。
class Wallet {
private int amount;
public void sendMoney(Wallet target, int value) {
synchronized(this) {
synchronized(target) {
if (this.amount > value) {
this.amount -= value;
target.amount += value;
}
}
}
}
}
然而,细粒度锁引入了新的风险:当两个线程同时进行转账操作时,可能出现循环等待的情况。线程1持有账户A的锁并等待账户B的锁,而线程2持有账户B的锁并等待账户A的锁,这种相互等待的状态就是死锁。
死锁形成的四个必要条件
死锁的产生需要满足以下四个必要条件:
- 互斥条件:资源一次只能被一个线程持有
- 占有并等待:线程在持有已有资源的同时,等待获取其他资源 不可抢占:已经分配给线程的资源,不能被强制夺取,只能由线程主动释放
- 循环等待:存在一种线程资源的环形等待链,每个线程都在等待下一个线程持有的资源
破坏死锁条件的三大策略
策略一:一次性申请所有资源
针对"占有并等待"条件,可以引入资源管理员角色,由它统一管理所有资源的分配。转账前向管理员申请所需的所有账户锁,只有全部申请成功才开始执行转账操作,完成后释放所有锁。
class ResourceManager {
private final Set<Object> lockedResources = new HashSet<>();
public synchronized boolean acquire(Object from, Object to) {
if (lockedResources.contains(from) || lockedResources.contains(to)) {
return false;
}
lockedResources.add(from);
lockedResources.add(to);
return true;
}
public synchronized void release(Object from, Object to) {
lockedResources.remove(from);
lockedResources.remove(to);
}
}
class Wallet {
private static ResourceManager manager;
private int amount;
public void sendMoney(Wallet target, int value) {
while (!manager.acquire(this, target)) {
// 循环等待直到获取所有锁
}
try {
synchronized(this) {
synchronized(target) {
if (this.amount > value) {
this.amount -= value;
target.amount += value;
}
}
}
} finally {
manager.release(this, target);
}
}
}
策略二:主动释放已占有资源
对于"不可抢占"条件,传统的synchronized关键字无法满足需求,因为线程在获取不到所需锁时会直接进入阻塞状态。Java并发包java.util.concurrent提供了可中断的锁机制,允许线程在等待过程中主动放弃已经持有的资源。
策略三:按序申请资源
针对"循环等待"条件,可以通过强制资源申请顺序来破除环路。约定所有线程必须按照统一的顺序(例如账户ID的大小顺序)来获取锁,这样就不会形成循环等待链。
class Wallet {
private final int id;
private int amount;
public void sendMoney(Wallet target, int value) {
Wallet first = this.id < target.id ? this : target;
Wallet second = this.id < target.id ? target : this;
synchronized(first) {
synchronized(second) {
if (this.amount > value) {
this.amount -= value;
target.amount += value;
}
}
}
}
}
总结
面对并发编程中的死锁问题,我们不应局限于代码层面的思考,可以借鉴现实世界的模型来寻找解决方案。资源管理员模式对应现实中的"窗口办理"机制,按序申请则类似于"排队取号"规则。理解问题的本质,选择合适的策略,才能在保证线程安全的同时最大化系统性能。
