/*
* GeoTools - The Open Source Java GIS Toolkit
* http://geotools.org
*
* (C) 2006-2008, Open Source Geospatial Foundation (OSGeo)
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation;
* version 2.1 of the License.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*/
package org.geotools.util;
import java.lang.ref.SoftReference;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geotools.resources.i18n.ErrorKeys;
import org.geotools.resources.i18n.Errors;
import org.geotools.util.logging.Logging;
/**
* A hash map implementation that uses {@linkplain SoftReference soft references}, leaving memory
* when an entry is not used anymore and memory is low.
* <p>
* This map implementation actually maintains some of the first entries as hard references.
* Only oldest entries are retained by soft references, in order to avoid too aggressive garbage
* collection. The amount of entries to retain by hard reference is specified at {@linkplain
* #SoftValueHashMap(int) construction time}.
* <p>
* This map is thread-safe. It accepts the null key, but doesn't accepts null values. Usage
* of {@linkplain #values value}, {@linkplain #keySet key} or {@linkplain #entrySet entry}
* collections are supported except for direct usage of their iterators, which may throw
* {@link java.util.ConcurrentModificationException} randomly depending on the garbage collector
* activity.
*
* @param <K> The type of keys in the map.
* @param <V> The type of values in the map.
*
* @since 2.3
*
* @source $URL$
* @version $Id$
* @author Simone Giannecchini
* @author Martin Desruisseaux
*/
public class SoftValueHashMap<K,V> extends AbstractMap<K,V> {
static final Logger LOGGER = Logging.getLogger(SoftValueHashMap.class);
/**
* The default value for {@link #hardReferencesCount}.
*/
private static final int DEFAULT_HARD_REFERENCE_COUNT = 20;
/**
* The map of hard or soft references. Values are either direct reference to the objects,
* or wrapped in a {@code Reference} object.
*/
private final Map<K,Object> hash = new HashMap<K,Object>();
/**
* The FIFO list of keys to hard references. Newest elements are first, and latest elements
* are last. This list should never be longer than {@link #hardReferencesCount}.
*/
private final LinkedList<K> hardCache = new LinkedList<K>();
/**
* The number of hard references to hold internally.
*/
private final int hardReferencesCount;
/**
* The entries to be returned by {@link #entrySet()}, or {@code null} if not yet created.
*/
private transient Set<Map.Entry<K,V>> entries;
/**
* The eventual cleaner
*/
private final ValueCleaner cleaner;
/**
* Creates a map with the default hard references count.
*/
public SoftValueHashMap() {
this.cleaner = null;
hardReferencesCount = DEFAULT_HARD_REFERENCE_COUNT;
}
/**
* Creates a map with the specified hard references count.
*
* @param hardReferencesCount The maximal number of hard references to keep.
*/
public SoftValueHashMap(final int hardReferencesCount) {
this.cleaner = null;
this.hardReferencesCount = hardReferencesCount;
}
/**
* Creates a map with the specified hard references count.
*
* @param hardReferencesCount The maximal number of hard references to keep.
*/
public SoftValueHashMap(final int hardReferencesCount, ValueCleaner cleaner) {
this.cleaner = cleaner;
this.hardReferencesCount = hardReferencesCount;
}
/**
* Ensures that the specified value is non-null.
*/
private static void ensureNotNull(final Object value) throws IllegalArgumentException {
if (value == null) {
throw new IllegalArgumentException(Errors.format(ErrorKeys.NULL_ARGUMENT_$1, "value"));
}
}
/**
* Performs a consistency check on this map. This method is used for tests and
* assertions only.
*/
final boolean isValid() {
int count=0, size=0;
synchronized (hash) {
for (final Map.Entry<K,?> entry : hash.entrySet()) {
if (entry.getValue() instanceof Reference) {
count++;
} else {
assert hardCache.contains(entry.getKey());
}
size++;
}
assert size == hash.size();
assert hardCache.size() == Math.min(size, hardReferencesCount);
}
return count == Math.max(size - hardReferencesCount, 0);
}
/**
* Returns the number of entries in this map.
*/
@Override
public int size() {
synchronized (hash) {
return hash.size();
}
}
/**
* Returns {@code true} if this map contains a mapping for the specified key.
*/
@Override
public boolean containsKey(final Object key) {
synchronized (hash) {
return hash.containsKey(key);
}
}
/**
* Returns {@code true} if this map maps one or more keys to this value.
*/
@Override
public boolean containsValue(final Object value) {
ensureNotNull(value);
synchronized (hash) {
/*
* We must rely on the super-class default implementation, not on HashMap
* implementation, because some references are wrapped into SoftReferences.
*/
return super.containsValue(value);
}
}
/**
* Returns the value to which this map maps the specified key. Returns {@code null} if
* the map contains no mapping for this key, or the value has been garbage collected.
*
* @param key key whose associated value is to be returned.
* @return the value to which this map maps the specified key, or {@code null} if none.
*/
@Override
public V get(final Object key) {
synchronized (hash) {
Object value = hash.get(key);
if (value instanceof Reference) {
/*
* The value is a soft reference only if it was not used for a while and the map
* contains more than 'hardReferenceCount' entries. Otherwise, it is an ordinary
* reference and is returned directly. See the 'retainStrongly' method.
*
* If the value is a soft reference, get the referent and clear it immediately
* for avoiding the reference to be enqueded. We abandon the soft reference and
* reinject the referent as a strong reference in the hash map, since we try to
* keep the last entries by strong references.
*/
value = ((Reference) value).getAndClear();
if (value != null) {
/*
* Transforms the soft reference into a hard one. The cast should be safe
* because hash.get(key) should not have returned a non-null value if the
* key wasn't valid.
*/
@SuppressWarnings("unchecked")
final K k = (K) key;
hash.put(k, value);
retainStrongly(k);
} else {
// The value has already been garbage collected.
hash.remove(key);
}
}
/*
* The safety of this cast depends only on this implementation, not on users.
* It should be safe if there is no bug in the way this class manages 'hash'.
*/
@SuppressWarnings("unchecked")
final V v = (V) value;
return v;
}
}
/**
* Declares that the value for the specified key must be retained by hard reference.
* If there is already {@link #hardReferencesCount} hard references, then this method
* replaces the oldest hard reference by a soft one.
*/
private void retainStrongly(final K key) {
assert Thread.holdsLock(hash);
assert !hardCache.contains(key) : key;
hardCache.addFirst(key);
if (hardCache.size() > hardReferencesCount) {
// Remove the last entry if list longer than hardReferencesCount
final K toRemove = hardCache.removeLast();
final Object value = hash.get(toRemove);
assert value!=null && !(value instanceof Reference) : toRemove;
@SuppressWarnings("unchecked")
final V v = (V) value;
hash.put(toRemove, new Reference<K,V>(hash, toRemove, v));
assert hardCache.size() == hardReferencesCount;
}
assert isValid();
}
/**
* Associates the specified value with the specified key in this map.
*
* @param key Key with which the specified value is to be associated.
* @param value Value to be associated with the specified key. The value can't be null.
*
* @return Previous value associated with specified key, or {@code null}
* if there was no mapping for key.
*/
@Override
public V put(final K key, final V value) {
ensureNotNull(value);
synchronized (hash) {
Object oldValue = hash.put(key, value);
if (oldValue instanceof Reference) {
oldValue = ((Reference) oldValue).getAndClear();
} else if (oldValue != null) {
/*
* The value was retained by hard reference, which implies that the key must be in
* the hard-cache list. Removes the key from the list, since we want to reinsert it
* at the begining of the list in order to mark the value as the most recently used.
* This method performs a linear search, which may be quite ineficient. But it still
* efficient enough if the key was recently used, in which case it appears near the
* begining of the list. We assume that this is a common case. We may revisit later
* if profiling show that this is a performance issue.
*/
if (!hardCache.remove(key)) {
throw new AssertionError(key);
}
}
retainStrongly(key);
@SuppressWarnings("unchecked")
final V v = (V) oldValue;
return v;
}
}
/**
* Copies all of the mappings from the specified map to this map.
*
* @param map Mappings to be stored in this map.
*/
@Override
public void putAll(final Map<? extends K, ? extends V> map) {
synchronized (hash) {
super.putAll(map);
}
}
/**
* Removes the mapping for this key from this map if present.
*
* @param key Key whose mapping is to be removed from the map.
* @return previous value associated with specified key, or {@code null}
* if there was no entry for key.
*/
@Override
public V remove(final Object key) {
synchronized (hash) {
Object oldValue = hash.remove(key);
if (oldValue instanceof Reference) {
oldValue = ((Reference) oldValue).getAndClear();
} else if (oldValue != null) {
/*
* See the comment in the 'put' method.
*/
if (!hardCache.remove(key)) {
throw new AssertionError(key);
}
}
@SuppressWarnings("unchecked")
final V v = (V) oldValue;
return v;
}
}
/**
* Removes all mappings from this map.
*/
@Override
public void clear() {
synchronized (hash) {
for (final Iterator it=hash.values().iterator(); it.hasNext();) {
final Object value = it.next();
if (value instanceof Reference) {
((Reference) value).getAndClear();
}
}
hash.clear();
hardCache.clear();
}
}
/**
* Returns a set view of the mappings contained in this map.
*/
@Override
public Set<Map.Entry<K,V>> entrySet() {
synchronized (hash) {
if (entries == null) {
entries = new Entries();
}
return entries;
}
}
/**
* Compares the specified object with this map for equality.
*
* @param object The object to compare with this map for equality.
*/
@Override
public boolean equals(final Object object) {
synchronized (hash) {
return super.equals(object);
}
}
/**
* Returns the hash code value for this map.
*/
@Override
public int hashCode() {
synchronized (hash) {
return super.hashCode();
}
}
/**
* Returns a string representation of this map.
*/
@Override
public String toString() {
synchronized (hash) {
return super.toString();
}
}
/**
* Implementation of the entries set to be returned by {@link #entrySet()}.
*/
private final class Entries extends AbstractSet<Map.Entry<K,V>> {
/**
* Returns an iterator over the elements contained in this collection.
*/
public Iterator<Map.Entry<K,V>> iterator() {
synchronized (hash) {
return new Iter<K,V>(hash);
}
}
/**
* Returns the number of elements in this collection.
*/
public int size() {
return SoftValueHashMap.this.size();
}
/**
* Returns {@code true} if this collection contains the specified element.
*/
@Override
public boolean contains(final Object entry) {
synchronized (hash) {
return super.contains(entry);
}
}
/**
* Returns an array containing all of the elements in this collection.
*/
@Override
public Object[] toArray() {
synchronized (hash) {
return super.toArray();
}
}
/**
* Returns an array containing all of the elements in this collection.
*/
@Override
public <T> T[] toArray(final T[] array) {
synchronized (hash) {
return super.toArray(array);
}
}
/**
* Removes a single instance of the specified element from this collection,
* if it is present.
*/
@Override
public boolean remove(final Object entry) {
synchronized (hash) {
return super.remove(entry);
}
}
/**
* Returns {@code true} if this collection contains all of the elements
* in the specified collection.
*/
@Override
public boolean containsAll(final Collection<?> collection) {
synchronized (hash) {
return super.containsAll(collection);
}
}
/**
* Adds all of the elements in the specified collection to this collection.
*/
@Override
public boolean addAll(final Collection<? extends Map.Entry<K,V>> collection) {
synchronized (hash) {
return super.addAll(collection);
}
}
/**
* Removes from this collection all of its elements that are contained in
* the specified collection.
*/
@Override
public boolean removeAll(final Collection<?> collection) {
synchronized (hash) {
return super.removeAll(collection);
}
}
/**
* Retains only the elements in this collection that are contained in the
* specified collection.
*/
@Override
public boolean retainAll(final Collection<?> collection) {
synchronized (hash) {
return super.retainAll(collection);
}
}
/**
* Removes all of the elements from this collection.
*/
@Override
public void clear() {
SoftValueHashMap.this.clear();
}
/**
* Returns a string representation of this collection.
*/
@Override
public String toString() {
synchronized (hash) {
return super.toString();
}
}
}
/**
* The iterator to be returned by {@link Entries}.
*/
private static final class Iter<K,V> implements Iterator<Map.Entry<K,V>> {
/**
* A copy of the {@link SoftValueHashMap#hash} field.
*/
private final Map<K,Object> hash;
/**
* The iterator over the {@link #hash} entries.
*/
private final Iterator<Map.Entry<K,Object>> iterator;
/**
* The next entry to be returned by the {@link #next} method, or {@code null}
* if not yet computed of if the iteration is finished.
*/
private transient Map.Entry<K,V> entry;
/**
* Creates an iterator for the specified {@link SoftValueHashMap#hash} field.
*/
Iter(final Map<K,Object> hash) {
this.hash = hash;
this.iterator = hash.entrySet().iterator();
}
/**
* Set {@link #entry} to the next entry to iterate. Returns {@code true} if
* an entry has been found, or {@code false} if the iteration is finished.
*/
@SuppressWarnings("unchecked")
private boolean findNext() {
assert Thread.holdsLock(hash);
while (iterator.hasNext()) {
final Map.Entry<K,Object> candidate = iterator.next();
Object value = candidate.getValue();
if (value instanceof Reference) {
value = ((Reference) value).get();
entry = new MapEntry<K,V>(candidate.getKey(), (V) value);
return true;
}
if (value != null) {
entry = (Map.Entry<K,V>) candidate;
return true;
}
}
return false;
}
/**
* Returns {@code true} if this iterator can return more value.
*/
public boolean hasNext() {
synchronized (hash) {
return entry!=null || findNext();
}
}
/**
* Returns the next value. If some value were garbage collected after the
* iterator was created, they will not be returned. Note however that a
* {@link ConcurrentModificationException} may be throw if the iteration
* is not synchronized on {@link #hash}.
*/
public Map.Entry<K,V> next() {
synchronized (hash) {
if (entry==null && !findNext()) {
throw new NoSuchElementException();
}
final Map.Entry<K,V> next = entry;
entry = null; // Flags that a new entry will need to be lazily fetched.
return next;
}
}
/**
* Removes the last entry.
*/
public void remove() {
synchronized (hash) {
iterator.remove();
}
}
}
/**
* A soft reference to a map entry. Soft references are created only when the map contains
* more than {@link #hardReferencesCount}, in order to avoid to put more pressure on the
* garbage collector.
*/
private static final class Reference<K,V> extends SoftReference<V> {
/**
* A reference to the {@link SoftValueHashMap#hash} entries. We keep this reference instead
* than a reference to {@link SoftValueHashMap} itself in order to avoid indirect retention
* of {@link SoftValueHashMap#hardCache}, which is not needed for this reference.
*/
private final Map<K,Object> hash;
/**
* The key for the entry to be removed when the soft reference is cleared.
*/
private final K key;
/**
* The eventual value cleaner
*/
private ValueCleaner cleaner;
/**
* Creates a soft reference for the specified key-value pair.
*/
Reference(final Map<K,Object> hash, final K key, final V value) {
super(value, WeakCollectionCleaner.DEFAULT.referenceQueue);
this.hash = hash;
this.key = key;
}
/**
* Gets and clear this reference object. This method performs no additional operation.
* More specifically:
* <ul>
* <li>It does not enqueue the reference.</li>
* <li>It does not remove the reference from the hash map.</li>
* </ul>
* This is because this method is invoked when the entry should have already be removed,
* or is about to be removed.</li>
*/
final Object getAndClear() {
assert Thread.holdsLock(hash);
final Object value = get();
super.clear();
return value;
}
/**
* Removes the entries from the backing hash map. This method need to
* override the {@link SoftReference#clear} method because it is invoked
* by {@link WeakCollectionCleaner}.
*/
@Override
public void clear() {
if(cleaner != null) {
final Object value = get();
if(value != null) {
try {
cleaner.clean(value);
} catch(Throwable t) {
// never let a bad implementation break soft reference cleaning
LOGGER.log(Level.SEVERE, "Exception occurred while cleaning soft referenced object", t);
}
}
}
super.clear();
synchronized (hash) {
final Object old = hash.remove(key);
/*
* If the entry was used for an other value, then put back the old value. This
* case may occurs if a new value was set in the hash map before the old value
* was garbage collected.
*/
if (old != this && old != null) {
hash.put(key, old);
}
}
}
}
/**
* A delegate that can be used to perform clean up operation, such as resource closing,
* before the values cached in soft part of the cache gets disposed of
* @author Andrea Aime - OpenGeo
*
*/
public static interface ValueCleaner {
/**
* Cleans the specified object
* @param object
*/
public void clean(Object object);
}
}