Affects: spring-boot 2.3.2 and higher

Since this commit https://github.com/spring-projects/spring-framework/commit/550f13e8ed678447a61872b4f9ff96d918003102#diff-00728bf33ef5ae1a833bdd47f5095b4bb377206328c288ad2290e9d89ba0d3d4 we get intermittent ConcurrentModificationException from GenericConversionService$Converters.getMatchableConverters(GenericConversionService.java:522) during start:

ERROR 2020-11-30 18:26:00.296 [AcxSpringBoot-56.starting][c1a00a] - Application run failed [SpringApplication]
java.util.ConcurrentModificationException: null
  at java.base/java.util.HashMap.computeIfAbsent(HashMap.java:1134)
  at org.springframework.core.convert.support.GenericConversionService$Converters.getMatchableConverters(GenericConversionService.java:522)
  at org.springframework.core.convert.support.GenericConversionService$Converters.add(GenericConversionService.java:515)
  at org.springframework.core.convert.support.GenericConversionService.addConverter(GenericConversionService.java:105)
  at org.springframework.core.convert.support.GenericConversionService.addConverter(GenericConversionService.java:99)
  at org.springframework.security.config.crypto.RsaKeyConversionServicePostProcessor.postProcessBeanFactory(RsaKeyConversionServicePostProcessor.java:72)
  at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:291)
  at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:182)
  at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:707)
  at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:533)
  at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
  at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:758)
  at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:750)
  at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:397)
  at org.springframework.boot.SpringApplication.run(SpringApplication.java:315)
  at com.wamas.acx4.mfs.core.server.remote.rest.AcxSpringAppForRest.start(AcxSpringAppForRest.java:97)
  at com.wamas.acx4.mfs.core.server.remote.rest.AcxSpringDaemon.onStart(AcxSpringDaemon.java:57)
  at com.wamas.acx4.mfs.core.etc.executor.AcxRunnable.lambda$onStartInternal$2(AcxRunnable.java:318)
  at com.wamas.acx4.mfs.core.decorator.internal.InternalDecorateWith.execWithNewContextAndTransaction(InternalDecorateWith.java:183)
  at com.wamas.acx4.mfs.core.decorator.Decorate.exec(Decorate.java:840)
  at com.wamas.acx4.mfs.core.etc.executor.AcxRunnable.onStartInternal(AcxRunnable.java:318)
  at com.wamas.acx4.mfs.core.server.platform.daemon.ServiceDaemon.onStartInternal(ServiceDaemon.java:96)
  at com.wamas.acx4.mfs.core.etc.executor.AcxRunnable.run(AcxRunnable.java:181)
  at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
  at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
  at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
  at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
  at java.base/java.lang.Thread.run(Thread.java:834)

Since Java 9 java.util.HashMap.computeIfAbsent shows this behavior. See https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/util/HashMap.html#computeIfAbsent(K,java.util.function.Function)

Comment From: jhoeller

I wonder where the actual concurrent modification comes from in your scenario? HashMap is only supposed to throw such an exception if the mapping function modifies the map... which doesn't seem to be the case here, with a simple ConvertersForPair instance being constructed.

Comment From: jhoeller

I have a suspicion that there might be concurrent modifications from another thread, even if this isn't designed for multi-threaded configuration calls at all at this point. The check in JDK 15's HashMap implementation would throw a ConcurrentModificationException on any intermediate modification, not just on modification from the mapping function.

If this is indeed a multi-threaded scenario, we need to consider making addConverter configuration calls actually thread-safe.

Comment From: jhoeller

I ended up switching our GenericConversionService state management to ConcurrentHashMap (with a supporting ConcurrentLinkedDeque in ConvertersForPair), not imposing any restrictions on our computeIfAbsent call and generally supporting addConverter calls from multiple threads now. This seems safer and covering more use scenarios than simply reverting to independent get/´put` calls (as we had them before the aforementioned commit, but not actually supporting concurrent registration back then, being exposed to race conditions and visibility limitations in any case).

Comment From: HelJani

Thanks, in our use case we have two Spring-Apps that are normally running in 2 JVMs. For unit testing we start them in a single JVM in 2 parallel Threads. There these race conditions lead to these sporadic exceptions. We will try with 396fb0c