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
- Use the concurrent variant: EconomicMapWrap around ConcurrentHashMap (new EconomicMapWrap<>(new ConcurrentHashMap<>())) for shared maps.
- Alternatively guard every access with external synchronization (same lock for reads and writes) - but the concurrent wrapper is simpler and faster.
- 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
- Never share EconomicMapImpl across threads; use EconomicMapWrap(new ConcurrentHashMap<>()) for shared maps.
- If external synchronization is unavoidable, guard reads and writes with the same lock.
- Treat this InternalError as a defect report: fix the sharing pattern, do not retry on the corrupted map.
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
- map grown too large!
- null not supported
- Cannot modify the always-empty map
- Interrupted while waiting for housekeeping thread shutdown.
- Allocator's housekeeping thread was interrupted.
AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14).
Data as JSON: /api/errors/b418292cd0c31941.
Report an issue: GitHub.