Skip to content

synchronize access to IdentityHashMap instances#1787

Open
asolntsev wants to merge 1 commit intomainfrom
fix/concurrent-issue-with-identity-hashmap
Open

synchronize access to IdentityHashMap instances#1787
asolntsev wants to merge 1 commit intomainfrom
fix/concurrent-issue-with-identity-hashmap

Conversation

@asolntsev
Copy link
Copy Markdown
Collaborator

@asolntsev asolntsev commented Mar 28, 2026

... 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.

How to reproduce the problem

This code shows how to stably reproduce the issue with IdentityHashMap:

import org.junit.jupiter.api.RepeatedTest;

import java.util.IdentityHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.CountDownLatch;

import static java.util.Collections.synchronizedMap;
import static org.assertj.core.api.Assertions.assertThat;

class ConcurrentIdentityHashMapTest {

    // Case 1: non-synchronized access. ~9 of 100_000 tests fails.
    private final Map<Object, Object> providersCache = new IdentityHashMap<>();

    // Case 2: synchronized access. 0 tests fails.
    // private final Map<Object, Object> providersCache = synchronizedMap(new IdentityHashMap<>());

    @RepeatedTest(100_000)
    void concurrentIdentityHashMapCanReturnWrongValue() throws InterruptedException {
        int threadCount = 32;
        List<String> wrongValueObserved = new CopyOnWriteArrayList<>();

        // Each thread has its own unique key and value
        Object[] keys = new Object[threadCount];
        Object[] values = new Object[threadCount];
        for (int i = 0; i < threadCount; i++) {
            keys[i] = "key-" + i;   // distinct String instances
            values[i] = "value-" + i;
        }

        CountDownLatch startLatch = new CountDownLatch(1);
        CountDownLatch doneLatch = new CountDownLatch(threadCount);

        for (int t = 0; t < threadCount; t++) {
            final int index = t;
            new Thread(() -> {
                try {
                    startLatch.await(); // all threads start at once
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                providersCache.computeIfAbsent(keys[index], o -> values[index]);

                Object retrieved = providersCache.get(keys[index]);
                if (retrieved != null && retrieved != values[index]) {
                    wrongValueObserved.add("Thread " + index  + ": expected " + values[index] + " but got " + retrieved);
                }
                doneLatch.countDown();
            }).start();
        }

        startLatch.countDown(); // fire!
        doneLatch.await();

        assertThat(wrongValueObserved).isEmpty();
    }
}

... 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.
@asolntsev asolntsev added this to the 2.6.0 milestone Mar 28, 2026
@asolntsev asolntsev self-assigned this Mar 28, 2026
@asolntsev asolntsev added the bug Something isn't working label Mar 28, 2026
@asolntsev asolntsev requested a review from snuyanzin March 28, 2026 16:34
@what-the-diff
Copy link
Copy Markdown

what-the-diff bot commented Mar 28, 2026

PR Summary

  • Improved Thread-Safety in BaseFaker.java
    Updated the providersCache in BaseFaker.java to use a synchronizedMap, enhancing the safety when accessing the cache concurently.

  • Enhanced Concurrent Access in ObjectMethods.java
    METHODS_BY_NAME and METHODS_BY_RETURN_TYPE in ObjectMethods.java have been adjusted to use synchronizedMap. This allows safer simultaneous accesses.

  • Added Thread-Safe Mapping in FakeValuesService.java
    Ensured concurrency protection in FakeValuesService.java for PRIMITIVE_WRAPPER_MAP by using a synchronizedMap.

  • Enhanced Synchronization in FakerContext.java
    Upgraded LOCALE_2_LOCALES_CHAIN and STRING_LOCALE_HASH_MAP in FakerContext.java to use synchronizedMap, thus improving the safety of multiple accesses.

  • Improved Thread-Safety for Mapping in JavaObjectTransformer.java
    In JavaObjectTransformer.java, enhanced thread-safety for SCHEMA2CONSUMER and CLASS2CONSTRUCTOR by switching to synchronizedMap.

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-commenter
Copy link
Copy Markdown

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.31%. Comparing base (8018cca) to head (adef835).
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@kingthorin kingthorin requested review from Copilot and removed request for snuyanzin March 28, 2026 16:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 IdentityHashMap caches with Collections.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.

Comment on lines +22 to +23
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<>());
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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<>();

Copilot uses AI. Check for mistakes.
}

private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP = new IdentityHashMap<>();
private static final Map<Class<?>, Class<?>> PRIMITIVE_WRAPPER_MAP = synchronizedMap(new IdentityHashMap<>());
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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).

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants