如何避免同步方法死锁?以转账方法交叉调用场景为例
按对象参数分组同步的转账方法与死锁解决
需求明确
我们需要实现transferMoney方法,满足两个核心要求:
- 当两次调用的账户参数完全不重叠时(比如调用(a,b)和(c,d)),方法可以并行执行,不互相阻塞
- 当两次调用的账户参数有重叠时(比如(a,b)和(a,c),或是(a,b)和(b,a)),方法必须串行执行,同时要避免(a,b)和(b,a)交叉调用时出现死锁
可行解决方案
1. 固定锁的获取顺序(最经典的死锁解决方法)
给每个Account分配一个唯一的标识(比如自增ID),获取锁时总是先获取ID较小的账户的锁,再获取ID较大的。这样不管调用顺序是(a,b)还是(b,a),所有线程都会遵循相同的锁获取顺序,从根源上避免死锁。
代码实现:
public void transferMoney(Account a, Account b, float value) { // 确定锁的获取顺序,保证(a,b)和(b,a)的锁顺序一致 Account firstLock = a.getId() < b.getId() ? a : b; Account secondLock = a.getId() < b.getId() ? b : a; synchronized(firstLock) { synchronized(secondLock) { // 执行具体转账逻辑 doTransfer(a, b, value); } } } // 示例Account类,包含唯一ID和余额 class Account { private final int id; private float balance; public Account(int id) { this.id = id; } public int getId() { return id; } // 转账核心逻辑,需保证线程安全(这里已被外层锁保护) private void doTransfer(Account from, Account to, float value) { from.balance -= value; to.balance += value; } }
优点:实现简单,无额外内存开销,并发度高,彻底避免死锁;完全符合“仅同一对象参与时同步”的需求——只要账户有重叠,锁的串行获取会自动保证同一账户的操作不会并发执行。
2. 基于一致哈希的分组同步
通过生成与账户顺序无关的哈希键,让(a,b)和(b,a)映射到同一个锁对象,从而保证交叉调用使用同一把锁,避免死锁。需要注意处理哈希冲突的问题。
代码实现:
import java.util.concurrent.ConcurrentHashMap; public class TransferService { // 全局锁映射表,存储哈希键对应的锁对象 private static final ConcurrentHashMap<Long, Object> lockMap = new ConcurrentHashMap<>(); public void transferMoney(Account a, Account b, float value) { // 生成与账户顺序无关的哈希键 long hashKey = generateConsistentHash(a, b); // 获取或创建对应的锁对象 Object lock = lockMap.computeIfAbsent(hashKey, k -> new Object()); synchronized(lock) { // 内层仍需对账户加锁,防止其他未通过该方法的并发操作 synchronized(a) { synchronized(b) { doTransfer(a, b, value); } } } // 可选:移除无用锁对象,避免内存泄漏 lockMap.remove(hashKey, lock); } // 生成(a,b)和(b,a)一致的哈希值 private long generateConsistentHash(Account a, Account b) { long hashA = System.identityHashCode(a); long hashB = System.identityHashCode(b); // 保证顺序不影响结果:小哈希值左移32位拼接大哈希值 return hashA < hashB ? (hashA << 32) | hashB : (hashB << 32) | hashA; } }
优点:不需要给Account增加ID字段;缺点:存在哈希冲突风险(不同账户对可能生成相同哈希键),会导致无关转账被串行,降低并发度;同时需要维护全局锁映射表,注意内存泄漏问题。
3. 使用显式锁+超时重试
通过ReentrantLock的tryLock方法尝试获取锁,若获取失败则释放已获取的锁,短暂休眠后重试,直到获取所有锁或超时。这种方式不会出现永久死锁,适合无法固定锁顺序的场景。
代码实现:
import java.util.concurrent.TimeUnit; import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; public void transferMoney(Account a, Account b, float value) throws InterruptedException { long timeout = 1000; // 超时时间1秒 long endTime = System.currentTimeMillis() + timeout; Lock lockA = a.getLock(); Lock lockB = b.getLock(); while (true) { boolean gotLockA = lockA.tryLock(); try { boolean gotLockB = lockB.tryLock(); try { if (gotLockA && gotLockB) { doTransfer(a, b, value); return; } } finally { if (gotLockB) lockB.unlock(); } } finally { if (gotLockA) lockA.unlock(); } // 超时则抛出异常或返回失败 if (System.currentTimeMillis() > endTime) { throw new RuntimeException("转账超时,避免死锁"); } // 短暂休眠,避免忙等 Thread.sleep(10); } } // Account类持有自己的显式锁 class Account { private final Lock lock = new ReentrantLock(); private float balance; public Lock getLock() { return lock; } }
优点:无需修改Account结构,不会出现永久死锁;缺点:需要处理重试和超时逻辑,高并发下重试会增加CPU开销,且可能存在转账失败的情况。
对您思路的分析
- volatile修饰a和b:无意义。volatile仅保证变量的可见性,而方法参数a、b是线程私有的,且volatile无法解决同步和死锁问题。
- 包含a和b的新对象:如果每次调用都创建新对象,每个调用的锁都不同,完全起不到同步作用;若要实现同一参数组合同步,必须像哈希方案那样做一致的映射,否则新对象无法满足需求。
内容的提问来源于stack exchange,提问作者Stupidquestion010
相关产品推荐
相关产品推荐

