Это правильный подход для достижения параллелизма? - PullRequest
2 голосов
/ 15 мая 2019

Я создал небольшое приложение, которое в основном включает в себя остальные 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 Ответ

1 голос
/ 15 мая 2019

Блокировки - плохой инструмент для этого.

В примере, который вы разместили, есть вероятность того, что будет тупик, если два потока отправят деньги между двумя счетами.

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 вцикл вместо.
  • Если вам нужно гарантировать, что сумма всех счетов постоянна во все времена, этот метод может не сработать (например, удалить деньги с одного счета, еще не добавил их на другой счет) - вВ этом случае вам нужен один большой замок.Но это победило бы параллелизм, верно?

Самая большая проблема при написании многопоточного кода состоит в том, чтобы выяснить, какие гарантии вы хотите дать - или что все еще является непротиворечивым состоянием.И затем убедитесь, что ни один поток не видит систему в несогласованном состоянии.

...