一、Bug 场景
在一个银行转账系统中,多个线程可能同时处理不同账户之间的转账操作。每个账户都有一个余额,转账操作需要从一个账户扣除金额并加到另一个账户上。为了保证数据的一致性,对每个账户的余额操作都需要进行同步控制。
二、代码示例
java
public class Account {
private double balance;
public Account(double initialBalance) {
this.balance = initialBalance;
}
public double getBalance() {
return balance;
}
public void debit(double amount) {
this.balance -= amount;
}
public void credit(double amount) {
this.balance += amount;
}
}
public class TransferThread extends Thread {
private Account fromAccount;
private Account toAccount;
private double amount;
public TransferThread(Account from, Account to, double amount) {
this.fromAccount = from;
this.toAccount = to;
this.amount = amount;
}
@Override
public void run() {
synchronized (fromAccount) {
synchronized (toAccount) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足,转账失败: " + amount);
}
}
}
}
}
public class MultiThreadTransferBugExample {
public static void main(String[] args) {
Account account1 = new Account(1000);
Account account2 = new Account(500);
TransferThread thread1 = new TransferThread(account1, account2, 200);
TransferThread thread2 = new TransferThread(account2, account1, 100);
thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("账户1余额: " + account1.getBalance());
System.out.println("账户2余额: " + account2.getBalance());
}
}
三、问题描述
- 预期行为:两个线程能够顺利完成转账操作,账户 1 和账户 2 的余额能够正确更新,不会出现数据不一致的情况,也不会发生死锁。
- 实际行为 :在某些情况下,会发生死锁。例如,
thread1先获取了account1的锁,同时thread2先获取了account2的锁。然后,thread1尝试获取account2的锁,而thread2尝试获取account1的锁,两个线程相互等待对方释放锁,从而导致死锁。此外,由于多个线程同时访问和修改账户余额,如果同步控制不当,还可能出现资源竞争问题,导致数据不一致。比如,在fromAccount.getBalance() >= amount判断之后,另一个线程可能已经修改了fromAccount的余额,导致实际扣除的金额超过了账户余额。
四、解决方案
- 按照固定顺序获取锁:为了避免死锁,可以按照固定的顺序获取锁。例如,对所有账户进行编号,始终先获取编号小的账户的锁,再获取编号大的账户的锁。
java
public class TransferThread extends Thread {
private Account fromAccount;
private Account toAccount;
private double amount;
public TransferThread(Account from, Account to, double amount) {
this.fromAccount = from;
this.toAccount = to;
this.amount = amount;
}
@Override
public void run() {
Account firstAccount, secondAccount;
if (System.identityHashCode(fromAccount) < System.identityHashCode(toAccount)) {
firstAccount = fromAccount;
secondAccount = toAccount;
} else {
firstAccount = toAccount;
secondAccount = fromAccount;
}
synchronized (firstAccount) {
synchronized (secondAccount) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足,转账失败: " + amount);
}
}
}
}
}
- 使用
ReentrantLock并设置超时 :使用ReentrantLock代替synchronized关键字,并设置获取锁的超时时间。这样,如果在规定时间内无法获取到锁,线程可以放弃尝试,避免无限期等待导致死锁。
java
import java.util.concurrent.locks.ReentrantLock;
public class Account {
private double balance;
private ReentrantLock lock = new ReentrantLock();
public Account(double initialBalance) {
this.balance = initialBalance;
}
public double getBalance() {
lock.lock();
try {
return balance;
} finally {
lock.unlock();
}
}
public void debit(double amount) {
lock.lock();
try {
this.balance -= amount;
} finally {
lock.unlock();
}
}
public void credit(double amount) {
lock.lock();
try {
this.balance += amount;
} finally {
lock.unlock();
}
}
}
public class TransferThread extends Thread {
private Account fromAccount;
private Account toAccount;
private double amount;
public TransferThread(Account from, Account to, double amount) {
this.fromAccount = from;
this.toAccount = to;
this.amount = amount;
}
@Override
public void run() {
boolean fromLocked = false;
boolean toLocked = false;
try {
fromLocked = fromAccount.lock.tryLock(5, java.util.concurrent.TimeUnit.SECONDS);
if (fromLocked) {
toLocked = toAccount.lock.tryLock(5, java.util.concurrent.TimeUnit.SECONDS);
if (toLocked) {
if (fromAccount.getBalance() >= amount) {
fromAccount.debit(amount);
toAccount.credit(amount);
System.out.println(Thread.currentThread().getName() + " 转账成功: " + amount);
} else {
System.out.println(Thread.currentThread().getName() + " 余额不足,转账失败: " + amount);
}
} else {
System.out.println(Thread.currentThread().getName() + " 无法获取 toAccount 的锁,放弃转账");
}
} else {
System.out.println(Thread.currentThread().getName() + " 无法获取 fromAccount 的锁,放弃转账");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
if (toLocked) {
toAccount.lock.unlock();
}
if (fromLocked) {
fromAccount.lock.unlock();
}
}
}
}