【问题标题】:Adding money from one account to another using multithreading使用多线程从一个帐户向另一个帐户添加资金
【发布时间】:2021-06-25 07:32:29
【问题描述】:

我有 2 个帐户和 2 个线程。 1个线程将钱从1个账户转移到2个账户,2个线程将钱从2个账户转移到1个账户,当然如果有足够的钱。我需要意识到死锁情况并解决确认安全转移的死锁情况。这是我现在所拥有的:

Account.java

public class Account {

    private /*volatile*/ long balance;

    public Account() {
        this(0L);
    }

    public Account(long balance) {
        this.balance = balance;
    }

    public long getBalance() {
        return balance;
    }

    public synchronized void deposit(long amount) {
        checkAmountNonNegative(amount);
        balance += amount;
    }

    public synchronized void withdraw(long amount) {
        checkAmountNonNegative(amount);
        if (balance < amount) {
            throw new IllegalArgumentException("not enough money");
        }
        balance -= amount;
    }

    private static void checkAmountNonNegative(long amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("negative amount");
        }
    }
}

Main.java

public class Main {

    public static void main(String[] args) {
        Account first = new Account(1_000_000);
        Account second = new Account(1_000_000);

        TransferThread thread1 = new TransferThread(first, second, 2000);
        TransferThread thread2 = new TransferThread(second, first, 2000);

        CompletableFuture.allOf(
                CompletableFuture.runAsync(thread1),
                CompletableFuture.runAsync(thread2)
        ).join();

        System.out.println(first.getBalance());
        System.out.println(second.getBalance());
    }
}

TransferThread.java

public class AccountThread implements Runnable {

    private final Account from;
    private final Account to;
    private final long amount;

    public AccountThread(Account from, Account to, long amount) {
        this.from = from;
        this.to = to;
        this.amount = amount;
    }

    @Override
    public void run() {
        for (int i = 0; i < 2000; i++) {
            // my realization
            try {
                if (from.getBalance() < 0) {
                    throw new InsufficientFundsException();
                } else {
                    from.deposit(amount);
                    to.withdraw(amount);
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

我决定将同步到存款和取款两种方法以安全转移。但怀疑用方法run实现。我有正确的实施吗?如果没有,将不胜感激解释和纠正。

【问题讨论】:

  • 我认为您需要将try-catch 放入具有相同监视器对象的synchronized 块中。在同步方法的情况下,在对象上获取锁。在这里,您有两个帐户,因此有两个单独的锁。
  • 同样先提款,如果成功则存入目标账​​户。
  • @onkarruikar 老实说,try-catch 没有让你明白,你能给我举个例子吗?
  • 汇款是死锁问题的典型例子。您不需要在 Account 类中使用 volatile 或 synchronize 或其他任何东西。您应该在转移前锁定两个帐户,然后释放它们。为了避免死锁,您必须引入某种排序并按顺序锁定一对帐户,正如@StephenC 所述

标签: java multithreading


【解决方案1】:

您的解决方案不易出现死锁,因为 TransferThread 实例一次永远不会持有多个锁。

但是,我认为这不是一个正确的解决方案。问题就在这里:

if (from.getBalance() < 0) {
    throw new InsufficientFundsException();
} else {
    from.deposit(amount);
    to.withdraw(amount);
}

第一个问题是您将资金转移到错误的方向!钱应该从from 帐户转到to 帐户。但是您将钱存入from 帐户,并从to 帐户取款

客户不会对此感到高兴的。

让我们解决这个问题:

if (from.getBalance() < 0) {
    throw new InsufficientFundsException();
} else {
    to.deposit(amount);
    from.withdraw(amount);
}

现在的问题是我们在提款之前先存入。为什么这是个问题?因为在from.getBalance()from.withdraw(...) 调用之间,另一个线程可以从from 帐户中提款。这可能意味着 我们的 from.withdraw(amount) 调用可能会失败。但我们已经将钱存入to 帐户。哎呀!

让我们修复那个

if (from.getBalance() < 0) {
    throw new InsufficientFundsException();
} else {
    from.withdraw(amount);
    // HERE
    to.deposit(amount);
}

关闭...

如果我们在此处标记的点出现功率下降会发生什么?好吧,如果我们处理真实的银行账户,那么我们实际上会将信息存储在数据库中。因此,当前帐户中的值将被保留。但是在标记为 HERE 的地方,我们会从一个帐户中提取资金,而不是将其存入另一个帐户。这笔钱怎么办?噗!走了!

这有关系吗?好吧,这取决于我们如何构建需求。假设可以将银行帐户表示为(仅)在内存对象中,我想说我们可以忽略在转移过程中幸存的电源故障的微妙之处。停电也会毁掉这些账目。

足够接近就足够好了,在这里。但我们可以做得更好一些。正如我们所指出的,from 帐户中的值可以在getBalance()withdraw() 调用之间发生变化,因此withdraw() 可能会失败。但是当您考虑它时,from.withdraw 无论如何只是在测试from.getBalance() &lt; 0。所以,我们可以摆脱测试:

    from.withdraw(amount);
    to.deposit(amount);

如果from.withdraw(amount) 将要透支帐户,它将失败并出现异常。然后我们不会拨打to.deposit(amount)


现在我们可以尝试实现一个transfer 方法,该方法将两个帐户作为参数,并将资金从一个帐户转移到另一个帐户作为原子操作。可以想象,您可以通过在进行转账之前获得两个账户的锁定来做到这一点;例如像这样:

  public static void transfer(Account from, Account to, long amount {
      synchronized (from) {
          synchronized (to) {
              from.withdraw(amount);
              to.deposit(amount);
          }
      }
  }

(我故意忽略异常和异常处理。)

但是现在我们不得不担心死锁。例如,如果一个线程尝试将钱从 A 转移到 B,而另一个线程同时将钱从 B 转移到 A。

有办法解决这个问题:

  • 一种方法是使用Lock API 和acquire 锁定超时来检测死锁。

  • 另一种方法是写转账方法,在做transfer(A,B)transfer(B,A)的时候按相同的顺序获取账户锁。例如,假设Account对象有唯一的账号,那么先锁定账号较小的Account

      public static void transfer(Account from, Account to, long amount {
          if (from.getAccountNo() < to.getAccountNo()) {
              synchronized (from) {
                  synchronized (to) {
                      from.withdraw(amount);
                      to.deposit(amount);
                  }
              }
          } else {
              synchronized (to) {
                  synchronized (from) {
                      from.withdraw(amount);
                      to.deposit(amount);
                  }
              }
          }
      }
    

【讨论】:

  • ? 将较低的帐号作为第一个锁定是我的建议。
  • 但是 getAccountNo() 呢?我应该在那里添加任何 id 吗?
  • 是的。如果您打算使用该技术来避免死锁,那么您需要记录每个帐户的唯一帐号,并提供访问它的方法。
  • @StephenC 我可以添加随机 id 吗?
  • 没有。 id 必须是唯一的。随机数不是唯一的。 (如果您曾经有两个具有相同 id 的帐户,那么您将面临死锁的风险,因为按 id 排序的帐户不再是总排序。)身份哈希码不能用于相同的原因。
【解决方案2】:

有关同步块的更多信息,请参阅。 https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

当我们使方法同步时,线程会在调用该方法的对象上获取锁。这里有两个帐户对象,因此有两个单独的锁。当另一个线程正在执行to.deposit(amount);时,一个线程不会等待执行from.withdraw(amount);

如果我们将事务逻辑放在一个同步块中,那么一个线程必须等待其他线程完成整个事务。

将业务逻辑放在synchronized 块中,如下所示:

public class Test {

    public static void main(String[] args) {
        Account first = new Account(1_000_000);
        Account second = new Account(1_000_000);

        AccountThread task1 = new AccountThread(first, second, 2000, first, second);
        AccountThread task2 = new AccountThread(second, first, 2000, first, second);
        
        CompletableFuture.allOf(CompletableFuture.runAsync(task1), CompletableFuture.runAsync(task2)).join();

        System.out.println(first.getBalance());
        System.out.println(second.getBalance());
    }
}

class AccountThread implements Runnable {
    private static int count = 0;
    private final Account from;
    private final Account to;
    private final long amount;
    private final Object lock1;
    private final Object lock2;

    public AccountThread(Account from, Account to, long amount, Object lock1, Object lock2) {
        this.from = from;
        this.to = to;
        this.amount = amount;
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        for (int i = 0; i < 2000; i++) {
            synchronized (lock1) {
                synchronized (lock2) {
                    try {
                        if (from.getBalance() < 0) {
                            throw new RuntimeException();
                        } else {
                            from.withdraw(amount);
                            to.deposit(amount);
                        }
                    } catch (Exception e) {
                        //e.printStackTrace();
                        System.out.println(e.getMessage());
                    }
                }
            }
        }
    }
}

输出:

not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
not enough money
710000
1290000

这个解决方案的好处是仅将线程锁定在相关对象上。
请分享更好的解决方案。

【讨论】:

  • 如果我们从两个帐户转帐,一个帐户总是有相同金额的原因是什么?
  • 你还没有分享TransferThread.class的实现。你已经分享了@98​​7654329@ 的实现。所以我不知道发生了什么。有时我得到两个帐户的最终余额为1000000,这意味着两个线程都有平等的机会来执行交易。
  • 这个解决方案的问题是任何银行账户上的所有操作都将使用一个共享锁。这使其成为并发瓶颈。有更好的解决方案。
  • @StephenC 使用两个共享锁?
  • 您可以像这样获取fromto 对象的锁定以避免瓶颈。 stackoverflow.com/questions/23217190/…
猜你喜欢
  • 2016-12-14
  • 2021-08-04
  • 1970-01-01
  • 2022-01-11
  • 1970-01-01
  • 2016-03-26
  • 2020-04-16
  • 2016-08-12
  • 1970-01-01
相关资源
最近更新 更多