First of all, I am aware of several similar (53998845, 71087512, 71269113) and related (28840047, 44224952) questions, but I believe that none of them exactly captures the same situation that I am encountering.
I am using a ConcurrentHashMap
as a cache for some runtime-generated Java classes. Specifically, I use computeIfAbsent
to either return a previously generated class, or generate the class on the fly. In some circumstances, this call throws an IllegalStateException: Recursive update
. An example stack trace looks like this:
java.lang.IllegalStateException: Recursive update
at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1742)
at pro.projo.internal.rcg.RuntimeCodeGenerationHandler.getImplementationOf(RuntimeCodeGenerationHandler.java:151)
at pro.projo.Projo.getImplementationClass(Projo.java:451)
at pro.projo.Projo.getImplementationClass(Projo.java:438)
at fxxx.natives.bootstrap.Bootstrap$1.lambda$configure$2(Bootstrap.java:63)
at java.base/java.util.ArrayList.forEach(ArrayList.java:1510)
at fxxx.natives.bootstrap.Bootstrap$1.configure(Bootstrap.java:76)
at com.google.inject.AbstractModule.configure(AbstractModule.java:66)
at com.google.inject.spi.Elements$RecordingBinder.install(Elements.java:409)
at com.google.inject.spi.Elements.getElements(Elements.java:108)
at com.google.inject.internal.InjectorShell$Builder.build(InjectorShell.java:160)
at com.google.inject.internal.InternalInjectorCreator.build(InternalInjectorCreator.java:107)
at com.google.inject.Guice.createInjector(Guice.java:87)
at com.google.inject.Guice.createInjector(Guice.java:69)
at com.google.inject.Guice.createInjector(Guice.java:59)
at fxxx.natives.bootstrap.Bootstrap.<init>(Bootstrap.java:89)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at java.base/jdk.internal.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
at java.base/jdk.internal.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
at java.base/java.lang.reflect.Constructor.newInstanceWithCaller(Constructor.java:500)
at java.base/java.lang.reflect.Constructor.newInstance(Constructor.java:481)
at java.base/java.util.ServiceLoader$ProviderImpl.newInstance(ServiceLoader.java:782)
at java.base/java.util.ServiceLoader$ProviderImpl.get(ServiceLoader.java:724)
at java.base/java.util.ServiceLoader$3.next(ServiceLoader.java:1396)
at java.base/java.util.ServiceLoader.findFirst(ServiceLoader.java:1811)
at fxxx.bootstrap.Bootstrap.load(Bootstrap.java:34)
at fxxx.test.junit.FxxxTestClassTestDescriptor.instantiateTestClass(FxxxTestClassTestDescriptor.java:27)
As far as I am aware, I am not violating ConcurrentHashMap.computeIfAbsent
's requirement that "The mapping function must not modify this map during computation", as that function is a pure function without side effects.
For the other requirement, that "the computation should be short and simple", my code's compliance is not as clear cut, since generating the classes on the fly is definitely an involved and expensive process (hence the caching).
I do, however, believe that the exception's claimed "Recursive update" is an incorrect assessment of what is actually going on here. If that were indeed the case, I would expect to see multiple occurrences of my code's call to CHM.computeIfAbsent
in the stack trace, but this is not what I see (not in the example stack trace, or any other stack trace that I've seen with this problem).
The code in CHM
is as follows:
1738 Node<K,V> pred = e;
1739 if ((e = e.next) == null) {
1740 if ((val = mappingFunction.apply(key)) != null) {
1741 if (pred.next != null)
1742 throw new IllegalStateException("Recursive update");
1743 added = true;
1744 pred.next = new Node<K,V>(h, key, val);
1745 }
1746 break;
1747 }
From the stack trace, I already know that this is not a recursive call to computeIfAbsent
from within another computeIfAbsent
invocation. The code saves a reference to the previous node as pred
(line 1738) and then updates e
to the next node (line 1739). Since we made it past the if
check, we know that the original e
's next
value was null
, therefore pred.next
should at this point also be null
.
However, a subsequent check (line 1741) reveals that pred
next is no longer null
(which triggers the exception).
As a recursive update is not corroborated by the stack trace, I am assuming that this must actually be a concurrent update instead. It appears that the original e
object, now known as pred
, has its next
pointer changed by another thread (java.util.concurrent.ConcurrentHashMap.Node
produces mutable objects, unfortunately).
I am using CHM
for the one reason that this caching mechanism must be thread-safe and have low overhead (i.e., no blanket locking). I would expect concurrent update of the cache to work. However, in this particular scenario, concurrent access does not work, and seems to be incorrectly classified as a recursive update instead.
For further context, my own code that invokes computeIfAbsent
looks like this:
public Class<? extends _Artifact_> getImplementationOf(Class<_Artifact_> type, boolean defaultPackage)
{
return implementationClassCache.computeIfAbsent(type, it -> generateImplementation(it, defaultPackage));
}
This code is from a utility library that needs to work on Java 8, but I've only ever seen the exception happen when the code is used from another project that's running on Java 11.
Is this another bug in ConcurrentHashMap
or am I overlooking something regarding the proper use of this class?