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 &gt; 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 &gt; 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);
+    }
+  }
+
+}


Reply via email to