Как сделать обновление BigDecimal в потоке ConcurrentHashMap безопасным
Я делаю приложение, которое берет кучу записей в журнале и вычисляет сумму.
Ниже приведен пример использования потока / concurrency, когда существует несколько потоков, вызывающих метод addToSum()
. Я хочу, чтобы каждый вызов обновлял общее количество.
Если это небезопасно, объясните, что мне нужно сделать для обеспечения безопасности потоков.
Нужно ли мне synchronize
получить/поставить или есть лучший способ?
private ConcurrentHashMap<String, BigDecimal> sumByAccount;
public void addToSum(String account, BigDecimal amount){
BigDecimal newSum = sumByAccount.get(account).add(amount);
sumByAccount.put(account, newSum);
}
Большое спасибо!
Update:
Спасибо всем за ответ, я уже понял, что код выше не потокобезопасен.
Спасибо Vint за предложение AtomicReference
в качестве альтернативы synchronize
. Я использовал AtomicInteger
для хранения целочисленных сумм раньше, и мне было интересно, есть ли что-то подобное для BigDecimal.
Является ли окончательный вывод о pro и con двух?
Ответы
Ответ 1
Вы можете использовать синхронизацию, как и другие, но если хотите минимально блокирующее решение, вы можете попробовать AtomicReference
в качестве хранилища для BigDecimal
ConcurrentHashMap<String,AtomicReference<BigDecimal>> map;
public void addToSum(String account, BigDecimal amount) {
AtomicReference<BigDecimal> newSum = map.get(account);
for (;;) {
BigDecimal oldVal = newSum.get();
if (newSum.compareAndSet(oldVal, oldVal.add(amount)))
return;
}
}
Изменить - я объясню это больше:
В AtomicReference используется CAS для атомарного назначения одной ссылки. Цикл говорит об этом.
Если текущее поле, сохраненное в AtomicReference == oldVal
[их местоположение в памяти, а не их значение], замените значение поля, сохраненного в AtomicReference, на oldVal.add(amount)
. Теперь, в любое время после цикла for вызывается newSum.get(), он будет иметь объект BigDecimal, который был добавлен в.
Вы хотите использовать цикл здесь, потому что возможно, что два потока пытаются добавить к тому же AtomicReference. Может случиться так, что один поток завершается успешно, а другой поток терпит неудачу, если это произойдет, просто повторите попытку с новым добавленным значением.
При умеренном конфликте с потоками это будет более быстрая реализация, с большим разногласием вам лучше использовать synchronized
Ответ 2
Это небезопасно, потому что потоки A и B могут одновременно вызывать sumByAccount.get(account)
(более или менее), поэтому никто не увидит результат другого add(amount)
. То есть в этой последовательности могут случиться:
- thread A вызывает
sumByAccount.get("accountX")
и получает (например) 10.0.
- поток B вызывает
sumByAccount.get("accountX")
и получает то же значение, что и поток A: 10.0.
- поток A устанавливает его
newSum
в (скажем) 10.0 + 2.0 = 12.0.
- поток B устанавливает его
newSum
в (скажем) 10.0 + 5.0 = 15.0.
- поток A вызывает
sumByAccount.put("accountX", 12.0)
.
- поток B вызывает
sumByAccount.put("accountX", 15.0)
, переписывая то, что сделал поток A.
Один из способов исправить это - положить synchronized
в свой метод addToSum
или обернуть его содержимое в synchronized(this)
или synchronized(sumByAccount)
. Другой способ, поскольку вышеупомянутая последовательность событий происходит только в том случае, если два потока одновременно обновляют одну и ту же учетную запись, может быть синхронизация извне на основе какого-либо объекта Account
. Не видя остальной логики вашей программы, я не могу быть уверен.
Ответ 3
Ваше решение не является потокобезопасным. Причина в том, что сумма может быть пропущена, так как операция по размещению отделена от операции для получения (поэтому новое значение, которое вы вводите на карту, может пропустить сумму, добавляемую одновременно).
Самый безопасный способ сделать то, что вы хотите сделать, - синхронизировать ваш метод.
Ответ 4
Да, вам нужно синхронизировать, так как в противном случае вы можете иметь два потока, каждый из которых получает одинаковое значение (для одного и того же ключа), например A и thread 1 добавьте B к нему, а thread 2 добавляет C к нему и сохраняет его обратно. Результат теперь не будет A + B + C, но A + B или + C.
Что вам нужно сделать, это заблокировать что-то, что является общим для дополнений. Синхронизация по get/put не поможет, если вы не сделаете
synchronize {
get
add
put
}
но если вы это сделаете, вы предотвратите обновление потоков, даже если это для разных ключей. Вы хотите синхронизировать свою учетную запись. Однако синхронизация в строке кажется небезопасной, поскольку это может привести к взаимоблокировкам (вы не знаете, что еще блокирует строку). Можете ли вы создать объект учетной записи и использовать это для блокировки?