package com.bigdata.cache;
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.concurrent.ConcurrentHashMap;
import org.apache.log4j.Logger;
/**
* A low-contention/high concurrency weak value cache. This class can offer
* substantially less lock contention and hence greater performance than the
* {@link WeakValueCache}.
* <p>
* The role of the {@link HardReferenceQueue} is to place a minimum upper bound
* on the #of objects in the cache. If the application holds hard references to
* more than this many objects, e.g., in various threads, collections, etc.,
* then the cache size can be larger than the queue capacity. Likewise, if the
* JVM takes too long to finalize weakly reachable references, then the cache
* size can exceed this limit even if the application does not hold ANY hard
* references to the objects in the cache. For these reasons, this class uses an
* initialCapacity for the inner {@link ConcurrentHashMap} that is larger than
* the <i>queueCapacity</i>. This helps to prevent resizing the
* {@link ConcurrentHashMap} which is a relatively expensive operation.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @param <K>
* The generic type of the keys.
* @param <V>
* The generic type of the values.
*/
public class ConcurrentWeakValueCache<K, V> implements
IConcurrentWeakValueCache<K, V> {
protected static transient final Logger log = Logger.getLogger(ConcurrentWeakValueCache.class);
protected static transient final boolean INFO = log.isInfoEnabled();
protected static transient final boolean DEBUG = log.isDebugEnabled();
/**
* A concurrency-savvy map.
*/
final private ConcurrentHashMap<K, WeakReference<V>> map;
/**
* Used to ensure that the [cacheCapacity] MRU weak references are not
* finalized (optional).
*/
final private IHardReferenceQueue<V> queue;
/**
* Reference queue for weak references in entered into the cache. A weak
* reference will appear on this queue once the reference has been cleared.
*/
final private ReferenceQueue<V> referenceQueue;
/**
* Return <code>true</code> iff a {@link ReferenceQueue} is being maintained
* and entries will be removed from the map once the corresponding
* {@link WeakReference} has been cleared. This behavior is controlled by a
* constructor option.
*/
public boolean isRemoveClearedReferences() {
return referenceQueue != null;
}
/**
* Returns the approximate number of keys in the map. Cleared references are
* removed before reporting the size of the map, but the return value is
* still approximate as garbage collection by the JVM can cause references
* to be cleared at any time.
*/
@Override
public int size() {
removeClearedEntries();
return map.size();
}
/**
* The capacity of the backing hard reference queue.
*/
@Override
public int capacity() {
if (queue == null)
return 0;
return queue.capacity();
}
@Override
public void clear() {
if (queue != null) {
// synchronize on the queue so that this operation is atomic.
synchronized (queue) {
// clear hard references so that we don't hold onto things.
queue.clear(true);
// clear the map entries (atomic).
map.clear();
}
}
/*
* Note: We do not need to remove cleared references here since
* removeClearedEntries() guards against removal of the entry under the
* key if the WeakReference object itself has been changed.
*
* However, it does not hurt do invoke it here either. And we do not
* need to be synchronized when we do this.
*/
removeClearedEntries();
}
/**
* Uses the default queue capacity (16), load factor (0.75) and concurrency
* level (16).
*/
public ConcurrentWeakValueCache() {
this(16/* queueCapacity */);
}
/**
* Uses the default load factor (0.75) and concurrency level (16).
*
* @param queueCapacity
* The {@link IHardReferenceQueue} capacity. When ZERO (0), there
* will not be a backing hard reference queue.
*/
public ConcurrentWeakValueCache(final int queueCapacity) {
this(queueCapacity, 0.75f/* loadFactor */, 16/* concurrencyLevel */);
}
/**
* Uses the specified values.
*
* @param queueCapacity
* The {@link IHardReferenceQueue} capacity. When ZERO (0), there
* will not be a backing hard reference queue.
* @param loadFactor
* The load factor.
* @param concurrencyLevel
* The concurrency level.
*/
public ConcurrentWeakValueCache(final int queueCapacity,
final float loadFactor, final int concurrencyLevel) {
this(queueCapacity, loadFactor, concurrencyLevel, true/* removeClearedReferences */);
}
/**
* Uses the specified values and creates a {@link HardReferenceQueue}
* without a timeout.
*
* @param queueCapacity
* The {@link HardReferenceQueue} capacity. When ZERO (0), there
* will not be a backing hard reference queue.
* @param loadFactor
* The load factor.
* @param concurrencyLevel
* The concurrency level.
* @param removeClearedReferences
* When <code>true</code> the cache will remove entries for
* cleared references. When <code>false</code> those entries will
* remain in the cache.
*/
public ConcurrentWeakValueCache(final int queueCapacity,
final float loadFactor, final int concurrencyLevel,
final boolean removeClearedReferences
) {
this(queueCapacity == 0 ? null : new HardReferenceQueue<V>(
null/* listener */, queueCapacity), loadFactor,
concurrencyLevel, removeClearedReferences);
}
/**
* Defaults the initial capacity of the map based on the capacity of the
* optional {@link IHardReferenceQueue} and uses the Java default of
* <code>16</code> if there is no queue.
*
* @param queue
* The {@link IHardReferenceQueue} (optional).
* @param loadFactor
* The load factor.
* @param concurrencyLevel
* The concurrency level.
* @param removeClearedReferences
* When <code>true</code> the cache will remove entries for
* cleared references. When <code>false</code> those entries will
* remain in the cache.
*/
public ConcurrentWeakValueCache(final IHardReferenceQueue<V> queue,
final float loadFactor, final int concurrencyLevel,
final boolean removeClearedReferences
) {
/*
* This uses Math.max(16,queue.capacity()) as initialCapacity of the
* map. The map CAN have more entries than the queue since it will
* contain all non-cleared references inserted into the map. On the
* other hand, the queue is normally used to bound the size of the map
* in designs were map entries are cleared as references are evicted
* from the queue.
*/
this(queue, (queue == null ? 16 : Math.max(16, queue.capacity() / 2)),
loadFactor, concurrencyLevel, removeClearedReferences);
}
/**
* Uses the specified values.
*
* @param queue
* The {@link IHardReferenceQueue} (optional).
* @param initialCapacity
* The initial capacity of the backing hash map.
* @param loadFactor
* The load factor.
* @param concurrencyLevel
* The concurrency level.
* @param removeClearedReferences
* When <code>true</code> the cache will remove entries for
* cleared references. When <code>false</code> those entries will
* remain in the cache.
*/
public ConcurrentWeakValueCache(final IHardReferenceQueue<V> queue,
final int initialCapacity, final float loadFactor,
final int concurrencyLevel, final boolean removeClearedReferences) {
// if (queue == null)
// throw new IllegalArgumentException();
this.queue = queue;
/*
* We set the initial capacity of the ConcurrentHashMap to be larger
* than the capacity of the hard reference queue. This helps to avoid
* resizing the ConcurrentHashMap, which is relatively expensive.
*/
map = new ConcurrentHashMap<K, WeakReference<V>>(initialCapacity,
loadFactor, concurrencyLevel);
if (removeClearedReferences) {
referenceQueue = new ReferenceQueue<V>();
} else {
referenceQueue = null;
}
}
/**
* Returns the value for the key.
*
* @param k
* The key.
*
* @return The value.
*
* @todo If we can locate a ConcurrentRingBuffer then we could further
* improve performance by removing synchronization on the hard
* reference queue in this method.
*
* @see http://en.wikipedia.org/wiki/Lock-free_and_wait-free_algorithms
* @see http://www.audiomulch.com/~rossb/code/lockfree/
*/
@Override
public V get(final K k) {
final WeakReference<V> ref = map.get(k);
if (ref != null) {
/*
* There is an entry under the key, so get the reference paired to
* the key.
*/
final V v = ref.get();
if (v != null) {
/*
* The reference paired with the key has not been cleared so we
* append it to the queue so that the reference will be retained
* longer (a touch).
*/
if (queue != null) {
synchronized (queue) {
queue.add(v);
}
}
return v;
}
}
// Note: Done by put().
// removeClearedEntries();
return null;
}
/**
* Return <code>true</code> iff the map contains an entry for the key
* whose weak reference has not been cleared.
*
* @param k
* The key.
*
* @return <code>true</code> iff the map contains an entry for that key
* whose weak reference has not been cleared.
*/
@Override
public boolean containsKey(final K k) {
final WeakReference<V> ref = map.get(k);
if (ref != null) {
/*
* There is an entry under the key, so get the reference paired to
* the key.
*/
final V v = ref.get();
if (v != null) {
/*
* The reference paired with the key has not been cleared so we
* append it to the queue so that the reference will be retained
* longer (a touch).
*/
if (queue != null) {
synchronized (queue) {
queue.add(v);
}
}
return true;
}
}
// Note: Done by put().
// removeClearedEntries();
return false;
}
/**
* Adds the key-value mapping to the cache.
*
* @param k
* The key.
* @param v
* The value.
*
* @return The old value under the key -or- <code>null</code> if there is
* no entry under the key or if the entry under the key has has its
* reference cleared.
*/
@Override
public V put(final K k, final V v) {
try {
// new reference.
final WeakReference<V> ref = newWeakRef(k, v, referenceQueue);
// final WeakReference<V> ref = referenceQueue == null //
// ? new WeakReference<V>(v)
// : new WeakRef<K, V>(k, v, referenceQueue);
// add to cache.
final WeakReference<V> oldRef = map.put(k, ref);
final V oldVal = oldRef == null ? null : oldRef.get();
if (queue != null) {
synchronized (queue) {
// put onto the hard reference queue.
if (queue.add(v) && DEBUG) {
log.debug("put: key=" + k + ", val=" + v);
}
}
}
// notification.
didUpdate(k, ref, oldRef);
return oldVal;
} finally {
removeClearedEntries();
}
}
/**
* Adds the key-value mapping to the cache iff there is no entry for that
* key. Note that a cleared reference under a key is treated in exactly the
* same manner as if there were no entry under the key (the entry under the
* key is replaced atomically).
*
* @param k
* The key.
* @param v
* The value.
*
* @return the previous value associated with the specified key, or
* <tt>null</tt> if there was no mapping for the key or if the
* entry under the key has has its reference cleared.
*/
@Override
public V putIfAbsent(final K k, final V v) {
try {
// new reference.
final WeakReference<V> ref = newWeakRef(k, v, referenceQueue);
// final WeakReference<V> ref = referenceQueue == null //
// ? new WeakReference<V>(v)
// : new WeakRef<K, V>(k, v, referenceQueue);
// add to cache.
final WeakReference<V> oldRef = map.putIfAbsent(k, ref);
final V oldVal = oldRef == null ? null : oldRef.get();
if (oldRef != null && oldVal == null) {
/*
* There was an entry under the key but its reference has been
* cleared. A cleared value paired to the key is equivalent to
* there being no entry under the key (the entry will be cleared
* the next time removeClearedRefernces() is invoked). Therefore
* we attempt to replace the entry under the key. If this can be
* done atomically, then we have achieved putIfAbsent semantics
* for a key paired to a cleared reference.
*/
if (map.replace(k, oldRef, ref)) {
if (queue != null) {
// no reference under that key.
synchronized (queue) {
// put the new value onto the hard reference queue.
if (queue.add(v) && DEBUG) {
log.debug("put: key=" + k + ", val=" + v);
}
}
}
// notification.
didUpdate(k, ref, oldRef);
// the old value for the key was a cleared reference.
return null;
} else {
/**
* We lost a potential concurrent data race, so make
* recursive call to ensure correct value is returned.
*
* @see <a href="http://trac.blazegraph.com/ticket/1004">
* Concurrent binding problem </a>
*/
return putIfAbsent(k, v);
}
}
if (oldVal == null) {
if (queue != null) {
// no reference under that key.
synchronized (queue) {
// put it onto the hard reference queue.
if (queue.add(v) && DEBUG) {
log.debug("put: key=" + k + ", val=" + v);
}
}
}
// notification.
didUpdate(k, ref, null/* oldVal */);
return null;
}
/*
* There was a non-null reference paired to the key so we return the
* old value and DO NOT update either the map or the hard reference
* queue.
*/
return oldVal;
} finally {
removeClearedEntries();
}
}
/**
* Notification method is invoked if a map entry is inserted or updated by
* {@link #put(Object, Object)} or {@link #putIfAbsent(Object, Object)}.
* This method IS NOT invoked if {@link #putIfAbsent(Object, Object)} did
* not update the map.
*
* @param k
* The key.
* @param newRef
* The {@link WeakReference} for the new value. This was
* generated using
* {@link #newWeakRef(Object, Object, ReferenceQueue)}.
* {@link WeakReference#get()} will always return the value
* inserted into the map since it is on the stack and hence can
* not have been cleared during this callback.
* @param oldRef
* The {@link WeakReference} for the old value. This will be
* <code>null</code> if there was no entry under the key for the
* map. If the entry for a cleared reference is updated then
* {@link WeakReference#get()} will return null for the
* <i>oldRef</i>.
*/
protected void didUpdate(final K k, final WeakReference<V> newRef,
final WeakReference<V> oldRef) {
// NOP
}
@Override
public V remove(final K k) {
try {
final WeakReference<V> ref = removeMapEntry(k);
if (ref != null) {
return ref.get();
}
return null;
} finally {
removeClearedEntries();
}
}
/**
* <p>
* Remove any entries whose weak reference has been cleared from the
* {@link #map}. This method does not block and only polls the
* {@link ReferenceQueue}.
* </p>
* <p>
* Note: This method does not clear entries from the hard reference queue
* because it is not possible to have a weak or soft reference cleared by
* the JVM while a hard reference exists, so it is not possible to have an
* entry in the hard reference queue for a reference that is being cleared
* here.
* </p>
*/
protected void removeClearedEntries()
{
if (referenceQueue == null)
return;
int counter = 0;
for (Reference<? extends V> ref = referenceQueue.poll(); ref != null; ref = referenceQueue
.poll()) {
final K k = ((WeakRef<K, V>) ref).k;
if (map.get(k) == ref) {
if (DEBUG) {
log.debug("Removing cleared reference: key=" + k);
}
removeMapEntry(k);
counter++;
}
}
if( counter > 1 ) {
if( INFO ) {
log.info("Removed " + counter + " cleared references");
}
}
}
/**
* Invoked when a reference needs to be removed from the map.
*
* @param k
* The key.
*/
protected WeakReference<V> removeMapEntry(final K k) {
return map.remove(k);
}
/**
* An iterator that visits the weak reference values in the map. You must
* test each weak reference in order to determine whether its value has been
* cleared as of the moment that you request that value. The entries visited
* by the iterator are not "touched" so the use of the iterator will not
* cause them to be retained any longer than they otherwise would have been
* retained.
*/
@Override
public Iterator<WeakReference<V>> iterator() {
return map.values().iterator();
}
/**
* An iterator that visits the entries in the map. You must test the weak
* reference for each entry in order to determine whether its value has been
* cleared as of the moment that you request that value. The entries visited
* by the iterator are not "touched" so the use of the iterator will not
* cause them to be retained any longer than they otherwise would have been
* retained.
*/
@Override
public Iterator<Map.Entry<K,WeakReference<V>>> entryIterator() {
return map.entrySet().iterator();
}
// /**
// * Clears stale references from the backing {@link HardReferenceQueue}.
// * <p>
// * Note: Evictions from the backing hard reference queue are driven by
// * touches (get, put, remove). This means that the LRU entries in the map
// * will not be cleared from the backing {@link HardReferenceQueue}
// * regardless of their age. Applications may invoke this method occasionally
// * (in general, with a delay of not less than the configured time) in order
// * to ensure that stale references are cleared in the absence of touched and
// * thus may become weakly reachable in a timely fashion.
// *
// * @see HardReferenceQueue#evictStaleRefs()
// */
// public void clearStaleRefs() {
//
// synchronized(queue) {
//
// queue.evictStaleRefs();
//
// }
//
// /*
// * Note: I double that this will notice any stale references that we
// * cleared above because the garbage collector is not synchronous with
// * us here.
// */
// removeClearedEntries();
//
// }
/**
* Factory for new weak references. The default implementation uses a
* {@link WeakReference} unless <code>referenceQueue!=null</code>, in which
* case it uses {@link WeakRef} to pair the key with the
* {@link WeakReference}. This may be extended if you need to track
* additional information in the map entries.
*
* @param k
* The key.
* @param v
* The value.
* @param referenceQueue
* The {@link ReferenceQueue} used to remove map entries whose
* {@link WeakReference}s have been cleared (optional).
*
* @return An instance of a class extending {@link WeakReference} -or- an
* instance of a class extending {@link WeakRef} if
* <code>referenceQueue!=null</code>.
*/
protected WeakReference<V> newWeakRef(final K k, final V v,
final ReferenceQueue<V> referenceQueue) {
if (referenceQueue == null) {
return new WeakReference<V>(v);
}
return new WeakRef<K, V>(k, v, referenceQueue);
}
/**
* Adds the key to the weak reference.
*
* @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson</a>
* @param <K>
* @param <V>
*/
protected static class WeakRef<K,V> extends WeakReference<V> {
private final K k;
public WeakRef(final K k, final V v, final ReferenceQueue<V> queue) {
super(v, queue);
this.k = k;
}
@Override
public String toString() {
return super.toString() + "{key=" + k + ",val=" + get() + "}";
}
}
}