/* * Copyright (c) 2016 Mockito contributors * This program is made available under the terms of the MIT License. */ package org.mockito.internal.util.concurrent; import java.lang.ref.Reference; import java.lang.ref.ReferenceQueue; import java.lang.ref.WeakReference; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.atomic.AtomicLong; /** * <p> * A thread-safe map with weak keys. Entries are based on a key's system hash code and keys are considered * equal only by reference equality. * </p> * This class does not implement the {@link java.util.Map} interface because this implementation is incompatible * with the map contract. While iterating over a map's entries, any key that has not passed iteration is referenced non-weakly. */ public class WeakConcurrentMap<K, V> extends ReferenceQueue<K> implements Runnable, Iterable<Map.Entry<K, V>> { private static final AtomicLong ID = new AtomicLong(); final ConcurrentMap<WeakKey<K>, V> target; private final Thread thread; /** * @param cleanerThread {@code true} if a thread should be started that removes stale entries. */ public WeakConcurrentMap(boolean cleanerThread) { target = new ConcurrentHashMap<WeakKey<K>, V>(); if (cleanerThread) { thread = new Thread(this); thread.setName("weak-ref-cleaner-" + ID.getAndIncrement()); thread.setPriority(Thread.MIN_PRIORITY); thread.setDaemon(true); thread.start(); } else { thread = null; } } /** * @param key The key of the entry. * @return The value of the entry or the default value if it did not exist. */ public V get(K key) { if (key == null) throw new NullPointerException(); V value = target.get(new LatentKey<K>(key)); if (value == null) { value = defaultValue(key); if (value != null) { V previousValue = target.putIfAbsent(new WeakKey<K>(key, this), value); if (previousValue != null) { value = previousValue; } } } return value; } /** * @param key The key of the entry. * @return {@code true} if the key already defines a value. */ public boolean containsKey(K key) { if (key == null) throw new NullPointerException(); return target.containsKey(new LatentKey<K>(key)); } /** * @param key The key of the entry. * @param value The value of the entry. * @return The previous entry or {@code null} if it does not exist. */ public V put(K key, V value) { if (key == null || value == null) throw new NullPointerException(); return target.put(new WeakKey<K>(key, this), value); } /** * @param key The key of the entry. * @return The removed entry or {@code null} if it does not exist. */ public V remove(K key) { if (key == null) throw new NullPointerException(); return target.remove(new LatentKey<K>(key)); } /** * Clears the entire map. */ public void clear() { target.clear(); } /** * Creates a default value. There is no guarantee that the requested value will be set as a once it is created * in case that another thread requests a value for a key concurrently. * * @param key The key for which to create a default value. * @return The default value for a key without value or {@code null} for not defining a default value. */ protected V defaultValue(K key) { return null; } /** * @return The cleaner thread or {@code null} if no such thread was set. */ public Thread getCleanerThread() { return thread; } /** * Cleans all unused references. */ public void expungeStaleEntries() { Reference<?> reference; while ((reference = poll()) != null) { target.remove(reference); } } /** * Returns the approximate size of this map where the returned number is at least as big as the actual number of entries. * * @return The minimum size of this map. */ public int approximateSize() { return target.size(); } @Override public void run() { try { while (true) { target.remove(remove()); } } catch (InterruptedException ignored) { clear(); } } @Override public Iterator<Map.Entry<K, V>> iterator() { return new EntryIterator(target.entrySet().iterator()); } /* * Why this works: * --------------- * * Note that this map only supports reference equality for keys and uses system hash codes. Also, for the * WeakKey instances to function correctly, we are voluntarily breaking the Java API contract for * hashCode/equals of these instances. * * * System hash codes are immutable and can therefore be computed prematurely and are stored explicitly * within the WeakKey instances. This way, we always know the correct hash code of a key and always * end up in the correct bucket of our target map. This remains true even after the weakly referenced * key is collected. * * If we are looking up the value of the current key via WeakConcurrentMap::get or any other public * API method, we know that any value associated with this key must still be in the map as the mere * existence of this key makes it ineligible for garbage collection. Therefore, looking up a value * using another WeakKey wrapper guarantees a correct result. * * If we are looking up the map entry of a WeakKey after polling it from the reference queue, we know * that the actual key was already collected and calling WeakKey::get returns null for both the polled * instance and the instance within the map. Since we explicitly stored the identity hash code for the * referenced value, it is however trivial to identify the correct bucket. From this bucket, the first * weak key with a null reference is removed. Due to hash collision, we do not know if this entry * represents the weak key. However, we do know that the reference queue polls at least as many weak * keys as there are stale map entries within the target map. If no key is ever removed from the map * explicitly, the reference queue eventually polls exactly as many weak keys as there are stale entries. * * Therefore, we can guarantee that there is no memory leak. */ private static class WeakKey<T> extends WeakReference<T> { private final int hashCode; WeakKey(T key, ReferenceQueue<? super T> queue) { super(key, queue); hashCode = System.identityHashCode(key); } @Override public int hashCode() { return hashCode; } @Override public boolean equals(Object other) { if (other instanceof LatentKey<?>) { return ((LatentKey<?>) other).key == get(); } else { return ((WeakKey<?>) other).get() == get(); } } } /* * A latent key must only be used for looking up instances within a map. For this to work, it implements an identical contract for * hash code and equals as the WeakKey implementation. At the same time, the latent key implementation does not extend WeakReference * and avoids the overhead that a weak reference implies. */ private static class LatentKey<T> { final T key; private final int hashCode; LatentKey(T key) { this.key = key; hashCode = System.identityHashCode(key); } @Override public boolean equals(Object other) { if (other instanceof LatentKey<?>) { return ((LatentKey<?>) other).key == key; } else { return ((WeakKey<?>) other).get() == key; } } @Override public int hashCode() { return hashCode; } } /** * A {@link WeakConcurrentMap} where stale entries are removed as a side effect of interacting with this map. */ public static class WithInlinedExpunction<K, V> extends WeakConcurrentMap<K, V> { public WithInlinedExpunction() { super(false); } @Override public V get(K key) { expungeStaleEntries(); return super.get(key); } @Override public boolean containsKey(K key) { expungeStaleEntries(); return super.containsKey(key); } @Override public V put(K key, V value) { expungeStaleEntries(); return super.put(key, value); } @Override public V remove(K key) { expungeStaleEntries(); return super.remove(key); } @Override public Iterator<Map.Entry<K, V>> iterator() { expungeStaleEntries(); return super.iterator(); } @Override public int approximateSize() { expungeStaleEntries(); return super.approximateSize(); } } private class EntryIterator implements Iterator<Map.Entry<K, V>> { private final Iterator<Map.Entry<WeakKey<K>, V>> iterator; private Map.Entry<WeakKey<K>, V> nextEntry; private K nextKey; private EntryIterator(Iterator<Map.Entry<WeakKey<K>, V>> iterator) { this.iterator = iterator; findNext(); } private void findNext() { while (iterator.hasNext()) { nextEntry = iterator.next(); nextKey = nextEntry.getKey().get(); if (nextKey != null) { return; } } nextEntry = null; nextKey = null; } @Override public boolean hasNext() { return nextKey != null; } @Override public Map.Entry<K, V> next() { if (nextKey == null) { throw new NoSuchElementException(); } try { return new SimpleEntry(nextKey, nextEntry); } finally { findNext(); } } @Override public void remove() { throw new UnsupportedOperationException(); } } private class SimpleEntry implements Map.Entry<K, V> { private final K key; final Map.Entry<WeakKey<K>, V> entry; private SimpleEntry(K key, Map.Entry<WeakKey<K>, V> entry) { this.key = key; this.entry = entry; } @Override public K getKey() { return key; } @Override public V getValue() { return entry.getValue(); } @Override public V setValue(V value) { if (value == null) throw new NullPointerException(); return entry.setValue(value); } } }