oracle/graal · critical · InternalError

endless collision link cycle, most likely due to unsynchroni

Error message

endless collision link cycle, most likely due to unsynchronized concurrent access

What it means

EconomicMapImpl is not thread-safe. putHashEntry() builds collision links and detects the degenerate case where an entry would link to itself (entryIndex == oldIndex), which cannot happen in correct single-threaded operation and indicates two threads mutated the map concurrently and corrupted its hash links; it then throws InternalError.

Source

Thrown at sdk/src/org.graalvm.collections/src/org/graalvm/collections/EconomicMapImpl.java:624

            Object entryKey = getKey(i);
            if (entryKey != null) {
                putHashEntry(entryKey, i, false);
            }
        }
    }

    private void putHashEntry(Object key, int entryIndex, boolean rehashOnCollision) {
        int hashIndex = getHashIndex(key);
        int oldIndex = getHashArray(hashIndex) - 1;
        if (oldIndex != -1 && rehashOnCollision) {
            this.createHash();
            return;
        }
        setHashArray(hashIndex, entryIndex + 1);
        Object value = getRawValue(entryIndex);
        if (oldIndex != -1) {
            if (entryIndex == oldIndex) {
                throw new InternalError("endless collision link cycle, most likely due to unsynchronized concurrent access");
            }
            if (value instanceof CollisionLink collisionLink) {
                setRawValue(entryIndex, new CollisionLink(collisionLink.value, oldIndex));
            } else {
                setRawValue(entryIndex, new CollisionLink(value, oldIndex));
            }
        } else {
            if (value instanceof CollisionLink collisionLink) {
                setRawValue(entryIndex, collisionLink.value);
            }
        }
    }

    @Override
    public int size() {
        return totalEntries - deletedEntries;
    }

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Use the concurrent variant: EconomicMapWrap around ConcurrentHashMap (new EconomicMapWrap<>(new ConcurrentHashMap<>())) for shared maps.
  2. Alternatively guard every access with external synchronization (same lock for reads and writes) - but the concurrent wrapper is simpler and faster.
  3. After fixing, audit for other unsynchronized EconomicMaps shared by the same threads; corruption may have been silently degrading them too.

Example fix

// before
EconomicMap<String, Object> shared = EconomicMap.create();
// ... written from multiple threads -> InternalError

// after
EconomicMap<String, Object> shared = new EconomicMapWrap<>(new ConcurrentHashMap<>());
Defensive patterns

Strategy: fallback

Validate before calling

// No pre-call check can detect races; enforce ownership instead:
assert Thread.holdsLock(guard) || singleThreadedContext : "EconomicMapImpl must not be shared unsynchronized"

Try / catch

// Do NOT catch and continue: the map is already corrupt.
// Catch only to fail fast with context, then rebuild state:
try {
    map.put(k, v);
} catch (InternalError e) {
    if (e.getMessage() != null && e.getMessage().contains("collision link cycle")) {
        throw new IllegalStateException("EconomicMap corrupted by concurrent access", e);
    }
    throw e;
}

Prevention

When it happens

Trigger: Two or more threads calling put/putIfAbsent/remove (or rehash-triggering operations) on the same EconomicMapImpl without external synchronization; by the time this fires, the map structure is already corrupt.

Common situations: Sharing a map created with EconomicMap.create() across compiler/parsing threads; refactoring previously single-threaded code to parallel streams; caches populated lazily from multiple workers.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/b418292cd0c31941. Report an issue: GitHub.