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?