During recent load test, we caught java.util.ConcurrentModificationException
at several GET REST endpoints. The exception stack trace shows that exception is thrown during foreach loop over collection of objects:
java.base/java.util.ArrayList$Itr.checkForComodification(Unknown Source)
at java.base/java.util.ArrayList$Itr.next(Unknown Source)
at com.myapp.service.ApplicantBioServiceImpl.getOtherInfo(ApplicantBioServiceImpl.java:1497)
at jdk.internal.reflect.GeneratedMethodAccessor1214.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(Unknown Source)
at java.base/java.lang.reflect.Method.invoke(Unknown Source)
at org.springframework.aop.support.AopUtils.invokeJoinpointUsingReflection(AopUtils.java:343)
at org.springframework.aop.framework.ReflectiveMethodInvocation.invokeJoinpoint(ReflectiveMethodInvocation.java:198)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.transaction.interceptor.TransactionInterceptor$$Lambda$965/0x0000000000000000.proceedWithInvocation(Unknown Source)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:295)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:98)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:212)
at com.sun.proxy.$Proxy788.getOtherInfoUX(Unknown Source)
The method looks like this
@Override
@Transactional
public AbstractBlockDTO getOtherInfo(long userId, long sectionId) {
AbstractBlockDTO blockDTO = questionService.getQuestionBlockData(userId, sectionId);
ServiceUtility.sortBlock(blockDTO);
User user = getUser(userId);
for (AbstractDataDTO dataDTO : blockDTO.getDataDTO()) {
String questionBlockName = dataDTO.getId();
if (questionBlockName.equalsIgnoreCase(Constants.ETHNICITY)) {
populateEthnicityInfo(dataDTO, user, instanceId);
} else if (questionBlockName.equalsIgnoreCase(Constants.RACE)) {
populateRaceInfo(dataDTO, user, instanceId);
} else if (questionBlockName.equalsIgnoreCase(Constants.ETHNICITY_2)) {
populateEthnicity2(dataDTO, user, instanceId);
}
}
return blockDTO;
}
As you can see, there is no logic that modifies(add/remove) the collection within the foreach
loop.
The AbstractBlockDTO
class looks like this:
public class AbstractBlockDTO implements Serializable {
private static final long serialVersionUID = -2718327468035422694L;
// other fields
@JsonProperty("questionBlocks")
private List<AbstractDataDTO> dataDTO = new ArrayList<>();
public List<AbstractDataDTO> getDataDTO() {
return dataDTO;
}
public void setDataDTO(List<AbstractDataDTO> dataDTO) {
this.dataDTO = dataDTO;
}
// Builder
}
Assumptions: The questionService.getQuestionBlockData(userId, sectionId);
returns object that is fetched from cache. For caching we are using Spring Caching with a memcashed.
My initial thought was that cache returns the same obj instance, therefore we may end up in the situation when the collection is sorted in one thread and iterated in another one. But I can't reproduce it with the integration test. In the app logs I see the different objects are returned for the same key, but I am not sure if it just different references that point to the same object.
CUSTOM_QUESTIONS6789 from cache configCache value 'com.myapp.beans.AbstractBlockDTO@72c90d72'
CUSTOM_QUESTIONS6789 from cache configCache value 'com.myapp.beans.AbstractBlockDTO@531640e1'
I struggle with this issue and appreciate any input that will help me to understand why the exception happening and how to solve it, thanks.