io.camunda.operate.util.SoftHashMap Maven / Gradle / Ivy
/*
* Copyright Camunda Services GmbH and/or licensed to Camunda Services GmbH under
* one or more contributor license agreements. See the NOTICE file distributed
* with this work for additional information regarding copyright ownership.
* Licensed under the Camunda License 1.0. You may not use this file
* except in compliance with the Camunda License 1.0.
*/
package io.camunda.operate.util;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.locks.ReentrantLock;
/**
* A SoftHashMap
is a memory-constrained map that stores its values
* in {@link SoftReference SoftReference}s. (Contrast this with the JDK's {@link WeakHashMap
* WeakHashMap}, which uses weak references for its keys, which is of little value if you
* want the cache to auto-resize itself based on memory constraints).
*
* Having the values wrapped by soft references allows the cache to automatically reduce its size
* based on memory limitations and garbage collection. This ensures that the cache will not cause
* memory leaks by holding strong references to all of its values.
*
*
This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney
* Redelinghuys's publicly posted
* version (with their approval), with continued modifications.
*
*
This implementation is thread-safe and usable in concurrent environments.
*
*
Copied from Apache Shiro library: https://shiro.apache.org/
*/
public class SoftHashMap implements Map {
/** The default value of the RETENTION_SIZE attribute, equal to 100. */
private static final int DEFAULT_RETENTION_SIZE = 100;
/** The internal HashMap that will hold the SoftReference. */
private final Map> map;
/**
* The number of strong references to hold internally, that is, the number of instances to prevent
* from being garbage collected automatically (unlike other soft references).
*/
private final int retentionSize;
/** The FIFO list of strong references (not to be garbage collected), order of last access. */
private final Queue strongReferences; // guarded by 'strongReferencesLock'
private final ReentrantLock strongReferencesLock;
/** Reference queue for cleared SoftReference objects. */
private final ReferenceQueue super V> queue;
/**
* Creates a new SoftHashMap with a default retention size size of {@link #DEFAULT_RETENTION_SIZE
* DEFAULT_RETENTION_SIZE} (100 entries).
*
* @see #SoftHashMap(int)
*/
public SoftHashMap() {
this(DEFAULT_RETENTION_SIZE);
}
/**
* Creates a new SoftHashMap with the specified retention size.
*
* The retention size (n) is the total number of most recent entries in the map that will be
* strongly referenced (ie 'retained') to prevent them from being eagerly garbage collected. That
* is, the point of a SoftHashMap is to allow the garbage collector to remove as many entries from
* this map as it desires, but there will always be (n) elements retained after a GC due to the
* strong references.
*
*
Note that in a highly concurrent environments the exact total number of strong references
* may differ slightly than the actual retentionSize
value. This number is intended
* to be a best-effort retention low water mark.
*
* @param retentionSize the total number of most recent entries in the map that will be strongly
* referenced (retained), preventing them from being eagerly garbage collected by the JVM.
*/
@SuppressWarnings({"unchecked"})
public SoftHashMap(final int retentionSize) {
super();
this.retentionSize = Math.max(0, retentionSize);
queue = new ReferenceQueue<>();
strongReferencesLock = new ReentrantLock();
map = new ConcurrentHashMap<>();
strongReferences = new ConcurrentLinkedQueue<>();
}
/**
* Creates a {@code SoftHashMap} backed by the specified {@code source}, with a default retention
* size of {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
*
* @param source the backing map to populate this {@code SoftHashMap}
* @see #SoftHashMap(Map,int)
*/
public SoftHashMap(final Map source) {
this(DEFAULT_RETENTION_SIZE);
putAll(source);
}
/**
* Creates a {@code SoftHashMap} backed by the specified {@code source}, with the specified
* retention size.
*
* The retention size (n) is the total number of most recent entries in the map that will be
* strongly referenced (ie 'retained') to prevent them from being eagerly garbage collected. That
* is, the point of a SoftHashMap is to allow the garbage collector to remove as many entries from
* this map as it desires, but there will always be (n) elements retained after a GC due to the
* strong references.
*
*
Note that in a highly concurrent environments the exact total number of strong references
* may differ slightly than the actual retentionSize
value. This number is intended
* to be a best-effort retention low water mark.
*
* @param source the backing map to populate this {@code SoftHashMap}
* @param retentionSize the total number of most recent entries in the map that will be strongly
* referenced (retained), preventing them from being eagerly garbage collected by the JVM.
*/
public SoftHashMap(final Map source, final int retentionSize) {
this(retentionSize);
putAll(source);
}
private void addToStrongReferences(final V result) {
strongReferencesLock.lock();
try {
strongReferences.add(result);
trimStrongReferencesIfNecessary();
} finally {
strongReferencesLock.unlock();
}
}
@SuppressFBWarnings(
value = "RV_RETURN_VALUE_IGNORED",
justification = "Poll is only used to trim the list")
private void trimStrongReferencesIfNecessary() {
// trim the strong ref queue if necessary:
while (strongReferences.size() > retentionSize) {
strongReferences.poll();
}
}
// Guarded by the strongReferencesLock in the addToStrongReferences method
/**
* Traverses the ReferenceQueue and removes garbage-collected SoftValue objects from the backing
* map by looking them up using the SoftValue.key data member.
*/
private void processQueue() {
SoftValue sv;
while ((sv = (SoftValue) queue.poll()) != null) {
//noinspection SuspiciousMethodCalls
map.remove(sv.key); // we can access private data!
}
}
@Override
public int size() {
processQueue(); // throw out garbage collected values first
return map.size();
}
@Override
public boolean isEmpty() {
processQueue();
return map.isEmpty();
}
@Override
public boolean containsKey(final Object key) {
processQueue();
return map.containsKey(key);
}
@Override
public boolean containsValue(final Object value) {
processQueue();
final Collection values = values();
return values != null && values.contains(value);
}
@Override
public V get(final Object key) {
processQueue();
V result = null;
final SoftValue value = map.get(key);
if (value != null) {
// unwrap the 'real' value from the SoftReference
result = value.get();
if (result == null) {
// The wrapped value was garbage collected, so remove this entry from the backing map:
//noinspection SuspiciousMethodCalls
map.remove(key);
} else {
// Add this value to the beginning of the strong reference queue (FIFO).
addToStrongReferences(result);
}
}
return result;
}
/**
* Creates a new entry, but wraps the value in a SoftValue instance to enable auto garbage
* collection.
*/
@Override
public V put(final K key, final V value) {
processQueue(); // throw out garbage collected values first
final SoftValue sv = new SoftValue(value, key, queue);
final SoftValue previous = map.put(key, sv);
addToStrongReferences(value);
return previous != null ? previous.get() : null;
}
@Override
public V remove(final Object key) {
processQueue(); // throw out garbage collected values first
final SoftValue raw = map.remove(key);
return raw != null ? raw.get() : null;
}
@Override
public void putAll(final Map extends K, ? extends V> m) {
if (m == null || m.isEmpty()) {
processQueue();
return;
}
for (final Map.Entry extends K, ? extends V> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public void clear() {
strongReferencesLock.lock();
try {
strongReferences.clear();
} finally {
strongReferencesLock.unlock();
}
processQueue(); // throw out garbage collected values
map.clear();
}
@Override
public Set keySet() {
processQueue();
return map.keySet();
}
@Override
public Collection values() {
processQueue();
final Collection keys = map.keySet();
if (keys.isEmpty()) {
//noinspection unchecked
return Collections.EMPTY_SET;
}
final Collection values = new ArrayList(keys.size());
for (final K key : keys) {
final V v = get(key);
if (v != null) {
values.add(v);
}
}
return values;
}
@Override
public Set> entrySet() {
processQueue(); // throw out garbage collected values first
final Collection keys = map.keySet();
if (keys.isEmpty()) {
//noinspection unchecked
return Collections.EMPTY_SET;
}
final Map kvPairs = new HashMap(keys.size());
for (final K key : keys) {
final V v = get(key);
if (v != null) {
kvPairs.put(key, v);
}
}
return kvPairs.entrySet();
}
/**
* We define our own subclass of SoftReference which contains not only the value but also the key
* to make it easier to find the entry in the HashMap after it's been garbage collected.
*/
private static final class SoftValue extends SoftReference {
private final K key;
/**
* Constructs a new instance, wrapping the value, key, and queue, as required by the superclass.
*
* @param value the map value
* @param key the map key
* @param queue the soft reference queue to poll to determine if the entry had been reaped by
* the GC.
*/
private SoftValue(final V value, final K key, final ReferenceQueue super V> queue) {
super(value, queue);
this.key = key;
}
}
}