synchronize access to IdentityHashMap instances#1787
Conversation
... to avoid concurrency issues like in build https://github.com/datafaker-net/datafaker/actions/runs/23618303854/job/68791305528?pr=1785: ``` java.lang.ClassCastException: class net.datafaker.providers.base.Color cannot be cast to class net.datafaker.providers.base.Vehicle (net.datafaker.providers.base.Color and net.datafaker.providers.base.Vehicle are in unnamed module of loader 'app') at net.datafaker.providers.base.BaseProviders.vehicle(BaseProviders.java:506) at net.datafaker.providers.base.VehicleLocaleTest.lambda$localeProviderListTest$3(VehicleLocaleTest.java:23) ``` Class `IdentityHashMap` is not thread-safe, all accesses to this class should be synchronized.
PR Summary
These changes collectively improve concurrent usage of these classes and reduce the chance of unexpected behavior in multithreaded environments. This PR ensures that simultaneous modifications from different threads are properly handled, increasing the reliability of the software. |
|
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1787 +/- ##
============================================
- Coverage 92.39% 92.31% -0.08%
+ Complexity 3448 3446 -2
============================================
Files 339 339
Lines 6794 6794
Branches 670 670
============================================
- Hits 6277 6272 -5
- Misses 352 355 +3
- Partials 165 167 +2 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses concurrency-related corruption of several identity-keyed caches by synchronizing access to IdentityHashMap instances across the codebase (aligning with the reported ClassCastException seen in concurrent runs).
Changes:
- Wrap multiple
IdentityHashMapcaches withCollections.synchronizedMap(...)to make basic map operations thread-safe. - Apply the same synchronization approach consistently across provider caching, locale chain caching, reflection/method caches, and transformer constructor/schema caches.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| src/main/java/net/datafaker/transformations/JavaObjectTransformer.java | Synchronizes static caches for schema-to-consumer and class-to-constructor lookups. |
| src/main/java/net/datafaker/service/FakerContext.java | Synchronizes static locale chain and locale normalization caches. |
| src/main/java/net/datafaker/service/FakeValuesService.java | Synchronizes the primitive-to-wrapper identity map. |
| src/main/java/net/datafaker/providers/base/ObjectMethods.java | Synchronizes method lookup caches keyed by class identity. |
| src/main/java/net/datafaker/providers/base/BaseFaker.java | Synchronizes the provider instance cache to avoid concurrent IdentityHashMap corruption. |
| private static final Map<SingletonLocale, List<SingletonLocale>> LOCALE_2_LOCALES_CHAIN = synchronizedMap(new IdentityHashMap<>()); | ||
| private static final Map<SingletonLocale, SingletonLocale> STRING_LOCALE_HASH_MAP = synchronizedMap(new IdentityHashMap<>()); |
There was a problem hiding this comment.
LOCALE_2_LOCALES_CHAIN / STRING_LOCALE_HASH_MAP are now Collections.synchronizedMap(...), but the class still uses synchronized (FakerContext.class) around some accesses. Since synchronizedMap uses the map instance as its mutex, those blocks don’t synchronize with other map operations and won’t make compound actions (e.g., containsKey+put) atomic. Consider either synchronizing on the map for compound operations, or using computeIfAbsent so the get+populate path is atomic under the map’s lock.
| private static final Map<SingletonLocale, List<SingletonLocale>> LOCALE_2_LOCALES_CHAIN = synchronizedMap(new IdentityHashMap<>()); | |
| private static final Map<SingletonLocale, SingletonLocale> STRING_LOCALE_HASH_MAP = synchronizedMap(new IdentityHashMap<>()); | |
| private static final Map<SingletonLocale, List<SingletonLocale>> LOCALE_2_LOCALES_CHAIN = new IdentityHashMap<>(); | |
| private static final Map<SingletonLocale, SingletonLocale> STRING_LOCALE_HASH_MAP = new IdentityHashMap<>(); |
| } | ||
|
|
||
| private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP = new IdentityHashMap<>(); | ||
| private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP = synchronizedMap(new IdentityHashMap<>()); |
There was a problem hiding this comment.
PRIMITIVE_WRAPPER_MAP is populated once in the static initializer and then only read. Wrapping it in Collections.synchronizedMap(...) adds unnecessary synchronization overhead; consider making it immutable instead (e.g., build the IdentityHashMap in the static block and then wrap with Collections.unmodifiableMap, or replace with Map.ofEntries if identity semantics aren’t required).
... to avoid concurrency issues like in build https://github.com/datafaker-net/datafaker/actions/runs/23618303854/job/68791305528?pr=1785:
Class
IdentityHashMapis not thread-safe, all accesses to this class should be synchronized.How to reproduce the problem
This code shows how to stably reproduce the issue with
IdentityHashMap: