Skip to content

Commit 1784728

Browse files
authored
Make WrapDynaClass instance cache thread-safe (#418)
* make WrapDynaClass instance cache thread-safe * add concurrency regression test for WrapDynaClass cache Signed-off-by: Naveed Khan <dxbnaveed.k@gmail.com> --------- Signed-off-by: Naveed Khan <dxbnaveed.k@gmail.com>
1 parent d7b4cf1 commit 1784728

2 files changed

Lines changed: 103 additions & 2 deletions

File tree

src/main/java/org/apache/commons/beanutils2/WrapDynaClass.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@
2323
import java.util.HashMap;
2424
import java.util.Map;
2525
import java.util.Objects;
26-
import java.util.WeakHashMap;
2726

2827
/**
2928
* Implements {@link DynaClass} to wrap standard JavaBean instances.
@@ -88,7 +87,7 @@ public int hashCode() {
8887
private static final ContextClassLoaderLocal<Map<CacheKey, WrapDynaClass>> CLASSLOADER_CACHE = new ContextClassLoaderLocal<Map<CacheKey, WrapDynaClass>>() {
8988
@Override
9089
protected Map<CacheKey, WrapDynaClass> initialValue() {
91-
return new WeakHashMap<>();
90+
return BeanUtils.createCache();
9291
}
9392
};
9493

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.apache.commons.beanutils2;
19+
20+
import static org.junit.jupiter.api.Assertions.assertEquals;
21+
import static org.junit.jupiter.api.Assertions.assertSame;
22+
23+
import java.util.Collections;
24+
import java.util.IdentityHashMap;
25+
import java.util.Set;
26+
import java.util.concurrent.CyclicBarrier;
27+
import java.util.concurrent.ExecutorService;
28+
import java.util.concurrent.Executors;
29+
import java.util.concurrent.Future;
30+
import java.util.concurrent.TimeUnit;
31+
32+
import org.junit.jupiter.api.Test;
33+
34+
/**
35+
* Tests {@link WrapDynaClass}.
36+
*/
37+
class WrapDynaClassTest {
38+
39+
/**
40+
* The cache key is one bean class, so {@code createDynaClass} must hand back a single instance no matter how many threads race to populate the
41+
* per-classloader cache. With a plain {@code WeakHashMap} the non-atomic {@code computeIfAbsent} let concurrent callers build and return distinct
42+
* instances for one key; this drives that race and fails if more than one instance escapes.
43+
*/
44+
@Test
45+
void testConcurrentCreateDynaClassReturnsSameInstance() throws Exception {
46+
final int threads = 16;
47+
final int rounds = 200;
48+
final ExecutorService pool = Executors.newFixedThreadPool(threads);
49+
try {
50+
for (int r = 0; r < rounds; r++) {
51+
WrapDynaClass.clear();
52+
final CyclicBarrier barrier = new CyclicBarrier(threads);
53+
final Set<WrapDynaClass> results = Collections.newSetFromMap(new IdentityHashMap<>());
54+
final Future<?>[] futures = new Future<?>[threads];
55+
for (int t = 0; t < threads; t++) {
56+
futures[t] = pool.submit(() -> {
57+
barrier.await();
58+
final WrapDynaClass wdc = WrapDynaClass.createDynaClass(ConcurrentBean.class);
59+
synchronized (results) {
60+
results.add(wdc);
61+
}
62+
return null;
63+
});
64+
}
65+
for (final Future<?> f : futures) {
66+
f.get();
67+
}
68+
assertEquals(1, results.size(), "createDynaClass returned more than one instance for one key");
69+
}
70+
} finally {
71+
pool.shutdownNow();
72+
pool.awaitTermination(10, TimeUnit.SECONDS);
73+
}
74+
}
75+
76+
/**
77+
* The single-threaded cache contract: repeated calls for one bean class return the same instance until the cache is cleared.
78+
*/
79+
@Test
80+
void testCreateDynaClassIsCached() {
81+
WrapDynaClass.clear();
82+
final WrapDynaClass first = WrapDynaClass.createDynaClass(ConcurrentBean.class);
83+
assertSame(first, WrapDynaClass.createDynaClass(ConcurrentBean.class));
84+
WrapDynaClass.clear();
85+
}
86+
87+
/**
88+
* Simple bean used as a cache key.
89+
*/
90+
public static final class ConcurrentBean {
91+
92+
private String value;
93+
94+
public String getValue() {
95+
return value;
96+
}
97+
98+
public void setValue(final String value) {
99+
this.value = value;
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)