0

Suppose, I have concurrentHashMap code such as below:

ConcurrentHashMap<Integer,Integer> balances = new ConcurrentHashMap<>();

public void add(int accountId, int amountToAdd){
     int currentBalance = balances.get(accountId);
     balances.put(accountId, currentBalance + amountToAdd);
}

This add method is called from multiple threads, there can be attempt to update the amount of the same accountId at same time.

How to ensure the currentBalance doesn't change between the get and the put? Because from my understanding, if thread preempts after doing get, and meanwhile some other thread updates the balance, put will perform update with stale balance.

runkar
  • 456
  • 1
  • 4
  • 15

2 Answers2

3

Java 8 adds a bunch of update methods to the Map interface, with atomicity guarantees for ConcurrentHashMap. In your case, you could do something like this:

public void add(int accountId, int amountToAdd){
    balances.computeIfPresent(accountId, (key, currentBalance) -> currentBalance + amountToAdd);
}

Javadoc

The entire method invocation is performed atomically. Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this map.

shmosel
  • 49,289
  • 6
  • 73
  • 138
  • 1
    Can you comment on about this quote from the documentation: https://docs.oracle.com/javase/8/docs/api/java/util/Map.html#computeIfPresent-K-java.util.function.BiFunction- ==> `The default implementation makes no guarantees about synchronization or atomicity properties of this method. Any implementation providing atomicity guarantees must override this method and document its concurrency properties.` – krokodilko May 18 '16 at 20:57
  • @kordirko it speaks for itself. What do you want me to comment on? – shmosel May 18 '16 at 20:58
  • 2
    You wrote in the answer `with atomicity guarantees for ConcurrentHashMap` while the documentation says `The default implementation makes no guarantees about synchronization or atomicity properties of this method.` I simply can't undertand how atomicy can be guarandet by a method that doesn't guarantee synchronization and atomicy. – krokodilko May 18 '16 at 21:01
  • 1
    @kordirko The `Map` interface makes no atomicity guarantees, but it allows for implementations to do so, as you quoted: *Any implementation providing atomicity guarantees must override this method and document its concurrency properties.* [`ConcurrentHashMap.getIfPresent()`](https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#computeIfPresent-K-java.util.function.BiFunction-) clearly does so: *The entire method invocation is performed atomically.* – shmosel May 18 '16 at 21:04
  • @YassinHajaj, your last edit was out of place and incorrect. Please be careful when editing people's answers. – shmosel May 18 '16 at 21:08
  • @YassinHajaj `Integer::sum` would be equivalent to `(key, value) -> key + value`, which is wrong here. – shmosel May 18 '16 at 21:11
  • Note that `computeIfPresent` silently does nothing, if the key is absent, which might not be what you want. The more natural operation would be `balances.merge(accountId, amountToAdd, Integer::sum);` which would perform the equivalent of a simple `put` for an absent key, which in this case is like treating absent mappings as zero. In either case, the effective result (the new balance) is returned, in case you want to use it. – Holger May 24 '16 at 10:14
1

You can use a hashmap of AtomicIntegers:

ConcurrentHashMap<Integer,AtomicInteger> balances = new ConcurrentHashMap<>();

AtomicInteger implements the compare and swap operation.


And then:

public void add(int accountId, int amountToAdd){
     balances.get(accountId).addAndGet( amountToAdd );
}

to create a new accont - and don't overwrite an account already created and initialised with some amount by the other thread - use this:

public void addNewAccount(int accountId){
     balances.putIfAbsent( accountId, new AtomicInteger(0) );
}

you can also combine both metods and use only one:

public void add(int accountId, int amountToAdd){
     balances.putIfAbsent( accountId, new AtomicInteger(0) );
     balances.get(accountId).addAndGet( amountToAdd );
}
krokodilko
  • 35,300
  • 7
  • 55
  • 79