Issue
I know there have been many questions around computeIfAbsent
.
Specifically what I am looking for is to understand the statement around atomicity for a concurrent hash map.
from href="https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/ConcurrentHashMap.html#computeIfAbsent-K-java.util.function.Function-" rel="nofollow noreferrer">the JavaDoc
The entire method invocation is performed atomically, so the function is applied at most once per key.
If two threads attempt to execute computeIfAbsent
with different key's and find that in both cases the map does not contain them, might the resulting executions of the compute if absent function be concurrent? I understand they would not be concurrent in the event that both threads were trying to add the SAME key.
The word Atomic is used and it is mentioned that this means applied at most once per key. But there isn't a specific mention of synchronized behaviour on the method.
As a side note, this is relevant to me in that the method called by computeIfAbsent modifies then uses a field of the class in its body.*
I want to understand if there is a threading concern resulting from two different thread executions of the computeIfAbsent method for the two different keys.
Essentially do I have to look at something along the lines of synchronizing access to the field variable and its subsequent use within the computeIfAbsent method I call.
*( The computeIfAbsent method invoked is the only method which modifies the field. There is no other invoker of the method outside of the call from the hash map computeIfAbsent method. There is only one instance of the concurrent hash map that calls the computeWithAbsent method that invokes the "atomic" method in question)
My field is volatile to avoid potential concerns with atomic visibility.
Solution
There are situations where the mapping function could be executed concurrently for different key values so it is important that your mapping function is thread-safe.
The computeIfAbsent
method only guarantees that the mapping function isn't called simultaneously for the same key value. Also note that a Map
works by hashing muliple keys into buckets of entries and if computeIfAbsent(a, mapFunc)
is called at same time as computeIfAbsent(b, mapFunc)
with pair of keys a+b that map to same sub-table of ConcurrentHashMap
, then mapFunc
for each key will be run one after the other and not at same time.
However where the different keys do not resolve to same sub-table within ConcurrentHashMap
you should expect your mapping function to be called simulaneously by different threads for different key values.
Here is an example which shows a thread-safe mapping function that detects concurrent callers:
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>(2096, 1.0f);
AtomicInteger concurrent = new AtomicInteger();
Function<String, String> mappingFunction = s -> {
int c = concurrent.incrementAndGet();
String value = "Value:"+s +" concurrent="+c+" thread="+Thread.currentThread().getName();
if (c != 1)
System.out.println("Multiple callers for "+value);
try { Thread.sleep(50); } catch (InterruptedException ignore) { }
concurrent.decrementAndGet();
return value;
};
Runnable task = () -> {
Random r = new Random();
for (int i = 0; i < 10_000; i++)
map.computeIfAbsent(String.valueOf(r.nextInt(10240)), mappingFunction);
};
Thread a = new Thread(task, "one");
a.start();
task.run();
a.join();
map.values().stream().limit(32).forEach(System.out::println);
}
If run enough, there will be occasions where the counter inside mappingFunction
shows that 2 instances are running at same time on the pair of threads.
EDIT
To answer your comment about synchronized (r)
:
Note that there is infinite while
loop inside the computeIfAbsent
which only exits on break
or return
, and mappingFunction.apply(key)
is called in two places:
when the key is the first entry into the sub-table it runs to the
synchronized (r)
block. As the line before declaresNode<K,V> r = new ReservationNode<K,V>()
there is NEVER contention onr
from different threads, but only one thread successfully enters theif (casTabAt(...)) { binCount = 1; ... }
block and returns, other losing threads resume the loop.when the key is not the first entry into the sub-table it runs to the
synchronized (f)
block which would block all but one threads trying tocomputeIfAbsent
for different keys that are hashed to the same sub-table. As each thread enters the block it verifiesf
is unchanged, and if so returns existing or computed new value - otherwise resumes the loop.
Answered By - DuncG
Answer Checked By - Robin (JavaFixing Admin)