Java 多线程环境下的资源竞争与死锁问题

一、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. 预期行为:两个线程能够顺利完成转账操作,账户 1 和账户 2 的余额能够正确更新,不会出现数据不一致的情况,也不会发生死锁。
  2. 实际行为 :在某些情况下,会发生死锁。例如,thread1 先获取了 account1 的锁,同时 thread2 先获取了 account2 的锁。然后,thread1 尝试获取 account2 的锁,而 thread2 尝试获取 account1 的锁,两个线程相互等待对方释放锁,从而导致死锁。此外,由于多个线程同时访问和修改账户余额,如果同步控制不当,还可能出现资源竞争问题,导致数据不一致。比如,在 fromAccount.getBalance() >= amount 判断之后,另一个线程可能已经修改了 fromAccount 的余额,导致实际扣除的金额超过了账户余额。

四、解决方案

  1. 按照固定顺序获取锁:为了避免死锁,可以按照固定的顺序获取锁。例如,对所有账户进行编号,始终先获取编号小的账户的锁,再获取编号大的账户的锁。
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);
                }
            }
        }
    }
}
  1. 使用 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();
            }
        }
    }
}
相关推荐
LiuYaoheng1 小时前
【Android】RecyclerView 刷新方式全解析:从 notifyDataSetChanged 到 DiffUtil
android·java
Wpa.wk1 小时前
selenium自动化测试-简单PO模式 (java版)
java·自动化测试·selenium·测试工具·po模式
洛_尘1 小时前
JAVA第十一学:认识异常
java·开发语言
沐浴露z1 小时前
如何应对服务雪崩?详解 服务降级与服务熔断
java·微服务
liwulin05061 小时前
【JAVA】AES加密
java
阿宁又菜又爱玩2 小时前
Maven基础知识
java·maven
S***q3772 小时前
【Springboot】@Autowired和@Resource的区别
java·spring boot·mybatis
南部余额2 小时前
SpringBoot自定义场景启动器
java·spring boot·场景启动器
p***s912 小时前
【SpringBoot】日志文件
java·spring boot·spring