Consequences of updating another key (s) in ConcurrentHashMap # computeIfAbsent

Javadoc of ConcurrentHashMap#computeIfAbsent says

The calculation should be short and simple and should not try to update any other mappings of this map.

But from what I see, using remove() and clear() mappingFunction inside mappingFunction works fine. For example, this

 Key element = elements.computeIfAbsent(key, e -> { if (usages.size() == maxSize) { elements.remove(oldest); } return loader.load(key); }); 

What are the bad consequences of using the remove () method inside a mappingFunction ?

+8
java java.util.concurrent concurrenthashmap
source share
3 answers

Javadoc clearly explains the reason:

Some attempts to perform update operations on this map by other threads may be blocked while the calculation is in progress , so the calculation should be short and simple, and should not try to update any other mappings of this map.

You should not forget that ConcurrentHashMap designed to provide the use of a stream-safe card without blocking, as is the case for older thread-safe card classes like HashTable .
When a modification occurs on the map, it only blocks the corresponding display, and not the entire map.

ConcurrentHashMap - a hash table that supports full concurrency search queries and expected concurrency for updates.

computeIfAbsent() is a new method added in Java 8.
If it is poorly used, that is, if in the body of computeIfAbsent() , which already blocks the matching of the key passed to the method, you block another key, you enter the path where you can defeat the goal of ConcurrentHashMap as finally you will block two displays.
Imagine a problem if you block a larger display inside computeIfAbsent() and that this method is not short. concurrency access on the map will become slow.

So, javadoc computeIfAbsent() emphasizes this potential problem, recalling the principles of ConcurrentHashMap : keep it simple and fast.


Here is a sample code illustrating the problem.
Suppose we have an instance of ConcurrentHashMap<Integer, String> .

We will start two threads that use it:

  • First thread: thread1 , which calls computeIfAbsent() with key 1
  • Second thread: thread2 , which calls computeIfAbsent() with key 2

thread1 performs a fairly fast task, but does not follow the instructions of computeIfAbsent() javadoc: it updates key 2 in computeIfAbsent() , that is, another mapping that is used in the current context of the method (i.e. key 1 ).

thread2 performs a rather long task. It calls computeIfAbsent() using key 2 , following the recommendations of javadoc: it does not update any other mapping in its implementation.
To simulate a lengthy task, we can use the Thread.sleep() method with parameter 5000 .

For this particular situation, if thread2 starts before thread1 , call map.put(2, someValue); in thread1 will be blocked, and thread2 will not be returned computeIfAbsent() , which blocks the display of key 2 .

Finally, we get an instance of ConcurrentHashMap that blocks the display of key 2 for 5 seconds, while computeIfAbsent() is called with the display of key 1 .
This is misleading, inefficient, and goes against the intent of ConcurrentHashMap and the description of computeIfAbsent() , which intent calculates the value for the current key:

if the specified key is not yet associated with the value, try to calculate its value using this mapping function and enter it on this map if null

Code example:

 import java.util.concurrent.ConcurrentHashMap; public class BlockingCallOfComputeIfAbsentWithConcurrentHashMap { public static void main(String[] args) throws InterruptedException { ConcurrentHashMap<Integer, String> map = new ConcurrentHashMap<>(); Thread thread1 = new Thread() { @Override public void run() { map.computeIfAbsent(1, e -> { String valueForKey2 = map.get(2); System.out.println("thread1 : get() returns with value for key 2 = " + valueForKey2); String oldValueForKey2 = map.put(2, "newValue"); System.out.println("thread1 : after put() returns, previous value for key 2 = " + oldValueForKey2); return map.get(2); }); } }; Thread thread2 = new Thread() { @Override public void run() { map.computeIfAbsent(2, e -> { try { Thread.sleep(5000); } catch (Exception e1) { e1.printStackTrace(); } String value = "valueSetByThread2"; System.out.println("thread2 : computeIfAbsent() returns with value for key 2 = " + value); return value; }); } }; thread2.start(); Thread.sleep(1000); thread1.start(); } } 

As a conclusion, we always get:

thread1: get () returns with a value for key 2 = null

thread2: computeIfAbsent () returns with a value for key 2 = valueSetByThread2

thread1: after put () returns, the previous value for key 2 = valueSetByThread2

This is written quickly, since reads on ConcurrentHashMap not blocked:

thread1: get () returns with a value for key 2 = null

but this:

thread1: after put () returns, the previous value for key 2 = valueSetByThread2

only output when thread2 returns computeIfAbsent() .

+4
source share

Here is an example of a bad consequence:

 ConcurrentHashMap<Integer,String> cmap = new ConcurrentHashMap<> (); cmap.computeIfAbsent (1, e-> {cmap.remove (1); return "x";}); 

This code causes a dead end.

+5
source share

Such advice is a bit like advice not to walk in the middle of the road. You can do this, and you may not be hit by a car; you can also get off the road if you see a car.

However, you would be safer if you just stayed on the sidewalk (sidewalk) in the first place.

If you have nothing to do in the API document, of course, nothing prevents you from doing this. And you can try to do this, and you will find that there are no serious consequences, at least in the limited circumstances that you are experiencing. You can even delve into to find out exactly what reasons are advised; You can study the source code and prove that it is safe in your use case.

However, API developers are free to modify the implementation within the limitations described in the API documentation. They can make changes that will make your code work tomorrow, because they are not required to maintain behavior that they explicitly warn about using.

So, to answer your question about what the bad consequences may be: literally everything (well, everything that usually ends or throws a RuntimeException ); and you will not necessarily observe the same consequences over time or on different JVMs.

Stay on the sidewalk: don't do what the documentation tells you.

+2
source share

All Articles