这是实现并发的正确方法吗?

时间:2019-05-15 05:45:29

标签: java multithreading junit reentrantlock

我创建了一个小应用程序,该应用程序主要包括用于在账户之间进行转账的剩余Apis。为了简单起见,我将并发哈希图用于数据存储。现在,要实现的最基本概念是多线程。

由于我没有使用任何数据库,因此我使用可重入锁在Account类本身上创建了锁。现在,当我进行交易时,我同时获得了两个帐户对象(即发送者和接收者)的锁定,然后将其传递给biconsumer.accept()。

我不确定这是否完全正确(仅就此应用而言)。 也不确定如何对此进行单元测试。

Account.java

public class Account {

    private ReentrantLock lock = new ReentrantLock();
    private String accountNumber;
    private long accountBalance;

    public String getAccountNumber() {
        return accountNumber;
    }

    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }

    public long getAccountBalance() {
        return accountBalance;
    }

    public void setAccountBalance(long accountBalance) {
        this.accountBalance = accountBalance;
    }

    public void getLock() {
        this.accountLock.lock();
    }

    public void doUnlock() {
        this.accountLock.unlock();
    }
}

Transfer.java

send(senderAccount, receiverAccount, (x, y) -> {
                    senderAccount.setAccountBalance(senderAccount.getAccountBalance() - (transferAmount));
                    receiverAccount.setAccountBalance(toAccountDto.getAccountBalance() + transferAmount));
                });


public void send(Account senderAccount, Account receiverAccount,
            BiConsumer<Account, Account> action) {
        senderAccount.lock();
        try {
            receiverAccount.lock();
            try {
                biConsumer.accept(senderAccount, receiverAccount);
            } finally {
                receiverAccount.unlock();
            }
        } finally {
            senderAccount.unlock();
        }
    }

就单线程而言,这可以按预期工作。 但是我该如何对它进行单元测试,以检查它是否可以在10000个线程中正常工作。

这个帐户级别是否也锁定了良好的做法(就此应用而言),还是我可以做其他事情?

1 个答案:

答案 0 :(得分:1)

锁是一个不好的工具。

在您发布的示例中,如果两个线程在两个帐户之间汇款,则很有可能会陷入僵局。

final Account acc1 = new Account();
final Account acc2 = new Account();
new Thread(() -> {
    while (true) {
        send(acc1, acc2, (x, y) -> {
            x.setAccountBalance(x.getAccountBalance() - 100);
            y.setAccountBalance(y.getAccountBalance() + 100);
        }); 
    }
}).start();
new Thread(() -> {
    while (true) {
        send(acc2, acc1, (x, y) -> {
            x.setAccountBalance(x.getAccountBalance() - 100);
            y.setAccountBalance(y.getAccountBalance() + 100);
        }); 
    }
}).start();

在某个时候,线程1将锁定acc1,而线程2将锁定锁定acc2

防止这种情况的通常方法是具有一定顺序的获取锁。
在这种情况下,太多的锁无法管理。

更好的解决方案是使用AtomicLong,但这需要对代码进行一些更改。

public class Account {

    private String accountNumber;
    private AtomicLong accountBalance = new AtomicLong();

    public String getAccountNumber() {
        return accountNumber;
    }

    public void setAccountNumber(String accountNumber) {
        this.accountNumber = accountNumber;
    }

    public long getAccountBalance() {
        return accountBalance.get();
    }

    /* Don't use this method if you expect that the balance has not changed since the last get */
    public void setAccountBalance(long accountBalance) {
        this.accountBalance.set(accountBalance);
    }

    /* Changes the balance atomically */
    public void addAccountBalance(long amountToAdd) {
        accountBalance.addAndGet(amountToAdd);
    }   
}

然后您可以像这样使用它:

senderAccount.addAccountBalance(-sendAmount);
receiverAccount.addAccountBalance(sendAmount);

因为没有锁,所以不会发生死锁。 虽然每个动作都是原子动作,但有一些警告:

  • 如果您希望旧值是某个数字,例如比转账金额大 您可能不得不在循环中使用compareAndSet
  • 如果您需要保证所有帐户的总和在任何时候都恒定不变,则此方法可能会失败(例如,从一个帐户中取出资金,尚未将其添加到另一帐户中)-在这种情况下,您需要一个大锁。但这会破坏并发,对吗?

编写多线程代码时最大的问题是找出要保证的内容-还是仍然保持一致的状态。然后确保没有线程看到系统处于不一致状态。

相关问题