From 002b177d1d4165db152c51fdbbb0789b86abcd4b Mon Sep 17 00:00:00 2001 From: AB Date: Tue, 28 Apr 2026 11:50:49 +0200 Subject: [PATCH] Use `ConcurrentReferenceHashMap` --- CHANGELOG.md | 3 + .../cache/internal/ZipFileFingerprinter.java | 7 +- .../xpath/internal/SaxonXPathRuleQuery.java | 7 +- .../xdev/pmd/analysis/PMDAnalyzer.java | 11 +- .../CurrentFileAnalysisManager.java | 4 +- .../apache/shiro/lang/util/SoftHashMap.java | 270 --- .../util/ConcurrentReferenceHashMap.java | 1662 +++++++++++++++++ ...leDescriptionDocMarkdownToHtmlService.java | 5 +- .../config/ConfigurationLocationFactory.java | 8 +- .../util/pmd/PMDLanguageFileTypeMapper.java | 5 +- 10 files changed, 1684 insertions(+), 298 deletions(-) delete mode 100644 src/main/java/software/xdev/pmd/external/org/apache/shiro/lang/util/SoftHashMap.java create mode 100644 src/main/java/software/xdev/pmd/external/org/springframework/util/ConcurrentReferenceHashMap.java diff --git a/CHANGELOG.md b/CHANGELOG.md index dc9a37c..e60d93f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ +# 1.1.1 +* Improve performance by using `ConcurrentReferenceHashMap` instead of `synchronized` + # 1.1.0 * Update PMD to 7.24.0 * Dropped support for IntelliJ IDEA < 261 to fix deprecations diff --git a/src/main/java/net/sourceforge/pmd/cache/internal/ZipFileFingerprinter.java b/src/main/java/net/sourceforge/pmd/cache/internal/ZipFileFingerprinter.java index 7c369d7..27e5879 100644 --- a/src/main/java/net/sourceforge/pmd/cache/internal/ZipFileFingerprinter.java +++ b/src/main/java/net/sourceforge/pmd/cache/internal/ZipFileFingerprinter.java @@ -12,7 +12,6 @@ import java.nio.ByteBuffer; import java.nio.file.NoSuchFileException; import java.util.ArrayList; -import java.util.Collections; import java.util.Comparator; import java.util.Enumeration; import java.util.List; @@ -25,7 +24,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import software.xdev.pmd.external.org.apache.shiro.lang.util.SoftHashMap; +import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; /** @@ -37,9 +36,7 @@ public class ZipFileFingerprinter implements ClasspathEntryFingerprinter { // IMPROVED - private static final Map FILE_CRC_CHECKSUMS_CACHE = - Collections.synchronizedMap(new SoftHashMap<>()); - + private static final Map FILE_CRC_CHECKSUMS_CACHE = new ConcurrentReferenceHashMap<>(); record UrlCachedPayload( long length, diff --git a/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/SaxonXPathRuleQuery.java b/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/SaxonXPathRuleQuery.java index db508f1..92f99e9 100644 --- a/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/SaxonXPathRuleQuery.java +++ b/src/main/java/net/sourceforge/pmd/lang/rule/xpath/internal/SaxonXPathRuleQuery.java @@ -5,14 +5,12 @@ package net.sourceforge.pmd.lang.rule.xpath.internal; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.WeakHashMap; import org.apache.commons.lang3.exception.ContextedRuntimeException; import org.slf4j.Logger; @@ -45,6 +43,7 @@ import net.sourceforge.pmd.properties.PropertyDescriptor; import net.sourceforge.pmd.util.DataMap; import net.sourceforge.pmd.util.DataMap.SimpleDataKey; +import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; /** @@ -57,8 +56,8 @@ public class SaxonXPathRuleQuery { // IMPROVED // Different XPathHandlers have different extensions (e.g. pmd-java) that are registered with configuration - private static Map cachedInitData = Collections.synchronizedMap(new WeakHashMap<>()); - + private static Map cachedInitData = new ConcurrentReferenceHashMap<>( + ConcurrentReferenceHashMap.ReferenceType.WEAK); record CachedInitData( Configuration configuration, diff --git a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java index 01a9574..2e75f29 100644 --- a/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java +++ b/src/main/java/software/xdev/pmd/analysis/PMDAnalyzer.java @@ -6,14 +6,13 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.Set; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.atomic.AtomicInteger; @@ -46,7 +45,7 @@ import net.sourceforge.pmd.reporting.Report; import software.xdev.pmd.config.PluginConfiguration; import software.xdev.pmd.config.PluginConfigurationManager; -import software.xdev.pmd.external.org.apache.shiro.lang.util.SoftHashMap; +import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; import software.xdev.pmd.langversion.ManagedLanguageVersionResolver; import software.xdev.pmd.model.config.ConfigurationLocation; @@ -61,11 +60,11 @@ public class PMDAnalyzer implements Disposable private final Project project; - private final Map, ReentrantLock> locks = Collections.synchronizedMap(new HashMap<>()); - private final Map, CacheFile> cacheFiles = Collections.synchronizedMap(new HashMap<>()); + private final Map, ReentrantLock> locks = new ConcurrentHashMap<>(); + private final Map, CacheFile> cacheFiles = new ConcurrentHashMap<>(); // Reuse classloader when path is the same private final Map, ClassLoader> cachedSdkLibAuxClassLoader = - Collections.synchronizedMap(new SoftHashMap<>()); + new ConcurrentReferenceHashMap<>(); public PMDAnalyzer(final Project project) { diff --git a/src/main/java/software/xdev/pmd/currentfile/CurrentFileAnalysisManager.java b/src/main/java/software/xdev/pmd/currentfile/CurrentFileAnalysisManager.java index b06ccc0..885a7b1 100644 --- a/src/main/java/software/xdev/pmd/currentfile/CurrentFileAnalysisManager.java +++ b/src/main/java/software/xdev/pmd/currentfile/CurrentFileAnalysisManager.java @@ -2,11 +2,11 @@ import java.util.ArrayList; import java.util.Collections; -import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicReference; import org.jetbrains.annotations.NotNull; @@ -40,7 +40,7 @@ public class CurrentFileAnalysisManager implements FileEditorManagerListener, Di @NotNull private final AtomicReference currentlySelectedFile = new AtomicReference<>(); private final Map, PMDAnalysisResult>> fileAnalysisResults = - Collections.synchronizedMap(new HashMap<>()); + new ConcurrentHashMap<>(); public CurrentFileAnalysisManager(@NotNull final Project project) { diff --git a/src/main/java/software/xdev/pmd/external/org/apache/shiro/lang/util/SoftHashMap.java b/src/main/java/software/xdev/pmd/external/org/apache/shiro/lang/util/SoftHashMap.java deleted file mode 100644 index 2a91e19..0000000 --- a/src/main/java/software/xdev/pmd/external/org/apache/shiro/lang/util/SoftHashMap.java +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package software.xdev.pmd.external.org.apache.shiro.lang.util; - -import java.lang.ref.ReferenceQueue; -import java.lang.ref.SoftReference; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - - -/** - * A SoftHashMap is a memory-constrained map that stores its values in - * {@link SoftReference SoftReference}s. (Contrast this with the JDK's {@link java.util.WeakHashMap WeakHashMap}, which - * uses weak references for its keys, which is of little value if you want the cache to auto-resize itself - * based on memory constraints). - *

- * Having the values wrapped by soft references allows the cache to automatically reduce its size based on memory - * limitations and garbage collection. This ensures that the cache will not cause memory leaks by holding strong - * references to all of its values. - *

- * This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney Redelinghuys's - * publicly posted version (with their approval), - * with continued modifications. - *

- * This implementation is thread-safe and usable in concurrent environments. - * - * @implNote AB 2025-10-22: Removed retention size as this is not needed - */ -@SuppressWarnings("all") -public class SoftHashMap implements Map -{ - /** - * The internal HashMap that will hold the SoftReference. - */ - private final Map> map; - - /** - * Reference queue for cleared SoftReference objects. - */ - private final ReferenceQueue queue; - - public SoftHashMap() - { - super(); - this.queue = new ReferenceQueue<>(); - this.map = new ConcurrentHashMap<>(); - } - - /** - * Creates a {@code SoftHashMap} backed by the specified {@code source}. - * - * @param source the backing map to populate this {@code SoftHashMap} - * @see #SoftHashMap(java.util.Map, int) - */ - public SoftHashMap(final Map source) - { - this(); - this.putAll(source); - } - - @Override - public V get(final Object key) - { - this.processQueue(); - - V result = null; - final SoftValue value = this.map.get(key); - - if(value != null) - { - // unwrap the 'real' value from the SoftReference - result = value.get(); - if(result == null) - { - // The wrapped value was garbage collected, so remove this entry from the backing map: - // noinspection SuspiciousMethodCalls - this.map.remove(key); - } - } - return result; - } - - /** - * Traverses the ReferenceQueue and removes garbage-collected SoftValue objects from the backing map by looking - * them - * up using the SoftValue.key data member. - */ - @SuppressWarnings({"unchecked", "SuspiciousMethodCalls"}) - private void processQueue() - { - SoftValue sv; - while((sv = (SoftValue)this.queue.poll()) != null) - { - // we can access private data! - this.map.remove(sv.key); - } - } - - @Override - public boolean isEmpty() - { - this.processQueue(); - return this.map.isEmpty(); - } - - @Override - public boolean containsKey(final Object key) - { - this.processQueue(); - return this.map.containsKey(key); - } - - @Override - public boolean containsValue(final Object value) - { - this.processQueue(); - final Collection values = this.values(); - return values != null && values.contains(value); - } - - @Override - public void putAll(final Map m) - { - if(m == null || m.isEmpty()) - { - this.processQueue(); - return; - } - for(final Map.Entry entry : m.entrySet()) - { - this.put(entry.getKey(), entry.getValue()); - } - } - - @Override - public Set keySet() - { - this.processQueue(); - return this.map.keySet(); - } - - @Override - @SuppressWarnings("unchecked") - public Collection values() - { - this.processQueue(); - final Collection keys = this.map.keySet(); - if(keys.isEmpty()) - { - return Collections.EMPTY_SET; - } - final Collection values = new ArrayList<>(keys.size()); - for(final K key : keys) - { - final V v = this.get(key); - if(v != null) - { - values.add(v); - } - } - return values; - } - - /** - * Creates a new entry, but wraps the value in a SoftValue instance to enable auto garbage collection. - */ - @Override - public V put(final K key, final V value) - { - // throw out garbage collected values first - this.processQueue(); - final SoftValue sv = new SoftValue<>(value, key, this.queue); - final SoftValue previous = this.map.put(key, sv); - return previous != null ? previous.get() : null; - } - - @Override - public V remove(final Object key) - { - // throw out garbage collected values first - this.processQueue(); - final SoftValue raw = this.map.remove(key); - return raw != null ? raw.get() : null; - } - - @Override - public void clear() - { - // throw out garbage collected values - this.processQueue(); - this.map.clear(); - } - - @Override - public int size() - { - // throw out garbage collected values first - this.processQueue(); - return this.map.size(); - } - - @Override - @SuppressWarnings("unchecked") - public Set> entrySet() - { - // throw out garbage collected values first - this.processQueue(); - final Collection keys = this.map.keySet(); - if(keys.isEmpty()) - { - // noinspection unchecked - return Collections.EMPTY_SET; - } - - final Map kvPairs = new HashMap<>(keys.size()); - for(final K key : keys) - { - final V v = this.get(key); - if(v != null) - { - kvPairs.put(key, v); - } - } - return kvPairs.entrySet(); - } - - /** - * We define our own subclass of SoftReference which contains not only the value but also the key to make it easier - * to find the entry in the HashMap after it's been garbage collected. - */ - private static final class SoftValue extends SoftReference - { - - private final K key; - - /** - * Constructs a new instance, wrapping the value, key, and queue, as required by the superclass. - * - * @param value the map value - * @param key the map key - * @param queue the soft reference queue to poll to determine if the entry had been reaped by the GC. - */ - private SoftValue(final V value, final K key, final ReferenceQueue queue) - { - super(value, queue); - this.key = key; - } - } -} diff --git a/src/main/java/software/xdev/pmd/external/org/springframework/util/ConcurrentReferenceHashMap.java b/src/main/java/software/xdev/pmd/external/org/springframework/util/ConcurrentReferenceHashMap.java new file mode 100644 index 0000000..ccbecf9 --- /dev/null +++ b/src/main/java/software/xdev/pmd/external/org/springframework/util/ConcurrentReferenceHashMap.java @@ -0,0 +1,1662 @@ +/* + * Copyright 2002-present the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package software.xdev.pmd.external.org.springframework.util; + +import java.lang.ref.ReferenceQueue; +import java.lang.ref.SoftReference; +import java.lang.ref.WeakReference; +import java.lang.reflect.Array; +import java.util.AbstractCollection; +import java.util.AbstractMap; +import java.util.AbstractSet; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumSet; +import java.util.HashSet; +import java.util.Iterator; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Objects; +import java.util.Set; +import java.util.Spliterator; +import java.util.Spliterators; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.locks.ReentrantLock; +import java.util.function.BiFunction; +import java.util.function.Function; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +// @formatter:off +/** + * This is a port of + * + * Spring's ConcurrentReferenceHashMap v7.0.7 + * + *


+ *

+ * A {@link ConcurrentHashMap} variant that uses {@link ReferenceType#SOFT soft} or {@linkplain ReferenceType#WEAK weak} + * references for both {@code keys} and {@code values}. + * + *

This class can be used as an alternative to + * {@code Collections.synchronizedMap(new WeakHashMap>())} in order to support better performance when + * accessed concurrently. This implementation follows the same design constraints as {@link ConcurrentHashMap} with the + * exception that {@code null} values and {@code null} keys are supported. + * + *

NOTE: The use of references means that there is no guarantee that items + * placed into the map will be subsequently available. The garbage collector may discard references at any time, so it + * may appear that an unknown thread is silently removing entries. + * + *

If not explicitly specified, this implementation will use + * {@linkplain SoftReference soft entry references}. + * + * @param the key type + * @param the value type + * @author Phillip Webb + * @author Juergen Hoeller + * @author Brian Clozel + * @since 3.2 + */ +// @formatter:on +@SuppressWarnings("PMD.GodClass") +public class ConcurrentReferenceHashMap extends AbstractMap implements ConcurrentMap +{ + private static final int DEFAULT_INITIAL_CAPACITY = 16; + + private static final float DEFAULT_LOAD_FACTOR = 0.75f; + + private static final int DEFAULT_CONCURRENCY_LEVEL = 16; + + private static final ReferenceType DEFAULT_REFERENCE_TYPE = ReferenceType.SOFT; + + private static final int MAXIMUM_CONCURRENCY_LEVEL = 1 << 16; + + private static final int MAXIMUM_SEGMENT_SIZE = 1 << 30; + + /** + * Array of segments indexed using the high order bits from the hash. + */ + private final Segment[] segments; + + /** + * When the average number of references per table exceeds this value resize will be attempted. + */ + private final float loadFactor; + + /** + * The reference type: SOFT or WEAK. + */ + private final ReferenceType referenceType; + + /** + * The shift value used to calculate the size of the segments array and an index from the hash. + */ + private final int shift; + + /** + * Late binding entry set. + */ + @SuppressWarnings("checkstyle:IllegalIdentifierName") + private @Nullable Set> entrySet; + + /** + * Late binding key set. + */ + @SuppressWarnings("checkstyle:IllegalIdentifierName") + private @Nullable Set keySet; + + /** + * Late binding values collection. + */ + private @Nullable Collection values; + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + */ + public ConcurrentReferenceHashMap() + { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param referenceType the reference type used for entries (soft or weak) + */ + public ConcurrentReferenceHashMap(final ReferenceType referenceType) + { + this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, referenceType); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param initialCapacity the initial capacity of the map + */ + public ConcurrentReferenceHashMap(final int initialCapacity) + { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + // @formatter:off + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. + * When the average number of references per table exceeds this value resize + * will be attempted. + */ + // @formatter:on + public ConcurrentReferenceHashMap(final int initialCapacity, final float loadFactor) + { + this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param initialCapacity the initial capacity of the map + * @param concurrencyLevel the expected number of threads that will concurrently write to the map + */ + public ConcurrentReferenceHashMap(final int initialCapacity, final int concurrencyLevel) + { + this(initialCapacity, DEFAULT_LOAD_FACTOR, concurrencyLevel, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param initialCapacity the initial capacity of the map + * @param referenceType the reference type used for entries (soft or weak) + */ + public ConcurrentReferenceHashMap(final int initialCapacity, final ReferenceType referenceType) + { + this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL, referenceType); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per table exceeds this value, + * resize will be attempted. + * @param concurrencyLevel the expected number of threads that will concurrently write to the map + */ + public ConcurrentReferenceHashMap(final int initialCapacity, final float loadFactor, final int concurrencyLevel) + { + this(initialCapacity, loadFactor, concurrencyLevel, DEFAULT_REFERENCE_TYPE); + } + + /** + * Create a new {@code ConcurrentReferenceHashMap} instance. + * + * @param initialCapacity the initial capacity of the map + * @param loadFactor the load factor. When the average number of references per table exceeds this value, + * resize will be attempted. + * @param concurrencyLevel the expected number of threads that will concurrently write to the map + * @param referenceType the reference type used for entries (soft or weak) + */ + @SuppressWarnings("unchecked") + public ConcurrentReferenceHashMap( + final int initialCapacity, + final float loadFactor, + final int concurrencyLevel, + final ReferenceType referenceType) + { + if(initialCapacity < 0) + { + throw new IllegalArgumentException("Initial capacity must not be negative"); + } + if(loadFactor <= 0f) + { + throw new IllegalArgumentException("Load factor must be positive"); + } + if(concurrencyLevel <= 0) + { + throw new IllegalArgumentException("Concurrency level must be positive"); + } + Objects.requireNonNull(referenceType, "Reference type must not be null"); + this.loadFactor = loadFactor; + this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL); + final int size = 1 << this.shift; + this.referenceType = referenceType; + final int roundedUpSegmentCapacity = (int)((initialCapacity + size - 1L) / size); + final int initialSize = 1 << calculateShift(roundedUpSegmentCapacity, MAXIMUM_SEGMENT_SIZE); + final Segment[] segments = (Segment[])Array.newInstance(Segment.class, size); + final int resizeThreshold = (int)(initialSize * this.getLoadFactor()); + for(int i = 0; i < segments.length; i++) + { + segments[i] = new Segment(initialSize, resizeThreshold); + } + this.segments = segments; + } + + protected final float getLoadFactor() + { + return this.loadFactor; + } + + protected final int getSegmentsSize() + { + return this.segments.length; + } + + protected final Segment getSegment(final int index) + { + return this.segments[index]; + } + + /** + * Factory method that returns the {@link ReferenceManager}. This method will be called once for each + * {@link Segment}. + * + * @return a new reference manager + */ + protected ReferenceManager createReferenceManager() + { + return new ReferenceManager(); + } + + // @formatter:off + /** + * Get the hash for a given object, apply an additional hash function to reduce collisions. This implementation + * uses the same Wang/Jenkins algorithm as {@link ConcurrentHashMap}. Subclasses can override to provide alternative + * hashing. + * + * @param o the object to hash (may be null) + * @return the resulting hash code + */ + // @formatter:on + @SuppressWarnings({"checkstyle:UnnecessaryParentheses", "checkstyle:MagicNumber"}) + protected int getHash(@Nullable final Object o) + { + int hash = (o != null ? o.hashCode() : 0); + hash += (hash << 15) ^ 0xffffcd7d; + hash ^= (hash >>> 10); + hash += (hash << 3); + hash ^= (hash >>> 6); + hash += (hash << 2) + (hash << 14); + hash ^= (hash >>> 16); + return hash; + } + + @Override + public @Nullable V get(@Nullable final Object key) + { + final Reference ref = this.getReference(key, Restructure.WHEN_NECESSARY); + final Entry entry = ref != null ? ref.get() : null; + return entry != null ? entry.getValue() : null; + } + + @Override + public @Nullable V getOrDefault(@Nullable final Object key, @Nullable final V defaultValue) + { + final Reference ref = this.getReference(key, Restructure.WHEN_NECESSARY); + final Entry entry = ref != null ? ref.get() : null; + return entry != null ? entry.getValue() : defaultValue; + } + + @Override + public boolean containsKey(@Nullable final Object key) + { + final Reference ref = this.getReference(key, Restructure.WHEN_NECESSARY); + final Entry entry = ref != null ? ref.get() : null; + return entry != null && Objects.deepEquals(entry.getKey(), key); + } + + /** + * Return a {@link Reference} to the {@link Entry} for the specified {@code key}, or {@code null} if not found. + * + * @param key the key (can be {@code null}) + * @param restructure types of restructure allowed during this call + * @return the reference, or {@code null} if not found + */ + protected final @Nullable Reference getReference(@Nullable final Object key, final Restructure restructure) + { + final int hash = this.getHash(key); + return this.getSegmentForHash(hash).getReference(key, hash, restructure); + } + + @Override + public @Nullable V put(@Nullable final K key, @Nullable final V value) + { + return this.put(key, value, true); + } + + @Override + public @Nullable V putIfAbsent(@Nullable final K key, @Nullable final V value) + { + return this.put(key, value, false); + } + + private @Nullable V put(final @Nullable K key, final @Nullable V value, final boolean overwriteExisting) + { + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) + { + @Override + protected @Nullable V execute( + @Nullable final Reference ref, + @Nullable final Entry entry, + @Nullable final Entries entries) + { + if(entry != null) + { + final V oldValue = entry.getValue(); + if(overwriteExisting) + { + entry.setValue(value); + } + return oldValue; + } + Objects.requireNonNull(entries, "No entries segment"); + entries.add(value); + return null; + } + }); + } + + @Override + public @Nullable V remove(@Nullable final Object key) + { + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) + { + @Override + protected @Nullable V execute(@Nullable final Reference ref, @Nullable final Entry entry) + { + if(entry != null) + { + if(ref != null) + { + ref.release(); + } + return entry.value; + } + return null; + } + }); + } + + @Override + public boolean remove(@Nullable final Object key, final @Nullable Object value) + { + final Boolean result = this.doTask( + key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) + { + @Override + protected Boolean execute(@Nullable final Reference ref, @Nullable final Entry entry) + { + if(entry != null && Objects.deepEquals(entry.getValue(), value)) + { + if(ref != null) + { + ref.release(); + } + return true; + } + return false; + } + }); + return Boolean.TRUE.equals(result); + } + + @Override + public boolean replace(@Nullable final K key, final @Nullable V oldValue, final @Nullable V newValue) + { + final Boolean result = this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) + { + @Override + protected Boolean execute(@Nullable final Reference ref, @Nullable final Entry entry) + { + if(entry != null && Objects.deepEquals(entry.getValue(), oldValue)) + { + entry.setValue(newValue); + return true; + } + return false; + } + }); + return Boolean.TRUE.equals(result); + } + + @Override + public @Nullable V replace(@Nullable final K key, final @Nullable V value) + { + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) + { + @Override + protected @Nullable V execute(@Nullable final Reference ref, @Nullable final Entry entry) + { + if(entry != null) + { + final V oldValue = entry.getValue(); + entry.setValue(value); + return oldValue; + } + return null; + } + }); + } + + @Override + public @Nullable V computeIfAbsent( + @Nullable final K key, + final Function<@Nullable ? super K, @Nullable ? extends V> mappingFunction) + { + // Avoid locking if entry is present + final Reference ref = this.getReference(key, Restructure.NEVER); + final Entry entry = ref != null ? ref.get() : null; + if(entry != null) + { + return entry.getValue(); + } + + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) + { + @Override + protected @Nullable V execute( + @Nullable final Reference ref, + @Nullable final Entry entry, + @Nullable final Entries entries) + { + if(entry != null) + { + return entry.getValue(); + } + final V value = mappingFunction.apply(key); + // Add entry only if not null + if(value != null) + { + Objects.requireNonNull(entries, "No entries segment"); + entries.add(value); + } + return value; + } + }); + } + + @Override + public @Nullable V computeIfPresent( + @Nullable final K key, + final BiFunction<@Nullable ? super K, @Nullable ? super V, @Nullable ? extends V> remappingFunction) + { + // Avoid locking if entry is absent + final Reference ref = this.getReference(key, Restructure.NEVER); + final Entry entry = ref != null ? ref.get() : null; + if(entry == null) + { + return null; + } + + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) + { + @Override + protected @Nullable V execute( + @Nullable final Reference ref, + @Nullable final Entry entry, + @Nullable final Entries entries) + { + if(entry != null) + { + final V oldValue = entry.getValue(); + final V value = remappingFunction.apply(key, oldValue); + if(value != null) + { + // Replace entry + entry.setValue(value); + return value; + } + else + { + // Remove entry + if(ref != null) + { + ref.release(); + } + } + } + return null; + } + }); + } + + @Override + public @Nullable V compute( + @Nullable final K key, + final BiFunction<@Nullable ? super K, @Nullable ? super V, @Nullable ? extends V> remappingFunction) + { + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) + { + @Override + protected @Nullable V execute( + @Nullable final Reference ref, + @Nullable final Entry entry, + @Nullable final Entries entries) + { + V oldValue = null; + if(entry != null) + { + oldValue = entry.getValue(); + } + final V value = remappingFunction.apply(key, oldValue); + if(value != null) + { + if(entry != null) + { + // Replace entry + entry.setValue(value); + } + else + { + // Add entry + Objects.requireNonNull(entries, "No entries segment"); + entries.add(value); + } + return value; + } + else + { + // Remove entry + if(ref != null) + { + ref.release(); + } + } + return null; + } + }); + } + + @Override + public @Nullable V merge( + @Nullable final K key, + @Nullable final V value, + final BiFunction<@Nullable ? super V, @Nullable ? super V, @Nullable ? extends V> remappingFunction) + { + return this.doTask( + key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) + { + @Override + protected @Nullable V execute( + @Nullable final Reference ref, + @Nullable final Entry entry, + @Nullable final Entries entries) + { + if(entry != null) + { + final V oldValue = entry.getValue(); + final V newValue = remappingFunction.apply(oldValue, value); + if(newValue != null) + { + // Replace entry + entry.setValue(newValue); + return newValue; + } + else + { + // Remove entry + if(ref != null) + { + ref.release(); + } + return null; + } + } + else + { + // Add entry + Objects.requireNonNull(entries, "No entries segment"); + entries.add(value); + return value; + } + } + }); + } + + @Override + public void clear() + { + for(final Segment segment : this.segments) + { + segment.clear(); + } + } + + // @formatter:off + /** + * Remove any entries that have been garbage collected and are no longer referenced. Under normal circumstances + * garbage collected entries are automatically purged as items are added or removed from the Map. This method + * can be used to force a purge, and is useful when the Map is read frequently but updated less often. + */ + // @formatter:on + public void purgeUnreferencedEntries() + { + for(final Segment segment : this.segments) + { + segment.restructureIfNecessary(false); + } + } + + @Override + public int size() + { + int size = 0; + for(final Segment segment : this.segments) + { + size += segment.getCount(); + } + return size; + } + + @Override + public boolean isEmpty() + { + for(final Segment segment : this.segments) + { + if(segment.getCount() > 0) + { + return false; + } + } + return true; + } + + @SuppressWarnings("checkstyle:IllegalIdentifierName") + @Override + public Set> entrySet() + { + Set> entrySet = this.entrySet; + if(entrySet == null) + { + entrySet = new EntrySet(); + this.entrySet = entrySet; + } + return entrySet; + } + + @SuppressWarnings("checkstyle:IllegalIdentifierName") + @Override + public @NonNull Set keySet() + { + Set keySet = this.keySet; + if(keySet == null) + { + keySet = new KeySet(); + this.keySet = keySet; + } + return keySet; + } + + @Override + public @NonNull Collection values() + { + Collection values = this.values; + if(values == null) + { + values = new Values(); + this.values = values; + } + return values; + } + + private @Nullable T doTask(@Nullable final Object key, final Task task) + { + final int hash = this.getHash(key); + return this.getSegmentForHash(hash).doTask(hash, key, task); + } + + private Segment getSegmentForHash(final int hash) + { + return this.segments[(hash >>> (32 - this.shift)) & (this.segments.length - 1)]; + } + + // @formatter:off + /** + * Calculate a shift value that can be used to create a power-of-two value between the specified maximum and + * minimum values. + * + * @param minimumValue the minimum value + * @param maximumValue the maximum value + * @return the calculated shift (use {@code 1 << shift} to obtain a value) + */ + // @formatter:on + protected static int calculateShift(final int minimumValue, final int maximumValue) + { + int shift = 0; + int value = 1; + while(value < minimumValue && value < maximumValue) + { + value <<= 1; + shift++; + } + return shift; + } + + /** + * Various reference types supported by this map. + */ + public enum ReferenceType + { + /** + * Use {@link SoftReference SoftReferences}. + */ + SOFT, + + /** + * Use {@link WeakReference WeakReferences}. + */ + WEAK + } + + + /** + * A single segment used to divide the map to allow better concurrent performance. + */ + protected final class Segment extends ReentrantLock + { + private final ReferenceManager referenceManager; + + private final int initialSize; + + /** + * Array of references indexed using the low order bits from the hash. This property should only be set along + * with {@code resizeThreshold}. + */ + @SuppressWarnings("PMD.AvoidUsingVolatile") + private volatile @Nullable Reference[] references; + + /** + * The total number of references contained in this segment. This includes chained references and references + * that have been garbage collected but not purged. + */ + private final AtomicInteger count = new AtomicInteger(); + + /** + * The threshold when resizing of the references should occur. When {@code count} exceeds this value references + * will be resized. + */ + private int resizeThreshold; + + Segment(final int initialSize, final int resizeThreshold) + { + this.referenceManager = ConcurrentReferenceHashMap.this.createReferenceManager(); + this.initialSize = initialSize; + this.references = this.createReferenceArray(initialSize); + this.resizeThreshold = resizeThreshold; + } + + @Nullable Reference getReference( + @Nullable final Object key, + final int hash, + final Restructure restructure) + { + if(restructure == Restructure.WHEN_NECESSARY) + { + this.restructureIfNecessary(false); + } + if(this.count.get() == 0) + { + return null; + } + // Use a local copy to protect against other threads writing + @Nullable + final Reference[] references = this.references; + final int index = this.getIndex(hash, references); + final Reference head = references[index]; + return this.findInChain(head, key, hash); + } + + /** + * Apply an update operation to this segment. The segment will be locked during the update. + * + * @param hash the hash of the key + * @param key the key + * @param task the update operation + * @return the result of the operation + */ + private @Nullable T doTask(final int hash, final @Nullable Object key, final Task task) + { + final boolean resize = task.hasOption(TaskOption.RESIZE); + if(task.hasOption(TaskOption.RESTRUCTURE_BEFORE)) + { + this.restructureIfNecessary(resize); + } + if(task.hasOption(TaskOption.SKIP_IF_EMPTY) && this.count.get() == 0) + { + return task.execute(null, null, null); + } + this.lock(); + try + { + final int index = this.getIndex(hash, this.references); + final Reference head = this.references[index]; + final Reference ref = this.findInChain(head, key, hash); + final Entry entry = ref != null ? ref.get() : null; + final Entries entries = value -> { + @SuppressWarnings("unchecked") + final Entry newEntry = new Entry<>((K)key, value); + final Reference newReference = + Segment.this.referenceManager.createReference(newEntry, hash, head); + Segment.this.references[index] = newReference; + Segment.this.count.incrementAndGet(); + }; + return task.execute(ref, entry, entries); + } + finally + { + this.unlock(); + if(task.hasOption(TaskOption.RESTRUCTURE_AFTER)) + { + this.restructureIfNecessary(resize); + } + } + } + + /** + * Clear all items from this segment. + */ + void clear() + { + if(this.count.get() == 0) + { + return; + } + this.lock(); + try + { + this.references = this.createReferenceArray(this.initialSize); + this.resizeThreshold = (int)(this.references.length * ConcurrentReferenceHashMap.this.getLoadFactor()); + this.count.set(0); + } + finally + { + this.unlock(); + } + } + + /** + * Restructure the underlying data structure when it becomes necessary. This method can increase the size of + * the + * references table as well as purge any references that have been garbage collected. + * + * @param allowResize if resizing is permitted + */ + void restructureIfNecessary(final boolean allowResize) + { + final int currCount = this.count.get(); + final boolean needsResize = allowResize && currCount > 0 && currCount >= this.resizeThreshold; + final Reference ref = this.referenceManager.pollForPurge(); + if(ref != null || needsResize) + { + this.restructure(allowResize, ref); + } + } + + @SuppressWarnings({"PMD.CognitiveComplexity", "PMD.NPathComplexity", "checkstyle:FinalParameters"}) + private void restructure(final boolean allowResize, @Nullable Reference ref) + { + this.lock(); + try + { + int expectedCount = this.count.get(); + Set> toPurge = Collections.emptySet(); + if(ref != null) + { + toPurge = new HashSet<>(); + while(ref != null) + { + toPurge.add(ref); + ref = this.referenceManager.pollForPurge(); + } + } + expectedCount -= toPurge.size(); + + // Estimate new count, taking into account count inside lock and items that + // will be purged. + final boolean needsResize = expectedCount > 0 && expectedCount >= this.resizeThreshold; + boolean resizing = false; + int restructureSize = this.references.length; + if(allowResize && needsResize && restructureSize < MAXIMUM_SEGMENT_SIZE) + { + restructureSize <<= 1; + resizing = true; + } + + int newCount = 0; + // Restructure the resized reference array + if(resizing) + { + final Reference[] restructured = this.createReferenceArray(restructureSize); + for(final Reference reference : this.references) + { + ref = reference; + while(ref != null) + { + if(!toPurge.contains(ref)) + { + final Entry entry = ref.get(); + // Also filter out null references that are now null + // they should be polled from the queue in a later restructure call. + if(entry != null) + { + final int index = this.getIndex(ref.getHash(), restructured); + restructured[index] = this.referenceManager.createReference( + entry, ref.getHash(), restructured[index]); + newCount++; + } + } + ref = ref.getNext(); + } + } + // Replace volatile members + this.references = restructured; + this.resizeThreshold = + (int)(this.references.length * ConcurrentReferenceHashMap.this.getLoadFactor()); + } + // Restructure the existing reference array "in place" + else + { + for(int i = 0; i < this.references.length; i++) + { + Reference purgedRef = null; + ref = this.references[i]; + while(ref != null) + { + if(!toPurge.contains(ref)) + { + final Entry entry = ref.get(); + // Also filter out null references that are now null: + // They should be polled from the queue in a later restructure call. + if(entry != null) + { + purgedRef = this.referenceManager.createReference( + entry, ref.getHash(), purgedRef); + } + newCount++; + } + ref = ref.getNext(); + } + this.references[i] = purgedRef; + } + } + this.count.set(newCount); + } + finally + { + this.unlock(); + } + } + + private @Nullable Reference findInChain( + @Nullable final Reference ref, + @Nullable final Object key, + final int hash) + { + Reference currRef = ref; + while(currRef != null) + { + if(currRef.getHash() == hash) + { + final Entry entry = currRef.get(); + if(entry != null) + { + final K entryKey = entry.getKey(); + if(Objects.deepEquals(entryKey, key)) + { + return currRef; + } + } + } + currRef = currRef.getNext(); + } + return null; + } + + @SuppressWarnings({"unchecked"}) + private Reference[] createReferenceArray(final int size) + { + return new Reference[size]; + } + + private int getIndex(final int hash, @Nullable final Reference[] references) + { + return hash & (references.length - 1); + } + + /** + * Return the size of the current references array. + */ + int getSize() + { + return this.references.length; + } + + /** + * Return the total number of references in this segment. + */ + int getCount() + { + return this.count.get(); + } + } + + + /** + * A reference to an {@link Entry} contained in the map. Implementations are usually wrappers around specific Java + * reference implementations (for example, {@link SoftReference}). + * + * @param the key type + * @param the value type + */ + protected interface Reference + { + /** + * Return the referenced entry, or {@code null} if the entry is no longer available. + */ + @Nullable Entry get(); + + /** + * Return the hash for the reference. + */ + int getHash(); + + /** + * Return the next reference in the chain, or {@code null} if none. + */ + @Nullable Reference getNext(); + + /** + * Release this entry and ensure that it will be returned from {@code ReferenceManager#pollForPurge()}. + */ + void release(); + } + + + /** + * A single map entry. + * + * @param the key type + * @param the value type + */ + protected static final class Entry implements Map.Entry + { + private final @Nullable K key; + + @SuppressWarnings("PMD.AvoidUsingVolatile") + private volatile @Nullable V value; + + Entry(@Nullable final K key, @Nullable final V value) + { + this.key = key; + this.value = value; + } + + @Override + public @Nullable K getKey() + { + return this.key; + } + + @Override + public @Nullable V getValue() + { + return this.value; + } + + @Override + public @Nullable V setValue(@Nullable final V value) + { + final V previous = this.value; + this.value = value; + return previous; + } + + @Override + public boolean equals(@Nullable final Object other) + { + return this == other || other instanceof final Map.Entry that + && Objects.deepEquals(this.getKey(), that.getKey()) + && Objects.deepEquals(this.getValue(), that.getValue()); + } + + @Override + public int hashCode() + { + return Arrays.deepHashCode(new Object[]{this.key, this.value}); + } + + @Override + public String toString() + { + return this.key + "=" + this.value; + } + } + + + /** + * A task that can be {@link Segment#doTask run} against a {@link Segment}. + */ + private abstract class Task + { + private final EnumSet options; + + Task(final TaskOption... options) + { + this.options = options.length == 0 + ? EnumSet.noneOf(TaskOption.class) + : EnumSet.of(options[0], options); + } + + boolean hasOption(final TaskOption option) + { + return this.options.contains(option); + } + + /** + * Execute the task. + * + * @param ref the found reference (or {@code null}) + * @param entry the found entry (or {@code null}) + * @param entries access to the underlying entries + * @return the result of the task + * @see #execute(Reference, Entry) + */ + protected @Nullable T execute( + @Nullable final Reference ref, + @Nullable final Entry entry, + @Nullable final Entries entries) + { + return this.execute(ref, entry); + } + + /** + * Convenience method that can be used for tasks that do not need access to {@link Entries}. + * + * @param ref the found reference (or {@code null}) + * @param entry the found entry (or {@code null}) + * @return the result of the task + * @see #execute(Reference, Entry, Entries) + */ + protected @Nullable T execute(@Nullable final Reference ref, @Nullable final Entry entry) + { + return null; + } + } + + + /** + * Various options supported by a {@code Task}. + */ + private enum TaskOption + { + RESTRUCTURE_BEFORE, RESTRUCTURE_AFTER, SKIP_IF_EMPTY, RESIZE + } + + + /** + * Allows a task access to {@link ConcurrentReferenceHashMap.Segment} entries. + */ + private interface Entries + { + /** + * Add a new entry with the specified value. + * + * @param value the value to add + */ + void add(@Nullable V value); + } + + + /** + * Internal entry-set implementation. + */ + private final class EntrySet extends AbstractSet> + { + @Override + public @NonNull Iterator> iterator() + { + return new EntryIterator(); + } + + @Override + public boolean contains(@Nullable final Object o) + { + if(o instanceof final Map.Entry entry) + { + final Reference ref = + ConcurrentReferenceHashMap.this.getReference(entry.getKey(), Restructure.NEVER); + final Entry otherEntry = ref != null ? ref.get() : null; + if(otherEntry != null) + { + return Objects.equals(entry.getValue(), otherEntry.getValue()); + } + } + return false; + } + + @Override + public boolean remove(final Object o) + { + if(o instanceof final Map.Entry entry) + { + return ConcurrentReferenceHashMap.this.remove(entry.getKey(), entry.getValue()); + } + return false; + } + + @Override + public int size() + { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public void clear() + { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public @NonNull Spliterator> spliterator() + { + return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT); + } + } + + + /** + * Internal key-set implementation. + */ + private final class KeySet extends AbstractSet + { + @Override + public @NonNull Iterator iterator() + { + return new KeyIterator(); + } + + @Override + public int size() + { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public boolean isEmpty() + { + return ConcurrentReferenceHashMap.this.isEmpty(); + } + + @Override + public void clear() + { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public boolean contains(final Object k) + { + return ConcurrentReferenceHashMap.this.containsKey(k); + } + + @Override + public @NonNull Spliterator spliterator() + { + return Spliterators.spliterator(this, Spliterator.DISTINCT | Spliterator.CONCURRENT); + } + } + + + /** + * Internal key iterator implementation. + */ + private final class KeyIterator implements Iterator + { + private final Iterator> iterator = ConcurrentReferenceHashMap.this.entrySet().iterator(); + + @Override + public boolean hasNext() + { + return this.iterator.hasNext(); + } + + @Override + public void remove() + { + this.iterator.remove(); + } + + @Override + public K next() + { + return this.iterator.next().getKey(); + } + } + + + /** + * Internal values collection implementation. + */ + private final class Values extends AbstractCollection + { + @Override + public @NonNull Iterator iterator() + { + return new ValueIterator(); + } + + @Override + public int size() + { + return ConcurrentReferenceHashMap.this.size(); + } + + @Override + public boolean isEmpty() + { + return ConcurrentReferenceHashMap.this.isEmpty(); + } + + @Override + public void clear() + { + ConcurrentReferenceHashMap.this.clear(); + } + + @Override + public boolean contains(final Object v) + { + return ConcurrentReferenceHashMap.this.containsValue(v); + } + + @Override + public @NonNull Spliterator spliterator() + { + return Spliterators.spliterator(this, Spliterator.CONCURRENT); + } + } + + + /** + * Internal value iterator implementation. + */ + private final class ValueIterator implements Iterator + { + private final Iterator> iterator = ConcurrentReferenceHashMap.this.entrySet().iterator(); + + @Override + public boolean hasNext() + { + return this.iterator.hasNext(); + } + + @Override + public void remove() + { + this.iterator.remove(); + } + + @Override + public V next() + { + return this.iterator.next().getValue(); + } + } + + + /** + * Internal entry iterator implementation. + */ + private final class EntryIterator implements Iterator> + { + private int segmentIndex; + + private int referenceIndex; + + private @Nullable Reference @Nullable [] references; + + private @Nullable Reference reference; + + private @Nullable Entry next; + + private @Nullable Entry last; + + EntryIterator() + { + this.moveToNextSegment(); + } + + @Override + public boolean hasNext() + { + this.getNextIfNecessary(); + return this.next != null; + } + + @Override + public Entry next() + { + this.getNextIfNecessary(); + if(this.next == null) + { + throw new NoSuchElementException(); + } + this.last = this.next; + this.next = null; + return this.last; + } + + private void getNextIfNecessary() + { + while(this.next == null) + { + this.moveToNextReference(); + if(this.reference == null) + { + return; + } + this.next = this.reference.get(); + } + } + + private void moveToNextReference() + { + if(this.reference != null) + { + this.reference = this.reference.getNext(); + } + while(this.reference == null && this.references != null) + { + if(this.referenceIndex >= this.references.length) + { + this.moveToNextSegment(); + this.referenceIndex = 0; + } + else + { + this.reference = this.references[this.referenceIndex]; + this.referenceIndex++; + } + } + } + + private void moveToNextSegment() + { + this.reference = null; + this.references = null; + if(this.segmentIndex < ConcurrentReferenceHashMap.this.segments.length) + { + this.references = ConcurrentReferenceHashMap.this.segments[this.segmentIndex].references; + this.segmentIndex++; + } + } + + @Override + public void remove() + { + if(this.last == null) + { + throw new IllegalStateException("No element to remove"); + } + ConcurrentReferenceHashMap.this.remove(this.last.getKey()); + this.last = null; + } + } + + + /** + * The types of restructuring that can be performed. + */ + protected enum Restructure + { + WHEN_NECESSARY, NEVER + } + + // @formatter:off + /** + * Strategy class used to manage {@link Reference References}. This class can be overridden if alternative + * reference types need to be supported. + */ + // @formatter:on + protected class ReferenceManager + { + private final ReferenceQueue> queue = new ReferenceQueue<>(); + + /** + * Factory method used to create a new {@link Reference}. + * + * @param entry the entry contained in the reference + * @param hash the hash + * @param next the next reference in the chain, or {@code null} if none + * @return a new {@link Reference} + */ + Reference createReference( + final Entry entry, + final int hash, + @Nullable final Reference next) + { + if(ConcurrentReferenceHashMap.this.referenceType == ReferenceType.WEAK) + { + return new WeakEntryReference<>(entry, hash, next, this.queue); + } + return new SoftEntryReference<>(entry, hash, next, this.queue); + } + + // @formatter:off + /** + * Return any reference that has been garbage collected and can be purged from the underlying structure or + * {@code null} if no references need purging. This method must be thread safe and ideally should not block + * when returning {@code null}. References should be returned once and only once. + * + * @return a reference to purge or {@code null} + */ + // @formatter:on + @SuppressWarnings("unchecked") + @Nullable Reference pollForPurge() + { + return (Reference)this.queue.poll(); + } + } + + + /** + * Internal {@link Reference} implementation for {@link SoftReference SoftReferences}. + */ + private static final class SoftEntryReference extends SoftReference> implements Reference + { + private final int hash; + + private final @Nullable Reference nextReference; + + SoftEntryReference( + final Entry entry, final int hash, @Nullable final Reference next, + final ReferenceQueue> queue) + { + super(entry, queue); + this.hash = hash; + this.nextReference = next; + } + + @Override + public int getHash() + { + return this.hash; + } + + @Override + public @Nullable Reference getNext() + { + return this.nextReference; + } + + @Override + public void release() + { + this.enqueue(); + } + } + + + /** + * Internal {@link Reference} implementation for {@link WeakReference WeakReferences}. + */ + private static final class WeakEntryReference extends WeakReference> implements Reference + { + private final int hash; + + private final @Nullable Reference nextReference; + + WeakEntryReference( + final Entry entry, final int hash, @Nullable final Reference next, + final ReferenceQueue> queue) + { + + super(entry, queue); + this.hash = hash; + this.nextReference = next; + } + + @Override + public int getHash() + { + return this.hash; + } + + @Override + public @Nullable Reference getNext() + { + return this.nextReference; + } + + @Override + public void release() + { + this.enqueue(); + } + } +} diff --git a/src/main/java/software/xdev/pmd/markdown/RuleDescriptionDocMarkdownToHtmlService.java b/src/main/java/software/xdev/pmd/markdown/RuleDescriptionDocMarkdownToHtmlService.java index c9e932d..65d9da4 100644 --- a/src/main/java/software/xdev/pmd/markdown/RuleDescriptionDocMarkdownToHtmlService.java +++ b/src/main/java/software/xdev/pmd/markdown/RuleDescriptionDocMarkdownToHtmlService.java @@ -1,9 +1,9 @@ package software.xdev.pmd.markdown; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -13,7 +13,6 @@ import com.intellij.openapi.project.Project; import net.sourceforge.pmd.PMDVersion; -import software.xdev.pmd.external.org.apache.shiro.lang.util.SoftHashMap; public class RuleDescriptionDocMarkdownToHtmlService @@ -24,7 +23,7 @@ public class RuleDescriptionDocMarkdownToHtmlService private final Project project; - private final Map cache = Collections.synchronizedMap(new SoftHashMap<>()); + private final Map cache = new ConcurrentHashMap<>(); public RuleDescriptionDocMarkdownToHtmlService(@NotNull final Project project) { diff --git a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java index 72fcb84..4bb1bd1 100644 --- a/src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java +++ b/src/main/java/software/xdev/pmd/model/config/ConfigurationLocationFactory.java @@ -1,15 +1,13 @@ package software.xdev.pmd.model.config; -import java.util.Collections; import java.util.Map; import java.util.Optional; -import java.util.WeakHashMap; import org.jetbrains.annotations.NotNull; import com.intellij.openapi.project.Project; -import software.xdev.pmd.external.org.apache.shiro.lang.util.SoftHashMap; +import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; import software.xdev.pmd.model.config.bundled.BundledConfig; import software.xdev.pmd.model.config.bundled.BundledConfigurationLocation; import software.xdev.pmd.model.config.bundled.UnknownBundledConfigurationLocation; @@ -20,7 +18,7 @@ public class ConfigurationLocationFactory { private final Map createCache = - Collections.synchronizedMap(new SoftHashMap<>()); + new ConcurrentReferenceHashMap<>(); record CreateCacheKey( @@ -37,7 +35,7 @@ record CreateCacheKey( * updates to one location (e.g. a URL change) are visible to other modules with a reference to the given location. */ private final Map instanceDeduplicationCache = - Collections.synchronizedMap(new WeakHashMap<>()); + new ConcurrentReferenceHashMap<>(ConcurrentReferenceHashMap.ReferenceType.WEAK); /** * Create a new location. diff --git a/src/main/java/software/xdev/pmd/util/pmd/PMDLanguageFileTypeMapper.java b/src/main/java/software/xdev/pmd/util/pmd/PMDLanguageFileTypeMapper.java index 349a9ab..cdf31bc 100644 --- a/src/main/java/software/xdev/pmd/util/pmd/PMDLanguageFileTypeMapper.java +++ b/src/main/java/software/xdev/pmd/util/pmd/PMDLanguageFileTypeMapper.java @@ -1,6 +1,5 @@ package software.xdev.pmd.util.pmd; -import java.util.Collections; import java.util.Map; import com.intellij.openapi.fileTypes.FileType; @@ -8,12 +7,12 @@ import com.intellij.openapi.fileTypes.UnknownFileType; import net.sourceforge.pmd.lang.Language; -import software.xdev.pmd.external.org.apache.shiro.lang.util.SoftHashMap; +import software.xdev.pmd.external.org.springframework.util.ConcurrentReferenceHashMap; public class PMDLanguageFileTypeMapper { - private final Map cache = Collections.synchronizedMap(new SoftHashMap<>()); + private final Map cache = new ConcurrentReferenceHashMap<>(); public FileType get(final Language language) {