当前位置: 首页 > 知识库问答 >
问题:

使用多线程将钱从一个帐户添加到另一个帐户

苏洛城
2023-03-14

我有两个帐户和两个线程。1个线程将钱从1个帐户转到2个帐户,2个线程将钱从2个帐户转到1个帐户,当然前提是有足够的钱。我需要了解死锁情况,并解决死锁情况以确认安全转移。以下是我目前的想法:

账户.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");
        }
    }
}

主类

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());
    }
}

传输线程.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();
            }
        }
    }

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

共有2个答案

鲁城
2023-03-14

有关同步块的更多信息,请参阅。https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html 当我们使方法同步时,线程会在调用该方法的对象上获取锁。这里有两个帐户对象,因此有两个单独的锁。一个线程不会等待执行from.withdraw(amount);当另一个线程正在执行to.deposit(amount);。 如果我们将事务逻辑放在同步块中,那么一个线程必须等到另一个线程完成整个事务。

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

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

好的一面是,此解决方案仅锁定相关对象上的线程。
请分享更好的解决方案。

解晟睿
2023-03-14

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

然而,我认为这不是一个正确的解决方案。问题如下:

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

第一个问题是你把钱转错方向了!钱应该从from账户转到to账户。但是你把钱存入from账户,从账户取钱。

顾客不会满意的。

让我们解决这个问题:

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

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

让我们解决这个问题:

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

关闭...

如果我们在这里标记的点上出现功率下降会发生什么?如果我们处理真实的银行账户,那么我们实际上将信息存储在数据库中。所以账户中当前的值将被保留。但是在这里标记的点上,我们将从一个账户中提取钱,而不是存入另一个账户。那笔钱会怎么样?噗!不见了!

这有关系吗?这取决于我们如何界定需求。假设将银行帐户表示为(仅)内存对象是可以的,我想说,我们可以忽略在传输过程中发生电源故障的可能性。停电也会毁掉账目。

够近就够好了,这里。但是我们可以做得稍微好一点。正如我们注意到的,< code>from帐户中的值可能会在< code>getBalance()和< code>withdraw()调用之间发生变化,因此< code>withdraw()可能会失败。但是仔细想想,< code > from . retract 只是测试< code>from.getBalance()

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

如果from.withdraw(量)要透支账户,它将失败,但有一个例外。然后我们不会执行to.deposit(量)调用。

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

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

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

但现在我们不得不担心僵局。例如,如果一个线程尝试将资金从 A 转移到 B,而另一个线程同时将资金从 B 转移到 A。

有一些方法可以解决这个问题:

> < li>

一种方法是使用< code>Lock API和< code>acquire带有超时的锁来检测死锁。

另一种方法是编写转账方法,以便在进行转账(A,B)和转账(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);
              }
          }
      }
  }
 类似资料:
  • 我在AWS云服务中设置了一个项目。在那里我使用了EC2实例、AMI、弹性IP、互联网门路、NACL、路由表、安全组、自定义VPC、私有和公共子网、弹性负载平衡、自动伸缩、启动配置、KMS-key、Lambda、RDS Aurora实例、S3桶、简单电子邮件服务、简单队列服务、简单通知服务、云观察日志。现在我的客户要求将所有服务从现有的AWS帐户迁移到新的AWS帐户。

  • 除公证人外,我们的Corda网络还有3个节点。图中显示了每个节点应执行的操作。 只有在这种情况下,我们才会遇到“需要将令牌从帐户持有人转移到乙方”的麻烦 流程代码: 我们需要在C方执行流程,实际持有人是账户持有人,新持有人是乙方。 使用此代码,将返回一个错误:net.corda.core.CordaRuntime 异常:java.lang.非法描述异常:未为以下事务参与者提供流会话:[O = B

  • 我的客户正在使用IMAP使用Outlook for Web中的“连接帐户”将一个Office365电子邮件帐户连接到另一个。当这样做时,它失败了,错误是它不能连接到帐户。帐户无法通过“不安全”方法连接,手动键入IMAP设置时也无法工作。Office365电子邮件帐户在不同的Office365帐户上,但我怀疑当他们在同一个Office365帐户上时也会发生这种情况。 我把这个问题作为一个问题发布,然

  • 是否可以将文件从源AWS S3 bucket发送/同步到位于不同位置的不同AWS帐户上的目标S3 bucket? 我发现了以下内容:https://aws.amazon.com/premiumsupport/knowled-center/copy-s3-objects-account/ 但是如果我理解正确的话,这就是如何从目标帐户同步文件的方法。 有没有别的方法可以做呢?从源帐户访问目标桶(使用源

  • 对于从lambda跨帐户访问SQS,我需要允许附加到lambda的IAM角色具有SQS权限,还是允许IAM角色在SQS的访问策略中具有权限?(或者两者都做?) 更新:基本上,我已经按照下面一些答案中的建议设置了SQS访问策略。 我得到一份工作 尝试向队列boto3.resource('sqs')发送内容时。队列(“某个队列url”)。send_message(**kwargs))`从lambda,

  • 我有2个AWS帐户。我试图复制文件从帐户1到帐户2在桶2在美国西部2地区。我有所有必要的IAM政策,相同的凭据适用于两个帐户。我使用python boto3库。 如图所示,复制函数在指向目标帐户2/us-west-2的客户端对象上执行。它是如何获取帐户1/us-east1中的源文件的?我应该提供作为复制函数的输入吗?