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