Skip to content

Commit 309cdc5

Browse files
christophstroblodrotbohm
authored andcommitted
DATACMNS-1210 - Fix concurrency issue in BasicPersitentEntity.
Make sure to use concurrent collection implementations for properties holding mutable state. Especially the ones making use of computeIfAbsent(…). Usage of ConcurrentReferenceHashMap allows us to move on without additional changes as it allows to store null values, whereas ConcurrentHashMap would require us to store explicit NullValue placeholders for such cases, potentially causing trouble in downstream store specific projects. JMH Benchmarks done with the Spring Data MongoDB mapping layer showed a potential performance loss of up to 5%. However a comparison between ConcurrentReferenceHashMap and ConcurrentHashMap did not show any significant difference in performance. Original pull request: #259.
1 parent d8cea8b commit 309cdc5

File tree

2 files changed

+88
-8
lines changed

2 files changed

+88
-8
lines changed

src/main/java/org/springframework/data/mapping/model/BasicPersistentEntity.java

+18-8
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,17 @@
2020

2121
import java.io.Serializable;
2222
import java.lang.annotation.Annotation;
23-
import java.util.*;
23+
import java.util.ArrayList;
24+
import java.util.Collections;
25+
import java.util.Comparator;
26+
import java.util.HashSet;
27+
import java.util.Iterator;
28+
import java.util.List;
29+
import java.util.Map;
30+
import java.util.Optional;
31+
import java.util.Set;
32+
import java.util.TreeSet;
33+
import java.util.concurrent.ConcurrentHashMap;
2434
import java.util.stream.Collectors;
2535

2636
import org.springframework.core.annotation.AnnotatedElementUtils;
@@ -30,7 +40,8 @@
3040
import org.springframework.data.util.TypeInformation;
3141
import org.springframework.lang.Nullable;
3242
import org.springframework.util.Assert;
33-
import org.springframework.util.LinkedMultiValueMap;
43+
import org.springframework.util.CollectionUtils;
44+
import org.springframework.util.ConcurrentReferenceHashMap;
3445
import org.springframework.util.MultiValueMap;
3546
import org.springframework.util.StringUtils;
3647

@@ -93,9 +104,10 @@ public BasicPersistentEntity(TypeInformation<T> information, @Nullable Comparato
93104
this.constructor = PreferredConstructorDiscoverer.discover(this);
94105
this.associations = comparator == null ? new HashSet<>() : new TreeSet<>(new AssociationComparator<>(comparator));
95106

96-
this.propertyCache = new HashMap<>();
97-
this.annotationCache = new HashMap<>();
98-
this.propertyAnnotationCache = new LinkedMultiValueMap<>();
107+
this.propertyCache = new ConcurrentReferenceHashMap<>();
108+
this.annotationCache = new ConcurrentReferenceHashMap<>();
109+
this.propertyAnnotationCache = CollectionUtils
110+
.toMultiValueMap(new ConcurrentReferenceHashMap<Class<? extends Annotation>, List<P>>());
99111
this.propertyAccessorFactory = BeanWrapperPropertyAccessorFactory.INSTANCE;
100112
this.typeAlias = Lazy.of(() -> getAliasFromAnnotation(getType()));
101113
}
@@ -193,9 +205,7 @@ public void addPersistentProperty(P property) {
193205
persistentPropertiesCache.add(property);
194206
}
195207

196-
if (!propertyCache.containsKey(property.getName())) {
197-
propertyCache.put(property.getName(), property);
198-
}
208+
propertyCache.computeIfAbsent(property.getName(), key -> property);
199209

200210
P candidate = returnPropertyIfBetterIdPropertyCandidateOrNull(property);
201211

src/test/java/org/springframework/data/mapping/model/BasicPersistentEntityUnitTests.java

+70
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@
1919
import static org.junit.Assume.*;
2020
import static org.mockito.Mockito.*;
2121

22+
import java.lang.annotation.Annotation;
2223
import java.lang.annotation.Retention;
2324
import java.lang.annotation.RetentionPolicy;
2425
import java.util.Comparator;
2526
import java.util.Iterator;
2627
import java.util.List;
28+
import java.util.concurrent.CountDownLatch;
29+
import java.util.concurrent.atomic.AtomicBoolean;
2730

2831
import org.hamcrest.CoreMatchers;
2932
import org.junit.Rule;
@@ -34,9 +37,12 @@
3437
import org.mockito.Mockito;
3538
import org.mockito.junit.MockitoJUnitRunner;
3639
import org.springframework.core.annotation.AliasFor;
40+
import org.springframework.data.annotation.AccessType;
41+
import org.springframework.data.annotation.AccessType.Type;
3742
import org.springframework.data.annotation.CreatedBy;
3843
import org.springframework.data.annotation.CreatedDate;
3944
import org.springframework.data.annotation.LastModifiedBy;
45+
import org.springframework.data.annotation.Persistent;
4046
import org.springframework.data.annotation.TypeAlias;
4147
import org.springframework.data.mapping.Alias;
4248
import org.springframework.data.mapping.Document;
@@ -49,6 +55,7 @@
4955
import org.springframework.data.mapping.context.SampleMappingContext;
5056
import org.springframework.data.mapping.context.SamplePersistentProperty;
5157
import org.springframework.data.util.ClassTypeInformation;
58+
import org.springframework.data.util.Version;
5259
import org.springframework.test.util.ReflectionTestUtils;
5360

5461
/**
@@ -276,6 +283,64 @@ public void getRequiredAnnotationThrowsException() {
276283
assertThatThrownBy(() -> entity.getRequiredAnnotation(Document.class)).isInstanceOf(IllegalStateException.class);
277284
}
278285

286+
@Test // DATACMNS-1210
287+
public void findAnnotationShouldBeThreadSafe() throws InterruptedException {
288+
289+
assumeTrue("Requires Java 9",
290+
Version.parse(System.getProperty("java.version")).isGreaterThanOrEqualTo(Version.parse("9.0")));
291+
292+
CountDownLatch latch = new CountDownLatch(2);
293+
CountDownLatch syncLatch = new CountDownLatch(1);
294+
295+
final AtomicBoolean failed = new AtomicBoolean(false);
296+
297+
PersistentEntity<EntityWithAnnotation, T> entity = new BasicPersistentEntity(
298+
ClassTypeInformation.from(EntityWithAnnotation.class), null) {
299+
300+
@Override
301+
public Annotation findAnnotation(Class annotationType) {
302+
303+
try {
304+
syncLatch.await();
305+
} catch (InterruptedException e) {
306+
e.printStackTrace();
307+
}
308+
309+
return super.findAnnotation(annotationType);
310+
}
311+
};
312+
313+
Runnable findAccessType = () -> {
314+
315+
try {
316+
entity.findAnnotation(AccessType.class);
317+
} catch (Exception e) {
318+
failed.set(true);
319+
} finally {
320+
latch.countDown();
321+
}
322+
};
323+
324+
Runnable findPersistent = () -> {
325+
326+
try {
327+
entity.findAnnotation(Persistent.class);
328+
} catch (Exception e) {
329+
failed.set(true);
330+
} finally {
331+
latch.countDown();
332+
}
333+
};
334+
335+
new Thread(findAccessType).start();
336+
new Thread(findPersistent).start();
337+
338+
syncLatch.countDown();
339+
latch.await();
340+
341+
assertThat(failed.get()).isFalse();
342+
}
343+
279344
private <S> BasicPersistentEntity<S, T> createEntity(Class<S> type) {
280345
return createEntity(type, null);
281346
}
@@ -315,4 +380,9 @@ public String getProperty() {
315380
static class AliasEntityUsingComposedAnnotation {}
316381

317382
static class Subtype extends Entity {}
383+
384+
@AccessType(Type.PROPERTY)
385+
static class EntityWithAnnotation {
386+
387+
}
318388
}

0 commit comments

Comments
 (0)