
在java并发编程中,synchronized关键字是实现线程同步的常用机制,它确保同一时刻只有一个线程可以执行特定的代码块或方法。然而,不当的同步策略可能导致死锁,即两个或多个线程无限期地等待彼此释放资源。
一个经典的死锁场景发生在多个线程尝试获取多个锁,但获取顺序不一致时。考虑一个transferMoney方法,它需要同步两个Account对象以执行转账操作:
public class Account {
private UUID id;
private float balance;
// 构造函数、getter/setter等
public UUID getId() {
return id;
}
public void debit(float amount) {
this.balance -= amount;
}
public void credit(float amount) {
this.balance += amount;
}
}
public class TransferService {
public void transferMoney(Account a, Account b, float value) {
synchronized(a) { // 线程1获取了A的锁
synchronized(b) { // 线程1尝试获取B的锁
// 执行转账逻辑
a.debit(value);
b.credit(value);
}
}
}
}假设现在有两个线程同时调用transferMoney方法:
如果线程1成功获取了accountA的锁,并紧接着线程2成功获取了accountB的锁,那么:
这将导致典型的死锁,两个线程都无法继续执行。
立即学习“Java免费学习笔记(深入)”;
避免死锁的关键在于确保所有线程以相同的、预定义的顺序获取多个锁。这意味着我们不能依赖于方法参数的传入顺序,而应该基于锁对象的某个固有属性来确定其获取顺序。
为了实现这一点,我们可以为每个Account对象引入一个唯一标识符(例如UUID或Long ID),并约定在获取锁时,总是先获取ID较小的账户的锁,再获取ID较大的账户的锁。
首先,修改Account类,使其包含一个用于比较的唯一ID:
import java.util.Comparator;
import java.util.UUID;
import java.util.function.BinaryOperator;
public class Account {
private UUID id;
private float balance;
public Account(UUID id, float initialBalance) {
this.id = id;
this.balance = initialBalance;
}
public UUID getId() {
return id;
}
public void debit(float amount) {
if (this.balance < amount) {
throw new IllegalArgumentException("Insufficient funds.");
}
this.balance -= amount;
}
public void credit(float amount) {
this.balance += amount;
}
public float getBalance() {
return balance;
}
@Override
public String toString() {
return "Account{" + "id=" + id.toString().substring(0, 8) + ", balance=" + balance + '}';
}
// 辅助方法,用于确定两个账户中ID较小的那个
public static final BinaryOperator<Account> FIRST =
BinaryOperator.minBy(Comparator.comparing(Account::getId));
// 辅助方法,用于确定两个账户中ID较大的那个
public static final BinaryOperator<Account> SECOND =
BinaryOperator.maxBy(Comparator.comparing(Account::getId));
}接下来,修改transferMoney方法,使用FIRST和SECOND辅助方法来确定锁的获取顺序:
public class TransferService {
public void transferMoney(Account a, Account b, float value) {
// 确保不能向同一个账户转账
if (a.getId().equals(b.getId())) {
throw new IllegalArgumentException("Cannot transfer money to the same account.");
}
// 确定锁的获取顺序:总是先获取ID较小的账户的锁,再获取ID较大的账户的锁
Account firstLock = Account.FIRST.apply(a, b);
Account secondLock = Account.SECOND.apply(a, b);
synchronized (firstLock) {
synchronized (secondLock) {
// 执行转账逻辑
System.out.println(Thread.currentThread().getName() + " acquired locks for " + firstLock.getId().toString().substring(0, 8) + " and " + secondLock.getId().toString().substring(0, 8));
try {
// 模拟转账耗时
Thread.sleep(100);
firstLock.debit(value);
secondLock.credit(value);
System.out.println(Thread.currentThread().getName() + " transferred " + value + " from " + a.getId().toString().substring(0, 8) + " to " + b.getId().toString().substring(0, 8));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}通过这种方式,无论transferMoney方法被调用时accountA和accountB的传入顺序如何,firstLock和secondLock变量总是会引用到具有一致ID顺序的账户。例如,如果accountA.id小于accountB.id,那么firstLock总是accountA,secondLock总是accountB。这样,所有线程都会以synchronized(account_with_smaller_id) { synchronized(account_with_larger_id) {...} }的顺序获取锁,从而有效避免死锁。
除了synchronized关键字,Java并发API还提供了java.util.concurrent.locks.Lock接口,它提供了更灵活的锁机制。Lock接口的实现(如ReentrantLock)允许更精细地控制锁的获取和释放,尤其是在处理死锁时提供了额外的工具。
Lock接口的核心思想是,当一个线程需要获取多个锁时,如果它无法一次性获取所有必需的锁,就应该释放已经持有的锁,并稍后重试。这可以通过tryLock()方法实现,它尝试获取锁而不阻塞,并返回一个布尔值指示是否成功获取。
使用Lock的基本模式如下:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
// 假设每个Account对象内部有一个ReentrantLock
public class AccountWithLock {
private UUID id;
private float balance;
private final Lock lock = new ReentrantLock(); // 每个账户一个独立的锁
// ... 构造函数、getter/setter等
public Lock getLock() {
return lock;
}
}
public class TransferServiceWithLock {
public void transferMoney(AccountWithLock a, AccountWithLock b, float value) {
// 同样,先确定一致的锁获取顺序
AccountWithLock first = AccountWithLock.FIRST.apply(a, b); // 假设AccountWithLock也有FIRST/SECOND
AccountWithLock second = AccountWithLock.SECOND.apply(a, b);
Lock lock1 = first.getLock();
Lock lock2 = second.getLock();
boolean acquired1 = false;
boolean acquired2 = false;
try {
// 尝试获取第一个锁
acquired1 = lock1.tryLock();
if (acquired1) {
// 尝试获取第二个锁
acquired2 = lock2.tryLock();
if (acquired2) {
// 成功获取所有锁,执行转账
a.debit(value);
b.credit(value);
} else {
// 未能获取第二个锁,释放第一个锁,稍后重试
lock1.unlock();
}
}
} finally {
// 确保所有获取的锁都被释放
if (acquired2) {
lock2.unlock();
}
if (acquired1) { // 再次检查,因为如果acquired2失败,acquired1可能仍为true
lock1.unlock();
}
}
}
}注意事项:
死锁是并发编程中的一个常见陷阱,但通过遵循一些基本原则可以有效避免。
通过深入理解死锁的成因并采纳上述策略,开发者可以显著提高并发应用程序的稳定性和可靠性。
以上就是Java并发:同步方法死锁预防策略的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号