diff --git a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/expression/PathExpression.java b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/expression/PathExpression.java index b90078c7cd..33f06502a5 100644 --- a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/expression/PathExpression.java +++ b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/expression/PathExpression.java @@ -20,10 +20,10 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; import org.jspecify.annotations.Nullable; @@ -43,12 +43,12 @@ public final class PathExpression implements Comparable { public static final String ROOT_EXPRESSION = "$"; private final List segments; - private int cachedHashCode; - private String cachedExpression; - private PathExpression cachedParent; - private Map cachedChildren; - private byte cachedHasWildcard; // 0=not computed, 1=false, 2=true - private byte cachedHasTypeSelector; // 0=not computed, 1=false, 2=true + private volatile int cachedHashCode; + private volatile String cachedExpression; + private volatile PathExpression cachedParent; + private final ConcurrentMap cachedChildren = new ConcurrentHashMap<>(16); + private volatile byte cachedHasWildcard; // 0=not computed, 1=false, 2=true + private volatile byte cachedHasTypeSelector; // 0=not computed, 1=false, 2=true private PathExpression(List segments) { this.segments = Collections.unmodifiableList(new ArrayList<>(segments)); @@ -204,12 +204,9 @@ private static Segment parseKeyValueUnion(String[] parts, String expression) { public PathExpression child(String propertyName) { Objects.requireNonNull(propertyName, "propertyName must not be null"); - Map children = cachedChildren; - if (children != null) { - PathExpression cached = children.get(propertyName); - if (cached != null) { - return cached; - } + PathExpression cached = cachedChildren.get(propertyName); + if (cached != null) { + return cached; } List newSegments = new ArrayList<>(segments.size() + 1); @@ -217,13 +214,8 @@ public PathExpression child(String propertyName) { newSegments.add(Segment.ofName(propertyName)); PathExpression result = new PathExpression(newSegments, true); - if (children == null) { - children = new HashMap<>(); - cachedChildren = children; - } - children.put(propertyName, result); - - return result; + PathExpression existing = cachedChildren.putIfAbsent(propertyName, result); + return existing != null ? existing : result; } public PathExpression index(int index) { diff --git a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeCandidateTreeContext.java b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeCandidateTreeContext.java index f64530bd4f..78c962d76d 100644 --- a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeCandidateTreeContext.java +++ b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeCandidateTreeContext.java @@ -90,7 +90,7 @@ void cacheSubtree( new ArrayList<>(children), new HashMap<>(parentChildMap) ); - subtreeCache.put(jvmType, snapshot); + subtreeCache.putIfAbsent(jvmType, snapshot); } /** diff --git a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeTree.java b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeTree.java index 605c5a3ef1..8afa321bc9 100644 --- a/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeTree.java +++ b/object-farm-api/src/main/java/com/navercorp/objectfarm/api/tree/JvmNodeTree.java @@ -282,8 +282,14 @@ private void buildPathMappings() { // Build paths by traversing the tree buildPathsRecursive(rootNode, PathExpression.root(), paths, parents); - this.nodePaths = paths; + // IMPORTANT: nodePaths must be written LAST. + // It acts as the gate variable in ensurePathMappingsInitialized() — + // other threads skip the synchronized block when they see nodePaths != null. + // Writing childToParent before nodePaths ensures that the volatile write of + // nodePaths happens-after childToParent, so any thread that reads + // nodePaths != null is guaranteed to see childToParent != null by transitivity. this.childToParent = parents; + this.nodePaths = paths; } private void buildPathsRecursive(