jetbrick.collection.ConcurrentSoftHashMap Maven / Gradle / Ivy
/**
* Copyright 2013-2016 Guoqiang Chen, Shanghai, China. All rights reserved.
*
* Author: Guoqiang Chen
* Email: [email protected]
* WebURL: https://github.com/subchen
*
* 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
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jetbrick.collection;
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.WeakHashMap;
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.
*/
public class ConcurrentSoftHashMap 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 RETENTION_SIZE;
/**
* 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 ConcurrentSoftHashMap with a default retention size size of
* {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
*
* @see #ConcurrentSoftHashMap(int)
*/
public ConcurrentSoftHashMap() {
this(DEFAULT_RETENTION_SIZE);
}
/**
* Creates a new ConcurrentSoftHashMap 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 ConcurrentSoftHashMap 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.
*/
public ConcurrentSoftHashMap(int retentionSize) {
super();
RETENTION_SIZE = Math.max(0, retentionSize);
queue = new ReferenceQueue();
strongReferencesLock = new ReentrantLock();
map = new ConcurrentHashMap>();
strongReferences = new ConcurrentLinkedQueue();
}
/**
* Creates a {@code ConcurrentSoftHashMap} 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 ConcurrentSoftHashMap}
* @see #ConcurrentSoftHashMap(Map,int)
*/
public ConcurrentSoftHashMap(Map source) {
this(DEFAULT_RETENTION_SIZE);
putAll(source);
}
/**
* Creates a {@code ConcurrentSoftHashMap} 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 ConcurrentSoftHashMap 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 ConcurrentSoftHashMap}
* @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 ConcurrentSoftHashMap(Map source, int retentionSize) {
this(retentionSize);
putAll(source);
}
@Override
public V get(Object key) {
processQueue();
V result = null;
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;
}
private void addToStrongReferences(V result) {
strongReferencesLock.lock();
try {
strongReferences.add(result);
trimStrongReferencesIfNecessary();
} finally {
strongReferencesLock.unlock();
}
}
//Guarded by the strongReferencesLock in the addToStrongReferences method
private void trimStrongReferencesIfNecessary() {
//trim the strong ref queue if necessary:
while (strongReferences.size() > RETENTION_SIZE) {
strongReferences.poll();
}
}
/**
* 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 boolean isEmpty() {
processQueue();
return map.isEmpty();
}
@Override
public boolean containsKey(Object key) {
processQueue();
return map.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
processQueue();
Collection values = values();
return values != null && values.contains(value);
}
@Override
public void putAll(Map extends K, ? extends V> m) {
if (m == null || m.isEmpty()) {
processQueue();
return;
}
for (Map.Entry extends K, ? extends V> entry : m.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
@Override
public Set keySet() {
processQueue();
return map.keySet();
}
@Override
public Collection values() {
processQueue();
Collection keys = map.keySet();
if (keys.isEmpty()) {
//noinspection unchecked
return Collections.emptySet();
}
Collection values = new ArrayList(keys.size());
for (K key : keys) {
V v = get(key);
if (v != null) {
values.add(v);
}
}
return values;
}
/**
* Creates a new entry, but wraps the value in a SoftValue instance to enable auto garbage collection.
*/
@Override
public V put(K key, V value) {
processQueue(); // throw out garbage collected values first
SoftValue sv = new SoftValue(value, key, queue);
SoftValue previous = map.put(key, sv);
addToStrongReferences(value);
return previous != null ? previous.get() : null;
}
@Override
public V remove(Object key) {
processQueue(); // throw out garbage collected values first
SoftValue raw = map.remove(key);
return raw != null ? raw.get() : null;
}
@Override
public void clear() {
strongReferencesLock.lock();
try {
strongReferences.clear();
} finally {
strongReferencesLock.unlock();
}
processQueue(); // throw out garbage collected values
map.clear();
}
@Override
public int size() {
processQueue(); // throw out garbage collected values first
return map.size();
}
@Override
public Set> entrySet() {
processQueue(); // throw out garbage collected values first
Collection keys = map.keySet();
if (keys.isEmpty()) {
//noinspection unchecked
return Collections.emptySet();
}
Map kvPairs = new HashMap(keys.size());
for (K key : keys) {
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 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(V value, K key, ReferenceQueue super V> queue) {
super(value, queue);
this.key = key;
}
}
}