This is an automated email from the ASF dual-hosted git repository.
rskraba pushed a commit to branch branch-1.11
in repository https://gitbox.apache.org/repos/asf/avro.git
The following commit(s) were added to refs/heads/branch-1.11 by this push:
new 634daea7c AVRO-3713: [Java] Fix Map synchronization regression
634daea7c is described below
commit 634daea7c1f4f777b95482aefe71f6809a86a49c
Author: Niels Basjes <[email protected]>
AuthorDate: Fri Feb 10 10:26:12 2023 +0100
AVRO-3713: [Java] Fix Map synchronization regression
Signed-off-by: Niels Basjes <[email protected]>
---
.../java/org/apache/avro/generic/GenericData.java | 24 +-
.../apache/avro/util/springframework/Assert.java | 121 +++
.../ConcurrentReferenceHashMap.java | 1111 ++++++++++++++++++++
.../avro/util/springframework/ObjectUtils.java | 320 ++++++
.../util/springframework/ComparableComparator.java | 44 +
.../util/springframework/NullSafeComparator.java | 132 +++
.../avro/util/springframework/StopWatch.java | 415 ++++++++
.../TestConcurrentReferenceHashMap.java | 688 ++++++++++++
8 files changed, 2840 insertions(+), 15 deletions(-)
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
b/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
index ee86ddeb3..875abc7d7 100644
--- a/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
+++ b/lang/java/avro/src/main/java/org/apache/avro/generic/GenericData.java
@@ -26,7 +26,6 @@ import java.time.temporal.Temporal;
import java.util.AbstractList;
import java.util.Arrays;
import java.util.Collection;
-import java.util.Collections;
import java.util.HashMap;
import java.util.IdentityHashMap;
import java.util.Iterator;
@@ -35,7 +34,7 @@ import java.util.List;
import java.util.Map;
import java.util.ServiceLoader;
import java.util.UUID;
-import java.util.WeakHashMap;
+import java.util.concurrent.ConcurrentMap;
import org.apache.avro.AvroMissingFieldException;
import org.apache.avro.AvroRuntimeException;
@@ -60,6 +59,9 @@ import org.apache.avro.util.Utf8;
import org.apache.avro.util.internal.Accessor;
import com.fasterxml.jackson.databind.JsonNode;
+import org.apache.avro.util.springframework.ConcurrentReferenceHashMap;
+
+import static
org.apache.avro.util.springframework.ConcurrentReferenceHashMap.ReferenceType.WEAK;
/**
* Utilities for generic Java data. See {@link GenericRecordBuilder} for a
@@ -1268,7 +1270,7 @@ public class GenericData {
}
}
- private final Map<Field, Object> defaultValueCache =
Collections.synchronizedMap(new WeakHashMap<>());
+ private final ConcurrentMap<Field, Object> defaultValueCache = new
ConcurrentReferenceHashMap<>(128, WEAK);
/**
* Gets the default value of the given field, if any.
@@ -1288,28 +1290,20 @@ public class GenericData {
}
// Check the cache
- Object defaultValue = defaultValueCache.get(field);
-
// If not cached, get the default Java value by encoding the default JSON
// value and then decoding it:
- if (defaultValue == null)
+ return defaultValueCache.computeIfAbsent(field, fieldToGetValueFor -> {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BinaryEncoder encoder = EncoderFactory.get().binaryEncoder(baos, null);
- Accessor.encode(encoder, field.schema(), json);
+ Accessor.encode(encoder, fieldToGetValueFor.schema(), json);
encoder.flush();
BinaryDecoder decoder =
DecoderFactory.get().binaryDecoder(baos.toByteArray(), null);
- defaultValue = createDatumReader(field.schema()).read(null, decoder);
-
- // this MAY result in two threads creating the same defaultValue
- // and calling put. The last thread will win. However,
- // that's not an issue.
- defaultValueCache.put(field, defaultValue);
+ return createDatumReader(fieldToGetValueFor.schema()).read(null,
decoder);
} catch (IOException e) {
throw new AvroRuntimeException(e);
}
-
- return defaultValue;
+ });
}
private static final Schema STRINGS = Schema.create(Type.STRING);
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/util/springframework/Assert.java
b/lang/java/avro/src/main/java/org/apache/avro/util/springframework/Assert.java
new file mode 100644
index 000000000..70e2e9f3b
--- /dev/null
+++
b/lang/java/avro/src/main/java/org/apache/avro/util/springframework/Assert.java
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2002-2020 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 org.apache.avro.util.springframework;
+
+import org.apache.avro.reflect.Nullable;
+
+/**
+ * Assertion utility class that assists in validating arguments.
+ *
+ * <p>
+ * Useful for identifying programmer errors early and clearly at runtime.
+ *
+ * <p>
+ * For example, if the contract of a public method states it does not allow
+ * {@code null} arguments, {@code Assert} can be used to validate that
contract.
+ * Doing this clearly indicates a contract violation when it occurs and
protects
+ * the class's invariants.
+ *
+ * <p>
+ * Typically used to validate method arguments rather than configuration
+ * properties, to check for cases that are usually programmer errors rather
than
+ * configuration errors. In contrast to configuration initialization code,
there
+ * is usually no point in falling back to defaults in such methods.
+ *
+ * <p>
+ * This class is similar to JUnit's assertion library. If an argument value is
+ * deemed invalid, an {@link IllegalArgumentException} is thrown (typically).
+ * For example:
+ *
+ * <pre class="code">
+ * Assert.notNull(clazz, "The class must not be null");
+ * Assert.isTrue(i > 0, "The value must be greater than zero");
+ * </pre>
+ *
+ * <p>
+ * Mainly for internal use within the framework; for a more comprehensive suite
+ * of assertion utilities consider {@code org.apache.commons.lang3.Validate}
+ * from <a href="https://commons.apache.org/proper/commons-lang/">Apache
Commons
+ * Lang</a>, Google Guava's <a href=
+ *
"https://github.com/google/guava/wiki/PreconditionsExplained">Preconditions</a>,
+ * or similar third-party libraries.
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @author Colin Sampaleanu
+ * @author Rob Harrop
+ * @since 1.1.2
+ */
+class Assert {
+ private Assert() {
+ }
+
+ /**
+ * Assert a boolean expression, throwing an {@code IllegalStateException} if
the
+ * expression evaluates to {@code false}.
+ *
+ * <pre class="code">
+ * Assert.state(id == null, "The id property must not already be
initialized");
+ * </pre>
+ *
+ * @param expression a boolean expression
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalStateException if {@code expression} is {@code false}
+ */
+ public static void state(boolean expression, String message) {
+ if (!expression) {
+ throw new IllegalStateException(message);
+ }
+ }
+
+ /**
+ * Assert a boolean expression, throwing an {@code IllegalArgumentException}
if
+ * the expression evaluates to {@code false}.
+ *
+ * <pre class="code">
+ * Assert.isTrue(i > 0, "The value must be greater than zero");
+ * </pre>
+ *
+ * @param expression a boolean expression
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if {@code expression} is {@code false}
+ */
+ public static void isTrue(boolean expression, String message) {
+ if (!expression) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+ /**
+ * Assert that an object is not {@code null}.
+ *
+ * <pre class="code">
+ * Assert.notNull(clazz, "The class must not be null");
+ * </pre>
+ *
+ * @param object the object to check
+ * @param message the exception message to use if the assertion fails
+ * @throws IllegalArgumentException if the object is {@code null}
+ */
+ public static void notNull(@Nullable Object object, String message) {
+ if (object == null) {
+ throw new IllegalArgumentException(message);
+ }
+ }
+
+}
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/util/springframework/ConcurrentReferenceHashMap.java
b/lang/java/avro/src/main/java/org/apache/avro/util/springframework/ConcurrentReferenceHashMap.java
new file mode 100644
index 000000000..1a137cf21
--- /dev/null
+++
b/lang/java/avro/src/main/java/org/apache/avro/util/springframework/ConcurrentReferenceHashMap.java
@@ -0,0 +1,1111 @@
+/*
+ * Copyright 2002-2021 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 org.apache.avro.util.springframework;
+
+import org.apache.avro.reflect.Nullable;
+
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.SoftReference;
+import java.lang.ref.WeakReference;
+import java.lang.reflect.Array;
+import java.util.AbstractMap;
+import java.util.AbstractSet;
+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.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * A {@link ConcurrentHashMap} that uses {@link ReferenceType#SOFT soft} or
+ * {@linkplain ReferenceType#WEAK weak} references for both {@code keys} and
+ * {@code values}.
+ *
+ * <p>
+ * This class can be used as an alternative to
+ * {@code Collections.synchronizedMap(new WeakHashMap<K, Reference<V>>())} 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.
+ *
+ * <p>
+ * <b>NOTE:</b> 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.
+ *
+ * <p>
+ * If not explicitly specified, this implementation will use
+ * {@linkplain SoftReference soft entry references}.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ * @author Phillip Webb
+ * @author Juergen Hoeller
+ * @since 3.2
+ */
+public class ConcurrentReferenceHashMap<K, V> extends AbstractMap<K, V>
implements ConcurrentMap<K, V> {
+
+ 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.
+ */
+ @Nullable
+ private volatile Set<Map.Entry<K, V>> entrySet;
+
+ /**
+ * 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 initialCapacity the initial capacity of the map
+ */
+ public ConcurrentReferenceHashMap(int initialCapacity) {
+ this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL,
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
+ */
+ public ConcurrentReferenceHashMap(int initialCapacity, 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(int initialCapacity, 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(int initialCapacity, 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(int initialCapacity, float loadFactor, 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(int initialCapacity, float loadFactor, int
concurrencyLevel,
+ ReferenceType referenceType) {
+
+ Assert.isTrue(initialCapacity >= 0, "Initial capacity must not be
negative");
+ Assert.isTrue(loadFactor > 0f, "Load factor must be positive");
+ Assert.isTrue(concurrencyLevel > 0, "Concurrency level must be positive");
+ Assert.notNull(referenceType, "Reference type must not be null");
+ this.loadFactor = loadFactor;
+ this.shift = calculateShift(concurrencyLevel, MAXIMUM_CONCURRENCY_LEVEL);
+ int size = 1 << this.shift;
+ this.referenceType = referenceType;
+ int roundedUpSegmentCapacity = (int) ((initialCapacity + size - 1L) /
size);
+ int initialSize = 1 << calculateShift(roundedUpSegmentCapacity,
MAXIMUM_SEGMENT_SIZE);
+ Segment[] segments = (Segment[]) Array.newInstance(Segment.class, size);
+ int resizeThreshold = (int) (initialSize * 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(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();
+ }
+
+ /**
+ * 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
+ */
+ protected int getHash(@Nullable 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
+ @Nullable
+ public V get(@Nullable Object key) {
+ Reference<K, V> ref = getReference(key, Restructure.WHEN_NECESSARY);
+ Entry<K, V> entry = (ref != null ? ref.get() : null);
+ return (entry != null ? entry.getValue() : null);
+ }
+
+ @Override
+ @Nullable
+ public V getOrDefault(@Nullable Object key, @Nullable V defaultValue) {
+ Reference<K, V> ref = getReference(key, Restructure.WHEN_NECESSARY);
+ Entry<K, V> entry = (ref != null ? ref.get() : null);
+ return (entry != null ? entry.getValue() : defaultValue);
+ }
+
+ @Override
+ public boolean containsKey(@Nullable Object key) {
+ Reference<K, V> ref = getReference(key, Restructure.WHEN_NECESSARY);
+ Entry<K, V> entry = (ref != null ? ref.get() : null);
+ return (entry != null && ObjectUtils.nullSafeEquals(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
+ */
+ @Nullable
+ protected final Reference<K, V> getReference(@Nullable Object key,
Restructure restructure) {
+ int hash = getHash(key);
+ return getSegmentForHash(hash).getReference(key, hash, restructure);
+ }
+
+ @Override
+ @Nullable
+ public V put(@Nullable K key, @Nullable V value) {
+ return put(key, value, true);
+ }
+
+ @Override
+ @Nullable
+ public V putIfAbsent(@Nullable K key, @Nullable V value) {
+ return put(key, value, false);
+ }
+
+ @Nullable
+ private V put(@Nullable final K key, @Nullable final V value, final boolean
overwriteExisting) {
+ return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE,
TaskOption.RESIZE) {
+ @Override
+ @Nullable
+ protected V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V>
entry, @Nullable Entries<V> entries) {
+ if (entry != null) {
+ V oldValue = entry.getValue();
+ if (overwriteExisting) {
+ entry.setValue(value);
+ }
+ return oldValue;
+ }
+ Assert.state(entries != null, "No entries segment");
+ entries.add(value);
+ return null;
+ }
+ });
+ }
+
+ @Override
+ @Nullable
+ public V remove(@Nullable Object key) {
+ return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_AFTER,
TaskOption.SKIP_IF_EMPTY) {
+ @Override
+ @Nullable
+ protected V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V>
entry) {
+ if (entry != null) {
+ if (ref != null) {
+ ref.release();
+ }
+ return entry.value;
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public boolean remove(@Nullable Object key, final @Nullable Object value) {
+ Boolean result = doTask(key, new
Task<Boolean>(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) {
+ @Override
+ protected Boolean execute(@Nullable Reference<K, V> ref, @Nullable
Entry<K, V> entry) {
+ if (entry != null && ObjectUtils.nullSafeEquals(entry.getValue(),
value)) {
+ if (ref != null) {
+ ref.release();
+ }
+ return true;
+ }
+ return false;
+ }
+ });
+ return (Boolean.TRUE.equals(result));
+ }
+
+ @Override
+ public boolean replace(@Nullable K key, final @Nullable V oldValue, final
@Nullable V newValue) {
+ Boolean result = doTask(key, new
Task<Boolean>(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) {
+ @Override
+ protected Boolean execute(@Nullable Reference<K, V> ref, @Nullable
Entry<K, V> entry) {
+ if (entry != null && ObjectUtils.nullSafeEquals(entry.getValue(),
oldValue)) {
+ entry.setValue(newValue);
+ return true;
+ }
+ return false;
+ }
+ });
+ return (Boolean.TRUE.equals(result));
+ }
+
+ @Override
+ @Nullable
+ public V replace(@Nullable K key, final @Nullable V value) {
+ return doTask(key, new Task<V>(TaskOption.RESTRUCTURE_BEFORE,
TaskOption.SKIP_IF_EMPTY) {
+ @Override
+ @Nullable
+ protected V execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V>
entry) {
+ if (entry != null) {
+ V oldValue = entry.getValue();
+ entry.setValue(value);
+ return oldValue;
+ }
+ return null;
+ }
+ });
+ }
+
+ @Override
+ public void clear() {
+ for (Segment segment : this.segments) {
+ segment.clear();
+ }
+ }
+
+ /**
+ * 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.
+ */
+ public void purgeUnreferencedEntries() {
+ for (Segment segment : this.segments) {
+ segment.restructureIfNecessary(false);
+ }
+ }
+
+ @Override
+ public int size() {
+ int size = 0;
+ for (Segment segment : this.segments) {
+ size += segment.getCount();
+ }
+ return size;
+ }
+
+ @Override
+ public boolean isEmpty() {
+ for (Segment segment : this.segments) {
+ if (segment.getCount() > 0) {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @Override
+ public Set<Map.Entry<K, V>> entrySet() {
+ Set<Map.Entry<K, V>> entrySet = this.entrySet;
+ if (entrySet == null) {
+ entrySet = new EntrySet();
+ this.entrySet = entrySet;
+ }
+ return entrySet;
+ }
+
+ @Nullable
+ private <T> T doTask(@Nullable Object key, Task<T> task) {
+ int hash = getHash(key);
+ return getSegmentForHash(hash).doTask(hash, key, task);
+ }
+
+ private Segment getSegmentForHash(int hash) {
+ return this.segments[(hash >>> (32 - this.shift)) & (this.segments.length
- 1)];
+ }
+
+ /**
+ * 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)
+ */
+ protected static int calculateShift(int minimumValue, 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.
+ */
+ @SuppressWarnings("serial")
+ 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}.
+ */
+ private volatile Reference<K, V>[] 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;
+
+ public Segment(int initialSize, int resizeThreshold) {
+ this.referenceManager = createReferenceManager();
+ this.initialSize = initialSize;
+ this.references = createReferenceArray(initialSize);
+ this.resizeThreshold = resizeThreshold;
+ }
+
+ @Nullable
+ public Reference<K, V> getReference(@Nullable Object key, int hash,
Restructure restructure) {
+ if (restructure == Restructure.WHEN_NECESSARY) {
+ restructureIfNecessary(false);
+ }
+ if (this.count.get() == 0) {
+ return null;
+ }
+ // Use a local copy to protect against other threads writing
+ Reference<K, V>[] references = this.references;
+ int index = getIndex(hash, references);
+ Reference<K, V> head = references[index];
+ return 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
+ */
+ @Nullable
+ public <T> T doTask(final int hash, @Nullable final Object key, final
Task<T> task) {
+ boolean resize = task.hasOption(TaskOption.RESIZE);
+ if (task.hasOption(TaskOption.RESTRUCTURE_BEFORE)) {
+ restructureIfNecessary(resize);
+ }
+ if (task.hasOption(TaskOption.SKIP_IF_EMPTY) && this.count.get() == 0) {
+ return task.execute(null, null, null);
+ }
+ lock();
+ try {
+ final int index = getIndex(hash, this.references);
+ final Reference<K, V> head = this.references[index];
+ Reference<K, V> ref = findInChain(head, key, hash);
+ Entry<K, V> entry = (ref != null ? ref.get() : null);
+ Entries<V> entries = value -> {
+ @SuppressWarnings("unchecked")
+ Entry<K, V> newEntry = new Entry<>((K) key, value);
+ Reference<K, V> newReference =
Segment.this.referenceManager.createReference(newEntry, hash, head);
+ Segment.this.references[index] = newReference;
+ Segment.this.count.incrementAndGet();
+ };
+ return task.execute(ref, entry, entries);
+ } finally {
+ unlock();
+ if (task.hasOption(TaskOption.RESTRUCTURE_AFTER)) {
+ restructureIfNecessary(resize);
+ }
+ }
+ }
+
+ /**
+ * Clear all items from this segment.
+ */
+ public void clear() {
+ if (this.count.get() == 0) {
+ return;
+ }
+ lock();
+ try {
+ this.references = createReferenceArray(this.initialSize);
+ this.resizeThreshold = (int) (this.references.length *
getLoadFactor());
+ this.count.set(0);
+ } finally {
+ 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
+ */
+ private void restructureIfNecessary(boolean allowResize) {
+ int currCount = this.count.get();
+ boolean needsResize = allowResize && (currCount > 0 && currCount >=
this.resizeThreshold);
+ Reference<K, V> ref = this.referenceManager.pollForPurge();
+ if (ref != null || (needsResize)) {
+ restructure(allowResize, ref);
+ }
+ }
+
+ private void restructure(boolean allowResize, @Nullable Reference<K, V>
ref) {
+ boolean needsResize;
+ lock();
+ try {
+ int countAfterRestructure = this.count.get();
+ Set<Reference<K, V>> toPurge = Collections.emptySet();
+ if (ref != null) {
+ toPurge = new HashSet<>();
+ while (ref != null) {
+ toPurge.add(ref);
+ ref = this.referenceManager.pollForPurge();
+ }
+ }
+ countAfterRestructure -= toPurge.size();
+
+ // Recalculate taking into account count inside lock and items that
+ // will be purged
+ needsResize = (countAfterRestructure > 0 && countAfterRestructure >=
this.resizeThreshold);
+ boolean resizing = false;
+ int restructureSize = this.references.length;
+ if (allowResize && needsResize && restructureSize <
MAXIMUM_SEGMENT_SIZE) {
+ restructureSize <<= 1;
+ resizing = true;
+ }
+
+ // Either create a new table or reuse the existing one
+ Reference<K, V>[] restructured = (resizing ?
createReferenceArray(restructureSize) : this.references);
+
+ // Restructure
+ for (int i = 0; i < this.references.length; i++) {
+ ref = this.references[i];
+ if (!resizing) {
+ restructured[i] = null;
+ }
+ while (ref != null) {
+ if (!toPurge.contains(ref)) {
+ Entry<K, V> entry = ref.get();
+ if (entry != null) {
+ int index = getIndex(ref.getHash(), restructured);
+ restructured[index] =
this.referenceManager.createReference(entry, ref.getHash(),
restructured[index]);
+ }
+ }
+ ref = ref.getNext();
+ }
+ }
+
+ // Replace volatile members
+ if (resizing) {
+ this.references = restructured;
+ this.resizeThreshold = (int) (this.references.length *
getLoadFactor());
+ }
+ this.count.set(Math.max(countAfterRestructure, 0));
+ } finally {
+ unlock();
+ }
+ }
+
+ @Nullable
+ private Reference<K, V> findInChain(Reference<K, V> ref, @Nullable Object
key, int hash) {
+ Reference<K, V> currRef = ref;
+ while (currRef != null) {
+ if (currRef.getHash() == hash) {
+ Entry<K, V> entry = currRef.get();
+ if (entry != null) {
+ K entryKey = entry.getKey();
+ if (ObjectUtils.nullSafeEquals(entryKey, key)) {
+ return currRef;
+ }
+ }
+ }
+ currRef = currRef.getNext();
+ }
+ return null;
+ }
+
+ @SuppressWarnings({ "unchecked" })
+ private Reference<K, V>[] createReferenceArray(int size) {
+ return new Reference[size];
+ }
+
+ private int getIndex(int hash, Reference<K, V>[] references) {
+ return (hash & (references.length - 1));
+ }
+
+ /**
+ * Return the size of the current references array.
+ */
+ public int getSize() {
+ return this.references.length;
+ }
+
+ /**
+ * Return the total number of references in this segment.
+ */
+ public 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 (e.g.,
+ * {@link SoftReference}).
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+ protected interface Reference<K, V> {
+
+ /**
+ * Return the referenced entry, or {@code null} if the entry is no longer
+ * available.
+ */
+ @Nullable
+ Entry<K, V> get();
+
+ /**
+ * Return the hash for the reference.
+ */
+ int getHash();
+
+ /**
+ * Return the next reference in the chain, or {@code null} if none.
+ */
+ @Nullable
+ Reference<K, V> getNext();
+
+ /**
+ * Release this entry and ensure that it will be returned from
+ * {@code ReferenceManager#pollForPurge()}.
+ */
+ void release();
+ }
+
+ /**
+ * A single map entry.
+ *
+ * @param <K> the key type
+ * @param <V> the value type
+ */
+ protected static final class Entry<K, V> implements Map.Entry<K, V> {
+
+ @Nullable
+ private final K key;
+
+ @Nullable
+ private volatile V value;
+
+ public Entry(@Nullable K key, @Nullable V value) {
+ this.key = key;
+ this.value = value;
+ }
+
+ @Override
+ @Nullable
+ public K getKey() {
+ return this.key;
+ }
+
+ @Override
+ @Nullable
+ public V getValue() {
+ return this.value;
+ }
+
+ @Override
+ @Nullable
+ public V setValue(@Nullable V value) {
+ V previous = this.value;
+ this.value = value;
+ return previous;
+ }
+
+ @Override
+ public String toString() {
+ return (this.key + "=" + this.value);
+ }
+
+ @Override
+ @SuppressWarnings("rawtypes")
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof Map.Entry)) {
+ return false;
+ }
+ Map.Entry otherEntry = (Map.Entry) other;
+ return (ObjectUtils.nullSafeEquals(getKey(), otherEntry.getKey())
+ && ObjectUtils.nullSafeEquals(getValue(), otherEntry.getValue()));
+ }
+
+ @Override
+ public int hashCode() {
+ return (ObjectUtils.nullSafeHashCode(this.key) ^
ObjectUtils.nullSafeHashCode(this.value));
+ }
+ }
+
+ /**
+ * A task that can be {@link Segment#doTask run} against a {@link Segment}.
+ */
+ private abstract class Task<T> {
+
+ private final EnumSet<TaskOption> options;
+
+ public Task(TaskOption... options) {
+ this.options = (options.length == 0 ? EnumSet.noneOf(TaskOption.class) :
EnumSet.of(options[0], options));
+ }
+
+ public boolean hasOption(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)
+ */
+ @Nullable
+ protected T execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V>
entry, @Nullable Entries<V> entries) {
+ return 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)
+ */
+ @Nullable
+ protected T execute(@Nullable Reference<K, V> ref, @Nullable Entry<K, V>
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 Segment} entries.
+ */
+ private interface Entries<V> {
+
+ /**
+ * Add a new entry with the specified value.
+ *
+ * @param value the value to add
+ */
+ void add(@Nullable V value);
+ }
+
+ /**
+ * Internal entry-set implementation.
+ */
+ private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
+
+ @Override
+ public Iterator<Map.Entry<K, V>> iterator() {
+ return new EntryIterator();
+ }
+
+ @Override
+ public boolean contains(@Nullable Object o) {
+ if (o instanceof Map.Entry<?, ?>) {
+ Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
+ Reference<K, V> ref =
ConcurrentReferenceHashMap.this.getReference(entry.getKey(), Restructure.NEVER);
+ Entry<K, V> otherEntry = (ref != null ? ref.get() : null);
+ if (otherEntry != null) {
+ return ObjectUtils.nullSafeEquals(entry.getValue(),
otherEntry.getValue());
+ }
+ }
+ return false;
+ }
+
+ @Override
+ public boolean remove(Object o) {
+ if (o instanceof Map.Entry<?, ?>) {
+ Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
+ 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();
+ }
+ }
+
+ /**
+ * Internal entry iterator implementation.
+ */
+ private class EntryIterator implements Iterator<Map.Entry<K, V>> {
+
+ private int segmentIndex;
+
+ private int referenceIndex;
+
+ @Nullable
+ private Reference<K, V>[] references;
+
+ @Nullable
+ private Reference<K, V> reference;
+
+ @Nullable
+ private Entry<K, V> next;
+
+ @Nullable
+ private Entry<K, V> last;
+
+ public EntryIterator() {
+ moveToNextSegment();
+ }
+
+ @Override
+ public boolean hasNext() {
+ getNextIfNecessary();
+ return (this.next != null);
+ }
+
+ @Override
+ public Entry<K, V> next() {
+ 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) {
+ 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) {
+ 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() {
+ Assert.state(this.last != null, "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
+ }
+
+ /**
+ * Strategy class used to manage {@link Reference References}. This class
can be
+ * overridden if alternative reference types need to be supported.
+ */
+ protected class ReferenceManager {
+
+ private final ReferenceQueue<Entry<K, V>> 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}
+ */
+ public Reference<K, V> createReference(Entry<K, V> entry, int hash,
@Nullable Reference<K, V> next) {
+ if (ConcurrentReferenceHashMap.this.referenceType == ReferenceType.WEAK)
{
+ return new WeakEntryReference<>(entry, hash, next, this.queue);
+ }
+ return new SoftEntryReference<>(entry, hash, next, this.queue);
+ }
+
+ /**
+ * 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}
+ */
+ @SuppressWarnings("unchecked")
+ @Nullable
+ public Reference<K, V> pollForPurge() {
+ return (Reference<K, V>) this.queue.poll();
+ }
+ }
+
+ /**
+ * Internal {@link Reference} implementation for {@link SoftReference
+ * SoftReferences}.
+ */
+ private static final class SoftEntryReference<K, V> extends
SoftReference<Entry<K, V>> implements Reference<K, V> {
+
+ private final int hash;
+
+ @Nullable
+ private final Reference<K, V> nextReference;
+
+ public SoftEntryReference(Entry<K, V> entry, int hash, @Nullable
Reference<K, V> next,
+ ReferenceQueue<Entry<K, V>> queue) {
+
+ super(entry, queue);
+ this.hash = hash;
+ this.nextReference = next;
+ }
+
+ @Override
+ public int getHash() {
+ return this.hash;
+ }
+
+ @Override
+ @Nullable
+ public Reference<K, V> getNext() {
+ return this.nextReference;
+ }
+
+ @Override
+ public void release() {
+ enqueue();
+ clear();
+ }
+ }
+
+ /**
+ * Internal {@link Reference} implementation for {@link WeakReference
+ * WeakReferences}.
+ */
+ private static final class WeakEntryReference<K, V> extends
WeakReference<Entry<K, V>> implements Reference<K, V> {
+
+ private final int hash;
+
+ @Nullable
+ private final Reference<K, V> nextReference;
+
+ public WeakEntryReference(Entry<K, V> entry, int hash, @Nullable
Reference<K, V> next,
+ ReferenceQueue<Entry<K, V>> queue) {
+
+ super(entry, queue);
+ this.hash = hash;
+ this.nextReference = next;
+ }
+
+ @Override
+ public int getHash() {
+ return this.hash;
+ }
+
+ @Override
+ @Nullable
+ public Reference<K, V> getNext() {
+ return this.nextReference;
+ }
+
+ @Override
+ public void release() {
+ enqueue();
+ clear();
+ }
+ }
+
+}
diff --git
a/lang/java/avro/src/main/java/org/apache/avro/util/springframework/ObjectUtils.java
b/lang/java/avro/src/main/java/org/apache/avro/util/springframework/ObjectUtils.java
new file mode 100644
index 000000000..a8e0c4518
--- /dev/null
+++
b/lang/java/avro/src/main/java/org/apache/avro/util/springframework/ObjectUtils.java
@@ -0,0 +1,320 @@
+/*
+ * Copyright 2002-2021 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 org.apache.avro.util.springframework;
+
+import org.apache.avro.reflect.Nullable;
+import org.apache.avro.util.ClassUtils;
+
+import java.util.Arrays;
+
+/**
+ * Miscellaneous object utility methods.
+ *
+ * <p>
+ * Mainly for internal use within the framework.
+ *
+ * <p>
+ * Thanks to Alex Ruiz for contributing several enhancements to this class!
+ *
+ * @author Juergen Hoeller
+ * @author Keith Donald
+ * @author Rod Johnson
+ * @author Rob Harrop
+ * @author Chris Beams
+ * @author Sam Brannen
+ * @see ClassUtils see CollectionUtils see StringUtils
+ * @since 19.03.2004
+ */
+class ObjectUtils {
+ private ObjectUtils() {
+ }
+
+ private static final int INITIAL_HASH = 7;
+ private static final int MULTIPLIER = 31;
+
+ /**
+ * Determine whether the given array is empty: i.e. {@code null} or of zero
+ * length.
+ *
+ * @param array the array to check
+ */
+ public static boolean isEmpty(@Nullable Object[] array) {
+ return (array == null || array.length == 0);
+ }
+
+ // ---------------------------------------------------------------------
+ // Convenience methods for content-based equality/hash-code handling
+ // ---------------------------------------------------------------------
+
+ /**
+ * Determine if the given objects are equal, returning {@code true} if both
are
+ * {@code null} or {@code false} if only one is {@code null}.
+ * <p>
+ * Compares arrays with {@code Arrays.equals}, performing an equality check
+ * based on the array elements rather than the array reference.
+ *
+ * @param o1 first Object to compare
+ * @param o2 second Object to compare
+ * @return whether the given objects are equal
+ * @see Object#equals(Object)
+ * @see Arrays#equals
+ */
+ public static boolean nullSafeEquals(@Nullable Object o1, @Nullable Object
o2) {
+ if (o1 == o2) {
+ return true;
+ }
+ if (o1 == null || o2 == null) {
+ return false;
+ }
+ if (o1.equals(o2)) {
+ return true;
+ }
+ if (o1.getClass().isArray() && o2.getClass().isArray()) {
+ return arrayEquals(o1, o2);
+ }
+ return false;
+ }
+
+ /**
+ * Compare the given arrays with {@code Arrays.equals}, performing an
equality
+ * check based on the array elements rather than the array reference.
+ *
+ * @param o1 first array to compare
+ * @param o2 second array to compare
+ * @return whether the given objects are equal
+ * @see #nullSafeEquals(Object, Object)
+ * @see Arrays#equals
+ */
+ private static boolean arrayEquals(Object o1, Object o2) {
+ if (o1 instanceof Object[] && o2 instanceof Object[]) {
+ return Arrays.equals((Object[]) o1, (Object[]) o2);
+ }
+ if (o1 instanceof boolean[] && o2 instanceof boolean[]) {
+ return Arrays.equals((boolean[]) o1, (boolean[]) o2);
+ }
+ if (o1 instanceof byte[] && o2 instanceof byte[]) {
+ return Arrays.equals((byte[]) o1, (byte[]) o2);
+ }
+ if (o1 instanceof char[] && o2 instanceof char[]) {
+ return Arrays.equals((char[]) o1, (char[]) o2);
+ }
+ if (o1 instanceof double[] && o2 instanceof double[]) {
+ return Arrays.equals((double[]) o1, (double[]) o2);
+ }
+ if (o1 instanceof float[] && o2 instanceof float[]) {
+ return Arrays.equals((float[]) o1, (float[]) o2);
+ }
+ if (o1 instanceof int[] && o2 instanceof int[]) {
+ return Arrays.equals((int[]) o1, (int[]) o2);
+ }
+ if (o1 instanceof long[] && o2 instanceof long[]) {
+ return Arrays.equals((long[]) o1, (long[]) o2);
+ }
+ if (o1 instanceof short[] && o2 instanceof short[]) {
+ return Arrays.equals((short[]) o1, (short[]) o2);
+ }
+ return false;
+ }
+
+ /**
+ * Return as hash code for the given object; typically the value of
+ * {@code Object#hashCode()}}. If the object is an array, this method will
+ * delegate to any of the {@code nullSafeHashCode} methods for arrays in this
+ * class. If the object is {@code null}, this method returns 0.
+ *
+ * @see Object#hashCode()
+ * @see #nullSafeHashCode(Object[])
+ * @see #nullSafeHashCode(boolean[])
+ * @see #nullSafeHashCode(byte[])
+ * @see #nullSafeHashCode(char[])
+ * @see #nullSafeHashCode(double[])
+ * @see #nullSafeHashCode(float[])
+ * @see #nullSafeHashCode(int[])
+ * @see #nullSafeHashCode(long[])
+ * @see #nullSafeHashCode(short[])
+ */
+ public static int nullSafeHashCode(@Nullable Object obj) {
+ if (obj == null) {
+ return 0;
+ }
+ if (obj.getClass().isArray()) {
+ if (obj instanceof Object[]) {
+ return nullSafeHashCode((Object[]) obj);
+ }
+ if (obj instanceof boolean[]) {
+ return nullSafeHashCode((boolean[]) obj);
+ }
+ if (obj instanceof byte[]) {
+ return nullSafeHashCode((byte[]) obj);
+ }
+ if (obj instanceof char[]) {
+ return nullSafeHashCode((char[]) obj);
+ }
+ if (obj instanceof double[]) {
+ return nullSafeHashCode((double[]) obj);
+ }
+ if (obj instanceof float[]) {
+ return nullSafeHashCode((float[]) obj);
+ }
+ if (obj instanceof int[]) {
+ return nullSafeHashCode((int[]) obj);
+ }
+ if (obj instanceof long[]) {
+ return nullSafeHashCode((long[]) obj);
+ }
+ if (obj instanceof short[]) {
+ return nullSafeHashCode((short[]) obj);
+ }
+ }
+ return obj.hashCode();
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable Object[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (Object element : array) {
+ hash = MULTIPLIER * hash + nullSafeHashCode(element);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable boolean[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (boolean element : array) {
+ hash = MULTIPLIER * hash + Boolean.hashCode(element);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable byte[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (byte element : array) {
+ hash = MULTIPLIER * hash + element;
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable char[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (char element : array) {
+ hash = MULTIPLIER * hash + element;
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable double[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (double element : array) {
+ hash = MULTIPLIER * hash + Double.hashCode(element);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable float[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (float element : array) {
+ hash = MULTIPLIER * hash + Float.hashCode(element);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable int[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (int element : array) {
+ hash = MULTIPLIER * hash + element;
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable long[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (long element : array) {
+ hash = MULTIPLIER * hash + Long.hashCode(element);
+ }
+ return hash;
+ }
+
+ /**
+ * Return a hash code based on the contents of the specified array. If
+ * {@code array} is {@code null}, this method returns 0.
+ */
+ public static int nullSafeHashCode(@Nullable short[] array) {
+ if (array == null) {
+ return 0;
+ }
+ int hash = INITIAL_HASH;
+ for (short element : array) {
+ hash = MULTIPLIER * hash + element;
+ }
+ return hash;
+ }
+}
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/util/springframework/ComparableComparator.java
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/ComparableComparator.java
new file mode 100644
index 000000000..54c887cc1
--- /dev/null
+++
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/ComparableComparator.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2002-2018 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 org.apache.avro.util.springframework;
+
+import java.util.Comparator;
+
+/**
+ * Comparator that adapts Comparables to the Comparator interface. Mainly for
+ * internal use in other Comparators, when supposed to work on Comparables.
+ *
+ * @author Keith Donald
+ * @since 1.2.2
+ * @param <T> the type of comparable objects that may be compared by this
+ * comparator
+ * @see Comparable
+ */
+class ComparableComparator<T extends Comparable<T>> implements Comparator<T> {
+
+ /**
+ * A shared instance of this default comparator. see Comparators#comparable()
+ */
+ @SuppressWarnings("rawtypes")
+ public static final ComparableComparator INSTANCE = new
ComparableComparator();
+
+ @Override
+ public int compare(T o1, T o2) {
+ return o1.compareTo(o2);
+ }
+
+}
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/util/springframework/NullSafeComparator.java
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/NullSafeComparator.java
new file mode 100644
index 000000000..f621abfe4
--- /dev/null
+++
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/NullSafeComparator.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright 2002-2018 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 org.apache.avro.util.springframework;
+
+import org.apache.avro.reflect.Nullable;
+
+import java.util.Comparator;
+
+/**
+ * A Comparator that will safely compare nulls to be lower or higher than other
+ * objects. Can decorate a given Comparator or work on Comparables.
+ *
+ * @author Keith Donald
+ * @author Juergen Hoeller
+ * @since 1.2.2
+ * @param <T> the type of objects that may be compared by this comparator
+ * @see Comparable
+ */
+class NullSafeComparator<T> implements Comparator<T> {
+
+ /**
+ * A shared default instance of this comparator, treating nulls lower than
+ * non-null objects. see Comparators#nullsLow()
+ */
+ @SuppressWarnings("rawtypes")
+ public static final NullSafeComparator NULLS_LOW = new
NullSafeComparator<>(true);
+
+ /**
+ * A shared default instance of this comparator, treating nulls higher than
+ * non-null objects. see Comparators#nullsHigh()
+ */
+ @SuppressWarnings("rawtypes")
+ public static final NullSafeComparator NULLS_HIGH = new
NullSafeComparator<>(false);
+
+ private final Comparator<T> nonNullComparator;
+
+ private final boolean nullsLow;
+
+ /**
+ * Create a NullSafeComparator that sorts {@code null} based on the provided
+ * flag, working on Comparables.
+ * <p>
+ * When comparing two non-null objects, their Comparable implementation will
be
+ * used: this means that non-null elements (that this Comparator will be
applied
+ * to) need to implement Comparable.
+ * <p>
+ * As a convenience, you can use the default shared instances:
+ * {@code NullSafeComparator.NULLS_LOW} and
+ * {@code NullSafeComparator.NULLS_HIGH}.
+ *
+ * @param nullsLow whether to treat nulls lower or higher than non-null
objects
+ * @see Comparable
+ * @see #NULLS_LOW
+ * @see #NULLS_HIGH
+ */
+ @SuppressWarnings("unchecked")
+ private NullSafeComparator(boolean nullsLow) {
+ this.nonNullComparator = ComparableComparator.INSTANCE;
+ this.nullsLow = nullsLow;
+ }
+
+ /**
+ * Create a NullSafeComparator that sorts {@code null} based on the provided
+ * flag, decorating the given Comparator.
+ * <p>
+ * When comparing two non-null objects, the specified Comparator will be
used.
+ * The given underlying Comparator must be able to handle the elements that
this
+ * Comparator will be applied to.
+ *
+ * @param comparator the comparator to use when comparing two non-null
objects
+ * @param nullsLow whether to treat nulls lower or higher than non-null
+ * objects
+ */
+ public NullSafeComparator(Comparator<T> comparator, boolean nullsLow) {
+ // Assert.notNull(comparator, "Non-null Comparator is required");
+ this.nonNullComparator = comparator;
+ this.nullsLow = nullsLow;
+ }
+
+ @Override
+ public int compare(@Nullable T o1, @Nullable T o2) {
+ if (o1 == o2) {
+ return 0;
+ }
+ if (o1 == null) {
+ return (this.nullsLow ? -1 : 1);
+ }
+ if (o2 == null) {
+ return (this.nullsLow ? 1 : -1);
+ }
+ return this.nonNullComparator.compare(o1, o2);
+ }
+
+ @Override
+ @SuppressWarnings("unchecked")
+ public boolean equals(@Nullable Object other) {
+ if (this == other) {
+ return true;
+ }
+ if (!(other instanceof NullSafeComparator)) {
+ return false;
+ }
+ NullSafeComparator<T> otherComp = (NullSafeComparator<T>) other;
+ return (this.nonNullComparator.equals(otherComp.nonNullComparator) &&
this.nullsLow == otherComp.nullsLow);
+ }
+
+ @Override
+ public int hashCode() {
+ return this.nonNullComparator.hashCode() * (this.nullsLow ? -1 : 1);
+ }
+
+ @Override
+ public String toString() {
+ return "NullSafeComparator: non-null comparator [" +
this.nonNullComparator + "]; "
+ + (this.nullsLow ? "nulls low" : "nulls high");
+ }
+
+}
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/util/springframework/StopWatch.java
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/StopWatch.java
new file mode 100644
index 000000000..10131fa30
--- /dev/null
+++
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/StopWatch.java
@@ -0,0 +1,415 @@
+/*
+ * Copyright 2002-2021 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 org.apache.avro.util.springframework;
+
+import org.apache.avro.reflect.Nullable;
+
+import java.text.NumberFormat;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Simple stop watch, allowing for timing of a number of tasks, exposing total
+ * running time and running time for each named task.
+ *
+ * <p>
+ * Conceals use of {@link System#nanoTime()}, improving the readability of
+ * application code and reducing the likelihood of calculation errors.
+ *
+ * <p>
+ * Note that this object is not designed to be thread-safe and does not use
+ * synchronization.
+ *
+ * <p>
+ * This class is normally used to verify performance during proof-of-concept
+ * work and in development, rather than as part of production applications.
+ *
+ * <p>
+ * As of Spring Framework 5.2, running time is tracked and reported in
+ * nanoseconds.
+ *
+ * @author Rod Johnson
+ * @author Juergen Hoeller
+ * @author Sam Brannen
+ * @since May 2, 2001
+ */
+class StopWatch {
+
+ /**
+ * Identifier of this {@code StopWatch}.
+ * <p>
+ * Handy when we have output from multiple stop watches and need to
distinguish
+ * between them in log or console output.
+ */
+ private final String id;
+
+ private boolean keepTaskList = true;
+
+ private final List<TaskInfo> taskList = new ArrayList<>(1);
+
+ /** Start time of the current task. */
+ private long startTimeNanos;
+
+ /** Name of the current task. */
+ @Nullable
+ private String currentTaskName;
+
+ @Nullable
+ private TaskInfo lastTaskInfo;
+
+ private int taskCount;
+
+ /** Total running time. */
+ private long totalTimeNanos;
+
+ /**
+ * Construct a new {@code StopWatch}.
+ * <p>
+ * Does not start any task.
+ */
+ public StopWatch() {
+ this("");
+ }
+
+ /**
+ * Construct a new {@code StopWatch} with the given ID.
+ * <p>
+ * The ID is handy when we have output from multiple stop watches and need to
+ * distinguish between them.
+ * <p>
+ * Does not start any task.
+ *
+ * @param id identifier for this stop watch
+ */
+ public StopWatch(String id) {
+ this.id = id;
+ }
+
+ /**
+ * Get the ID of this {@code StopWatch}, as specified on construction.
+ *
+ * @return the ID (empty String by default)
+ * @since 4.2.2
+ * @see #StopWatch(String)
+ */
+ public String getId() {
+ return this.id;
+ }
+
+ /**
+ * Configure whether the {@link TaskInfo} array is built over time.
+ * <p>
+ * Set this to {@code false} when using a {@code StopWatch} for millions of
+ * intervals; otherwise, the {@code TaskInfo} structure will consume
excessive
+ * memory.
+ * <p>
+ * Default is {@code true}.
+ */
+ public void setKeepTaskList(boolean keepTaskList) {
+ this.keepTaskList = keepTaskList;
+ }
+
+ /**
+ * Start an unnamed task.
+ * <p>
+ * The results are undefined if {@link #stop()} or timing methods are called
+ * without invoking this method first.
+ *
+ * @see #start(String)
+ * @see #stop()
+ */
+ public void start() throws IllegalStateException {
+ start("");
+ }
+
+ /**
+ * Start a named task.
+ * <p>
+ * The results are undefined if {@link #stop()} or timing methods are called
+ * without invoking this method first.
+ *
+ * @param taskName the name of the task to start
+ * @see #start()
+ * @see #stop()
+ */
+ public void start(String taskName) throws IllegalStateException {
+ if (this.currentTaskName != null) {
+ throw new IllegalStateException("Can't start StopWatch: it's already
running");
+ }
+ this.currentTaskName = taskName;
+ this.startTimeNanos = System.nanoTime();
+ }
+
+ /**
+ * Stop the current task.
+ * <p>
+ * The results are undefined if timing methods are called without invoking at
+ * least one pair of {@code start()} / {@code stop()} methods.
+ *
+ * @see #start()
+ * @see #start(String)
+ */
+ public void stop() throws IllegalStateException {
+ if (this.currentTaskName == null) {
+ throw new IllegalStateException("Can't stop StopWatch: it's not
running");
+ }
+ long lastTime = System.nanoTime() - this.startTimeNanos;
+ this.totalTimeNanos += lastTime;
+ this.lastTaskInfo = new TaskInfo(this.currentTaskName, lastTime);
+ if (this.keepTaskList) {
+ this.taskList.add(this.lastTaskInfo);
+ }
+ ++this.taskCount;
+ this.currentTaskName = null;
+ }
+
+ /**
+ * Determine whether this {@code StopWatch} is currently running.
+ *
+ * @see #currentTaskName()
+ */
+ public boolean isRunning() {
+ return (this.currentTaskName != null);
+ }
+
+ /**
+ * Get the name of the currently running task, if any.
+ *
+ * @since 4.2.2
+ * @see #isRunning()
+ */
+ @Nullable
+ public String currentTaskName() {
+ return this.currentTaskName;
+ }
+
+ /**
+ * Get the time taken by the last task in nanoseconds.
+ *
+ * @since 5.2
+ * @see #getLastTaskTimeMillis()
+ */
+ public long getLastTaskTimeNanos() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task
interval");
+ }
+ return this.lastTaskInfo.getTimeNanos();
+ }
+
+ /**
+ * Get the time taken by the last task in milliseconds.
+ *
+ * @see #getLastTaskTimeNanos()
+ */
+ public long getLastTaskTimeMillis() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task
interval");
+ }
+ return this.lastTaskInfo.getTimeMillis();
+ }
+
+ /**
+ * Get the name of the last task.
+ */
+ public String getLastTaskName() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task
name");
+ }
+ return this.lastTaskInfo.getTaskName();
+ }
+
+ /**
+ * Get the last task as a {@link TaskInfo} object.
+ */
+ public TaskInfo getLastTaskInfo() throws IllegalStateException {
+ if (this.lastTaskInfo == null) {
+ throw new IllegalStateException("No tasks run: can't get last task
info");
+ }
+ return this.lastTaskInfo;
+ }
+
+ /**
+ * Get the total time in nanoseconds for all tasks.
+ *
+ * @since 5.2
+ * @see #getTotalTimeMillis()
+ * @see #getTotalTimeSeconds()
+ */
+ public long getTotalTimeNanos() {
+ return this.totalTimeNanos;
+ }
+
+ /**
+ * Get the total time in milliseconds for all tasks.
+ *
+ * @see #getTotalTimeNanos()
+ * @see #getTotalTimeSeconds()
+ */
+ public long getTotalTimeMillis() {
+ return nanosToMillis(this.totalTimeNanos);
+ }
+
+ /**
+ * Get the total time in seconds for all tasks.
+ *
+ * @see #getTotalTimeNanos()
+ * @see #getTotalTimeMillis()
+ */
+ public double getTotalTimeSeconds() {
+ return nanosToSeconds(this.totalTimeNanos);
+ }
+
+ /**
+ * Get the number of tasks timed.
+ */
+ public int getTaskCount() {
+ return this.taskCount;
+ }
+
+ /**
+ * Get an array of the data for tasks performed.
+ */
+ public TaskInfo[] getTaskInfo() {
+ if (!this.keepTaskList) {
+ throw new UnsupportedOperationException("Task info is not being kept!");
+ }
+ return this.taskList.toArray(new TaskInfo[0]);
+ }
+
+ /**
+ * Get a short description of the total running time.
+ */
+ public String shortSummary() {
+ return "StopWatch '" + getId() + "': running time = " +
getTotalTimeNanos() + " ns";
+ }
+
+ /**
+ * Generate a string with a table describing all tasks performed.
+ * <p>
+ * For custom reporting, call {@link #getTaskInfo()} and use the task info
+ * directly.
+ */
+ public String prettyPrint() {
+ StringBuilder sb = new StringBuilder(shortSummary());
+ sb.append('\n');
+ if (!this.keepTaskList) {
+ sb.append("No task info kept");
+ } else {
+ sb.append("---------------------------------------------\n");
+ sb.append("ns % Task name\n");
+ sb.append("---------------------------------------------\n");
+ NumberFormat nf = NumberFormat.getNumberInstance();
+ nf.setMinimumIntegerDigits(9);
+ nf.setGroupingUsed(false);
+ NumberFormat pf = NumberFormat.getPercentInstance();
+ pf.setMinimumIntegerDigits(3);
+ pf.setGroupingUsed(false);
+ for (TaskInfo task : getTaskInfo()) {
+ sb.append(nf.format(task.getTimeNanos())).append(" ");
+ sb.append(pf.format((double) task.getTimeNanos() /
getTotalTimeNanos())).append(" ");
+ sb.append(task.getTaskName()).append('\n');
+ }
+ }
+ return sb.toString();
+ }
+
+ /**
+ * Generate an informative string describing all tasks performed
+ * <p>
+ * For custom reporting, call {@link #getTaskInfo()} and use the task info
+ * directly.
+ */
+ @Override
+ public String toString() {
+ StringBuilder sb = new StringBuilder(shortSummary());
+ if (this.keepTaskList) {
+ for (TaskInfo task : getTaskInfo()) {
+ sb.append("; [").append(task.getTaskName()).append("] took
").append(task.getTimeNanos()).append(" ns");
+ long percent = Math.round(100.0 * task.getTimeNanos() /
getTotalTimeNanos());
+ sb.append(" = ").append(percent).append('%');
+ }
+ } else {
+ sb.append("; no task info kept");
+ }
+ return sb.toString();
+ }
+
+ private static long nanosToMillis(long duration) {
+ return TimeUnit.NANOSECONDS.toMillis(duration);
+ }
+
+ private static double nanosToSeconds(long duration) {
+ return duration / 1_000_000_000.0;
+ }
+
+ /**
+ * Nested class to hold data about one task executed within the
+ * {@code StopWatch}.
+ */
+ public static final class TaskInfo {
+
+ private final String taskName;
+
+ private final long timeNanos;
+
+ TaskInfo(String taskName, long timeNanos) {
+ this.taskName = taskName;
+ this.timeNanos = timeNanos;
+ }
+
+ /**
+ * Get the name of this task.
+ */
+ public String getTaskName() {
+ return this.taskName;
+ }
+
+ /**
+ * Get the time in nanoseconds this task took.
+ *
+ * @since 5.2
+ * @see #getTimeMillis()
+ * @see #getTimeSeconds()
+ */
+ public long getTimeNanos() {
+ return this.timeNanos;
+ }
+
+ /**
+ * Get the time in milliseconds this task took.
+ *
+ * @see #getTimeNanos()
+ * @see #getTimeSeconds()
+ */
+ public long getTimeMillis() {
+ return nanosToMillis(this.timeNanos);
+ }
+
+ /**
+ * Get the time in seconds this task took.
+ *
+ * @see #getTimeMillis()
+ * @see #getTimeNanos()
+ */
+ public double getTimeSeconds() {
+ return nanosToSeconds(this.timeNanos);
+ }
+
+ }
+
+}
diff --git
a/lang/java/avro/src/test/java/org/apache/avro/util/springframework/TestConcurrentReferenceHashMap.java
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/TestConcurrentReferenceHashMap.java
new file mode 100644
index 000000000..c35176886
--- /dev/null
+++
b/lang/java/avro/src/test/java/org/apache/avro/util/springframework/TestConcurrentReferenceHashMap.java
@@ -0,0 +1,688 @@
+/*
+ * Copyright 2002-2021 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 org.apache.avro.util.springframework;
+
+import org.apache.avro.reflect.Nullable;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.apache.avro.util.springframework.ConcurrentReferenceHashMap.Entry;
+import
org.apache.avro.util.springframework.ConcurrentReferenceHashMap.Reference;
+import
org.apache.avro.util.springframework.ConcurrentReferenceHashMap.Restructure;
+
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.WeakHashMap;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.equalTo;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotEquals;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Tests for {@link ConcurrentReferenceHashMap}.
+ *
+ * @author Phillip Webb
+ * @author Juergen Hoeller
+ */
+class TestConcurrentReferenceHashMap {
+
+ private static final Comparator<? super String> NULL_SAFE_STRING_SORT = new
NullSafeComparator<>(
+ new ComparableComparator<String>(), true);
+
+ private TestWeakConcurrentCache<Integer, String> map = new
TestWeakConcurrentCache<>();
+
+ @Test
+ void shouldCreateWithDefaults() {
+ ConcurrentReferenceHashMap<Integer, String> map = new
ConcurrentReferenceHashMap<>();
+ assertThat(map.getSegmentsSize(), equalTo(16));
+ assertThat(map.getSegment(0).getSize(), equalTo(1));
+ assertThat(map.getLoadFactor(), equalTo(0.75f));
+ }
+
+ @Test
+ void shouldCreateWithInitialCapacity() {
+ ConcurrentReferenceHashMap<Integer, String> map = new
ConcurrentReferenceHashMap<>(32);
+ assertThat(map.getSegmentsSize(), equalTo(16));
+ assertThat(map.getSegment(0).getSize(), equalTo(2));
+ assertThat(map.getLoadFactor(), equalTo(0.75f));
+ }
+
+ @Test
+ void shouldCreateWithInitialCapacityAndLoadFactor() {
+ ConcurrentReferenceHashMap<Integer, String> map = new
ConcurrentReferenceHashMap<>(32, 0.5f);
+ assertThat(map.getSegmentsSize(), equalTo(16));
+ assertThat(map.getSegment(0).getSize(), equalTo(2));
+ assertThat(map.getLoadFactor(), equalTo(0.5f));
+ }
+
+ @Test
+ void shouldCreateWithInitialCapacityAndConcurrentLevel() {
+ ConcurrentReferenceHashMap<Integer, String> map = new
ConcurrentReferenceHashMap<>(16, 2);
+ assertThat(map.getSegmentsSize(), equalTo(2));
+ assertThat(map.getSegment(0).getSize(), equalTo(8));
+ assertThat(map.getLoadFactor(), equalTo(0.75f));
+ }
+
+ @Test
+ void shouldCreateFullyCustom() {
+ ConcurrentReferenceHashMap<Integer, String> map = new
ConcurrentReferenceHashMap<>(5, 0.5f, 3);
+ // concurrencyLevel of 3 ends up as 4 (nearest power of 2)
+ assertThat(map.getSegmentsSize(), equalTo(4));
+ // initialCapacity is 5/4 (rounded up, to nearest power of 2)
+ assertThat(map.getSegment(0).getSize(), equalTo(2));
+ assertThat(map.getLoadFactor(), equalTo(0.5f));
+ }
+
+ @Test
+ void shouldNeedNonNegativeInitialCapacity() {
+ new ConcurrentReferenceHashMap<Integer, String>(0, 1);
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new TestWeakConcurrentCache<Integer, String>(-1, 1));
+ assertTrue(e.getMessage().contains("Initial capacity must not be
negative"));
+ }
+
+ @Test
+ void shouldNeedPositiveLoadFactor() {
+ new ConcurrentReferenceHashMap<Integer, String>(0, 0.1f, 1);
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new TestWeakConcurrentCache<Integer, String>(0, 0.0f, 1));
+ assertTrue(e.getMessage().contains("Load factor must be positive"));
+ }
+
+ @Test
+ void shouldNeedPositiveConcurrencyLevel() {
+ new ConcurrentReferenceHashMap<Integer, String>(1, 1);
+ IllegalArgumentException e = assertThrows(IllegalArgumentException.class,
+ () -> new TestWeakConcurrentCache<Integer, String>(1, 0));
+ assertTrue(e.getMessage().contains("Concurrency level must be positive"));
+ }
+
+ @Test
+ void shouldPutAndGet() {
+ // NOTE we are using mock references so we don't need to worry about GC
+ assertEquals(0, this.map.size());
+ this.map.put(123, "123");
+ assertThat(this.map.get(123), equalTo("123"));
+ assertEquals(1, this.map.size());
+ this.map.put(123, "123b");
+ assertEquals(1, this.map.size());
+ this.map.put(123, null);
+ assertEquals(1, this.map.size());
+ }
+
+ @Test
+ void shouldReplaceOnDoublePut() {
+ this.map.put(123, "321");
+ this.map.put(123, "123");
+ assertThat(this.map.get(123), equalTo("123"));
+ }
+
+ @Test
+ void shouldPutNullKey() {
+ assertNull(this.map.get(null));
+ assertThat(this.map.getOrDefault(null, "456"), equalTo("456"));
+ this.map.put(null, "123");
+ assertThat(this.map.get(null), equalTo("123"));
+ assertThat(this.map.getOrDefault(null, "456"), equalTo("123"));
+ }
+
+ @Test
+ void shouldPutNullValue() {
+ assertNull(this.map.get(123));
+ assertThat(this.map.getOrDefault(123, "456"), equalTo("456"));
+ this.map.put(123, "321");
+ assertThat(this.map.get(123), equalTo("321"));
+ assertThat(this.map.getOrDefault(123, "456"), equalTo("321"));
+ this.map.put(123, null);
+ assertNull(this.map.get(123));
+ assertNull(this.map.getOrDefault(123, "456"));
+ }
+
+ @Test
+ void shouldGetWithNoItems() {
+ assertNull(this.map.get(123));
+ }
+
+ @Test
+ void shouldApplySupplementalHash() {
+ Integer key = 123;
+ this.map.put(key, "123");
+ assertNotEquals(this.map.getSupplementalHash(), key.hashCode());
+ assertNotEquals(this.map.getSupplementalHash() >> 30 & 0xFF, 0);
+ }
+
+ @Test
+ void shouldGetFollowingNexts() {
+ // Use loadFactor to disable resize
+ this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1);
+ this.map.put(1, "1");
+ this.map.put(2, "2");
+ this.map.put(3, "3");
+ assertThat(this.map.getSegment(0).getSize(), equalTo(1));
+ assertThat(this.map.get(1), equalTo("1"));
+ assertThat(this.map.get(2), equalTo("2"));
+ assertThat(this.map.get(3), equalTo("3"));
+ assertNull(this.map.get(4));
+ }
+
+ @Test
+ void shouldResize() {
+ this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1);
+ this.map.put(1, "1");
+ assertThat(this.map.getSegment(0).getSize(), equalTo(1));
+ assertThat(this.map.get(1), equalTo("1"));
+
+ this.map.put(2, "2");
+ assertThat(this.map.getSegment(0).getSize(), equalTo(2));
+ assertThat(this.map.get(1), equalTo("1"));
+ assertThat(this.map.get(2), equalTo("2"));
+
+ this.map.put(3, "3");
+ assertThat(this.map.getSegment(0).getSize(), equalTo(4));
+ assertThat(this.map.get(1), equalTo("1"));
+ assertThat(this.map.get(2), equalTo("2"));
+ assertThat(this.map.get(3), equalTo("3"));
+
+ this.map.put(4, "4");
+ assertThat(this.map.getSegment(0).getSize(), equalTo(8));
+ assertThat(this.map.get(4), equalTo("4"));
+
+ // Putting again should not increase the count
+ for (int i = 1; i <= 5; i++) {
+ this.map.put(i, String.valueOf(i));
+ }
+ assertThat(this.map.getSegment(0).getSize(), equalTo(8));
+ assertThat(this.map.get(5), equalTo("5"));
+ }
+
+ @Test
+ void shouldPurgeOnGet() {
+ this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1);
+ for (int i = 1; i <= 5; i++) {
+ this.map.put(i, String.valueOf(i));
+ }
+ this.map.getMockReference(1, Restructure.NEVER).queueForPurge();
+ this.map.getMockReference(3, Restructure.NEVER).queueForPurge();
+ assertNull(this.map.getReference(1, Restructure.WHEN_NECESSARY));
+ assertThat(this.map.get(2), equalTo("2"));
+ assertNull(this.map.getReference(3, Restructure.WHEN_NECESSARY));
+ assertThat(this.map.get(4), equalTo("4"));
+ assertThat(this.map.get(5), equalTo("5"));
+ }
+
+ @Test
+ void shouldPurgeOnPut() {
+ this.map = new TestWeakConcurrentCache<>(1, 0.75f, 1);
+ for (int i = 1; i <= 5; i++) {
+ this.map.put(i, String.valueOf(i));
+ }
+ this.map.getMockReference(1, Restructure.NEVER).queueForPurge();
+ this.map.getMockReference(3, Restructure.NEVER).queueForPurge();
+ this.map.put(1, "1");
+ assertThat(this.map.get(1), equalTo("1"));
+ assertThat(this.map.get(2), equalTo("2"));
+ assertNull(this.map.getReference(3, Restructure.WHEN_NECESSARY));
+ assertThat(this.map.get(4), equalTo("4"));
+ assertThat(this.map.get(5), equalTo("5"));
+ }
+
+ @Test
+ void shouldPutIfAbsent() {
+ assertNull(this.map.putIfAbsent(123, "123"));
+ assertThat(this.map.putIfAbsent(123, "123b"), equalTo("123"));
+ assertThat(this.map.get(123), equalTo("123"));
+ }
+
+ @Test
+ void shouldPutIfAbsentWithNullValue() {
+ assertNull(this.map.putIfAbsent(123, null));
+ assertNull(this.map.putIfAbsent(123, "123"));
+ assertNull(this.map.get(123));
+ }
+
+ @Test
+ void shouldPutIfAbsentWithNullKey() {
+ assertNull(this.map.putIfAbsent(null, "123"));
+ assertThat(this.map.putIfAbsent(null, "123b"), equalTo("123"));
+ assertThat(this.map.get(null), equalTo("123"));
+ }
+
+ @Test
+ void shouldRemoveKeyAndValue() {
+ this.map.put(123, "123");
+ assertFalse(this.map.remove(123, "456"));
+ assertThat(this.map.get(123), equalTo("123"));
+ assertTrue(this.map.remove(123, "123"));
+ assertFalse(this.map.containsKey(123));
+ assertTrue(this.map.isEmpty());
+ }
+
+ @Test
+ void shouldRemoveKeyAndValueWithExistingNull() {
+ this.map.put(123, null);
+ assertFalse(this.map.remove(123, "456"));
+ assertNull(this.map.get(123));
+ assertTrue(this.map.remove(123, null));
+ assertFalse(this.map.containsKey(123));
+ assertTrue(this.map.isEmpty());
+ }
+
+ @Test
+ void shouldReplaceOldValueWithNewValue() {
+ this.map.put(123, "123");
+ assertFalse(this.map.replace(123, "456", "789"));
+ assertThat(this.map.get(123), equalTo("123"));
+ assertTrue(this.map.replace(123, "123", "789"));
+ assertThat(this.map.get(123), equalTo("789"));
+ }
+
+ @Test
+ void shouldReplaceOldNullValueWithNewValue() {
+ this.map.put(123, null);
+ assertFalse(this.map.replace(123, "456", "789"));
+ assertNull(this.map.get(123));
+ assertTrue(this.map.replace(123, null, "789"));
+ assertThat(this.map.get(123), equalTo("789"));
+ }
+
+ @Test
+ void shouldReplaceValue() {
+ this.map.put(123, "123");
+ assertThat(this.map.replace(123, "456"), equalTo("123"));
+ assertThat(this.map.get(123), equalTo("456"));
+ }
+
+ @Test
+ void shouldReplaceNullValue() {
+ this.map.put(123, null);
+ assertNull(this.map.replace(123, "456"));
+ assertThat(this.map.get(123), equalTo("456"));
+ }
+
+ @Test
+ void shouldGetSize() {
+ assertEquals(0, this.map.size());
+ this.map.put(123, "123");
+ this.map.put(123, null);
+ this.map.put(456, "456");
+ assertEquals(2, this.map.size());
+ }
+
+ @Test
+ void shouldSupportIsEmpty() {
+ assertTrue(this.map.isEmpty());
+ this.map.put(123, "123");
+ this.map.put(123, null);
+ this.map.put(456, "456");
+ assertFalse(this.map.isEmpty());
+ }
+
+ @Test
+ void shouldContainKey() {
+ assertFalse(this.map.containsKey(123));
+ assertFalse(this.map.containsKey(456));
+ this.map.put(123, "123");
+ this.map.put(456, null);
+ assertTrue(this.map.containsKey(123));
+ assertTrue(this.map.containsKey(456));
+ }
+
+ @Test
+ void shouldContainValue() {
+ assertFalse(this.map.containsValue("123"));
+ assertFalse(this.map.containsValue(null));
+ this.map.put(123, "123");
+ this.map.put(456, null);
+ assertTrue(this.map.containsValue("123"));
+ assertTrue(this.map.containsValue(null));
+ }
+
+ @Test
+ void shouldRemoveWhenKeyIsInMap() {
+ this.map.put(123, null);
+ this.map.put(456, "456");
+ this.map.put(null, "789");
+ assertNull(this.map.remove(123));
+ assertThat(this.map.remove(456), equalTo("456"));
+ assertThat(this.map.remove(null), equalTo("789"));
+ assertTrue(this.map.isEmpty());
+ }
+
+ @Test
+ void shouldRemoveWhenKeyIsNotInMap() {
+ assertNull(this.map.remove(123));
+ assertNull(this.map.remove(null));
+ assertTrue(this.map.isEmpty());
+ }
+
+ @Test
+ void shouldPutAll() {
+ Map<Integer, String> m = new HashMap<>();
+ m.put(123, "123");
+ m.put(456, null);
+ m.put(null, "789");
+ this.map.putAll(m);
+ assertEquals(3, this.map.size());
+ assertThat(this.map.get(123), equalTo("123"));
+ assertNull(this.map.get(456));
+ assertThat(this.map.get(null), equalTo("789"));
+ }
+
+ @Test
+ void shouldClear() {
+ this.map.put(123, "123");
+ this.map.put(456, null);
+ this.map.put(null, "789");
+ this.map.clear();
+ assertEquals(0, this.map.size());
+ assertFalse(this.map.containsKey(123));
+ assertFalse(this.map.containsKey(456));
+ assertFalse(this.map.containsKey(null));
+ }
+
+ @Test
+ void shouldGetKeySet() {
+ this.map.put(123, "123");
+ this.map.put(456, null);
+ this.map.put(null, "789");
+ Set<Integer> expected = new HashSet<>();
+ expected.add(123);
+ expected.add(456);
+ expected.add(null);
+ assertThat(this.map.keySet(), equalTo(expected));
+ }
+
+ @Test
+ void shouldGetValues() {
+ this.map.put(123, "123");
+ this.map.put(456, null);
+ this.map.put(null, "789");
+ List<String> actual = new ArrayList<>(this.map.values());
+ List<String> expected = new ArrayList<>();
+ expected.add("123");
+ expected.add(null);
+ expected.add("789");
+ actual.sort(NULL_SAFE_STRING_SORT);
+ expected.sort(NULL_SAFE_STRING_SORT);
+ assertThat(actual, equalTo(expected));
+ }
+
+ @Test
+ void shouldGetEntrySet() {
+ this.map.put(123, "123");
+ this.map.put(456, null);
+ this.map.put(null, "789");
+ HashMap<Integer, String> expected = new HashMap<>();
+ expected.put(123, "123");
+ expected.put(456, null);
+ expected.put(null, "789");
+ assertThat(this.map.entrySet(), equalTo(expected.entrySet()));
+ }
+
+ @Test
+ void shouldGetEntrySetFollowingNext() {
+ // Use loadFactor to disable resize
+ this.map = new TestWeakConcurrentCache<>(1, 10.0f, 1);
+ this.map.put(1, "1");
+ this.map.put(2, "2");
+ this.map.put(3, "3");
+ HashMap<Integer, String> expected = new HashMap<>();
+ expected.put(1, "1");
+ expected.put(2, "2");
+ expected.put(3, "3");
+ assertThat(this.map.entrySet(), equalTo(expected.entrySet()));
+ }
+
+ @Test
+ void shouldRemoveViaEntrySet() {
+ this.map.put(1, "1");
+ this.map.put(2, "2");
+ this.map.put(3, "3");
+ Iterator<Map.Entry<Integer, String>> iterator =
this.map.entrySet().iterator();
+ iterator.next();
+ iterator.next();
+ iterator.remove();
+ assertThrows(IllegalStateException.class, iterator::remove);
+ iterator.next();
+ assertFalse(iterator.hasNext());
+ assertEquals(2, this.map.size());
+ assertFalse(this.map.containsKey(2));
+ }
+
+ @Test
+ void shouldSetViaEntrySet() {
+ this.map.put(1, "1");
+ this.map.put(2, "2");
+ this.map.put(3, "3");
+ Iterator<Map.Entry<Integer, String>> iterator =
this.map.entrySet().iterator();
+ iterator.next();
+ iterator.next().setValue("2b");
+ iterator.next();
+ assertFalse(iterator.hasNext());
+ assertEquals(3, this.map.size());
+ assertThat(this.map.get(2), equalTo("2b"));
+ }
+
+ @Test
+ void containsViaEntrySet() {
+ this.map.put(1, "1");
+ this.map.put(2, "2");
+ this.map.put(3, "3");
+ Set<Map.Entry<Integer, String>> entrySet = this.map.entrySet();
+ Set<Map.Entry<Integer, String>> copy = new HashMap<>(this.map).entrySet();
+ copy.forEach(entry -> assertTrue(entrySet.contains(entry)));
+ this.map.put(1, "A");
+ this.map.put(2, "B");
+ this.map.put(3, "C");
+ copy.forEach(entry -> assertFalse(entrySet.contains(entry)));
+ this.map.put(1, "1");
+ this.map.put(2, "2");
+ this.map.put(3, "3");
+ copy.forEach(entry -> assertTrue(entrySet.contains(entry)));
+ entrySet.clear();
+ copy.forEach(entry -> assertFalse(entrySet.contains(entry)));
+ }
+
+ @Test
+ @Disabled("Intended for use during development only")
+ void shouldBeFasterThanSynchronizedMap() throws InterruptedException {
+ Map<Integer, WeakReference<String>> synchronizedMap = Collections
+ .synchronizedMap(new WeakHashMap<Integer, WeakReference<String>>());
+ StopWatch mapTime = timeMultiThreaded("SynchronizedMap", synchronizedMap,
+ v -> new WeakReference<>(String.valueOf(v)));
+ System.out.println(mapTime.prettyPrint());
+
+ this.map.setDisableTestHooks(true);
+ StopWatch cacheTime = timeMultiThreaded("WeakConcurrentCache", this.map,
String::valueOf);
+ System.out.println(cacheTime.prettyPrint());
+
+ // We should be at least 4 time faster
+ assertTrue(cacheTime.getTotalTimeSeconds() <
(mapTime.getTotalTimeSeconds() / 4.0));
+ }
+
+ @Test
+ void shouldSupportNullReference() {
+ // GC could happen during restructure so we must be able to create a
reference
+ // for a null entry
+ map.createReferenceManager().createReference(null, 1234, null);
+ }
+
+ /**
+ * Time a multi-threaded access to a cache.
+ *
+ * @return the timing stopwatch
+ */
+ private <V> StopWatch timeMultiThreaded(String id, final Map<Integer, V>
map, ValueFactory<V> factory)
+ throws InterruptedException {
+
+ StopWatch stopWatch = new StopWatch(id);
+ for (int i = 0; i < 500; i++) {
+ map.put(i, factory.newValue(i));
+ }
+ Thread[] threads = new Thread[30];
+ stopWatch.start("Running threads");
+ for (int threadIndex = 0; threadIndex < threads.length; threadIndex++) {
+ threads[threadIndex] = new Thread("Cache access thread " + threadIndex) {
+ @Override
+ public void run() {
+ for (int j = 0; j < 1000; j++) {
+ for (int i = 0; i < 1000; i++) {
+ map.get(i);
+ }
+ }
+ }
+ };
+ }
+ for (Thread thread : threads) {
+ thread.start();
+ }
+
+ for (Thread thread : threads) {
+ if (thread.isAlive()) {
+ thread.join(2000);
+ }
+ }
+ stopWatch.stop();
+ return stopWatch;
+ }
+
+ private interface ValueFactory<V> {
+
+ V newValue(int k);
+ }
+
+ private static class TestWeakConcurrentCache<K, V> extends
ConcurrentReferenceHashMap<K, V> {
+
+ private int supplementalHash;
+
+ private final LinkedList<MockReference<K, V>> queue = new LinkedList<>();
+
+ private boolean disableTestHooks;
+
+ public TestWeakConcurrentCache() {
+ super();
+ }
+
+ public void setDisableTestHooks(boolean disableTestHooks) {
+ this.disableTestHooks = disableTestHooks;
+ }
+
+ public TestWeakConcurrentCache(int initialCapacity, float loadFactor, int
concurrencyLevel) {
+ super(initialCapacity, loadFactor, concurrencyLevel);
+ }
+
+ public TestWeakConcurrentCache(int initialCapacity, int concurrencyLevel) {
+ super(initialCapacity, concurrencyLevel);
+ }
+
+ @Override
+ protected int getHash(@Nullable Object o) {
+ if (this.disableTestHooks) {
+ return super.getHash(o);
+ }
+ // For testing we want more control of the hash
+ this.supplementalHash = super.getHash(o);
+ return (o != null ? o.hashCode() : 0);
+ }
+
+ public int getSupplementalHash() {
+ return this.supplementalHash;
+ }
+
+ @Override
+ protected ReferenceManager createReferenceManager() {
+ return new ReferenceManager() {
+ @Override
+ public Reference<K, V> createReference(Entry<K, V> entry, int hash,
@Nullable Reference<K, V> next) {
+ if (TestWeakConcurrentCache.this.disableTestHooks) {
+ return super.createReference(entry, hash, next);
+ }
+ return new MockReference<>(entry, hash, next,
TestWeakConcurrentCache.this.queue);
+ }
+
+ @Override
+ public Reference<K, V> pollForPurge() {
+ if (TestWeakConcurrentCache.this.disableTestHooks) {
+ return super.pollForPurge();
+ }
+ return TestWeakConcurrentCache.this.queue.isEmpty() ? null :
TestWeakConcurrentCache.this.queue.removeFirst();
+ }
+ };
+ }
+
+ public MockReference<K, V> getMockReference(K key, Restructure
restructure) {
+ return (MockReference<K, V>) super.getReference(key, restructure);
+ }
+ }
+
+ private static class MockReference<K, V> implements Reference<K, V> {
+
+ private final int hash;
+
+ private Entry<K, V> entry;
+
+ private final Reference<K, V> next;
+
+ private final LinkedList<MockReference<K, V>> queue;
+
+ public MockReference(Entry<K, V> entry, int hash, Reference<K, V> next,
LinkedList<MockReference<K, V>> queue) {
+ this.hash = hash;
+ this.entry = entry;
+ this.next = next;
+ this.queue = queue;
+ }
+
+ @Override
+ public Entry<K, V> get() {
+ return this.entry;
+ }
+
+ @Override
+ public int getHash() {
+ return this.hash;
+ }
+
+ @Override
+ public Reference<K, V> getNext() {
+ return this.next;
+ }
+
+ @Override
+ public void release() {
+ this.queue.add(this);
+ this.entry = null;
+ }
+
+ public void queueForPurge() {
+ this.queue.add(this);
+ }
+ }
+
+}