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; import com.bigdata.BigdataStatics; /** * A low-contention/high concurrency weak value cache. This class can offer * substantially less lock contention and hence greater performance than the * {@link WeakValueCache}. The class batches touches using a thread-local queue * in order to improve throughput over the {@link ConcurrentWeakValueCache}. * Because of the thread-local queues, which must have 64 to 128 elements to be * effective, <em>this class is not suitable when the total size of the cache is * limited to a relatively small number of entries</em>. Further, since there is * no provision for flushing the thread-local queues in response to inactivity * or the end of some work cycle for a thread, the thread-local queues can cause * hard references to be retained for an arbitrary period of time. Therefore * <em>DO NOT use this class when objects entered into the cache MUST have their * references cleared in a timely manner</em>. * <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> * @version $Id: ConcurrentWeakValueCache.java 2265 2009-10-26 12:51:06Z * thompsonbry $ * @param <K> * The generic type of the keys. * @param <V> * The generic type of the values. */ public class ConcurrentWeakValueCacheWithBatchedUpdates<K, V> implements IConcurrentWeakValueCache<K, V> { private static transient final Logger log = Logger.getLogger(ConcurrentWeakValueCacheWithBatchedUpdates.class); // private static transient final boolean INFO = log.isInfoEnabled(); private 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). * <p> * Note: Unlike the {@link ConcurrentWeakValueCache}, the {@link #queue} in * this implementation is thread-safe due to the use of thread-local queues * and batched updates to the backing {@link IHardReferenceQueue}. This can * substantially reduce thread contention. */ final private HardReferenceQueueWithBatchingUpdates<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. */ public int size() { removeClearedEntries(); return map.size(); } /** * The capacity of the backing hard reference queue. */ public int capacity() { return queue.capacity(); } /** * Note: This operation IS NOT atomic. It can not be made atomic without * synchronizing all updates on the backing queue, which would defeat the * purpose of this class. */ 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 ConcurrentWeakValueCacheWithBatchedUpdates() { 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 ConcurrentWeakValueCacheWithBatchedUpdates(final int queueCapacity) { this(queueCapacity, 0.75f/* loadFactor */, 16/* concurrencyLevel */); } /** * Uses the specified values. * * @param queueCapacity * The {@link IHardReferenceQueue} capacity. * @param loadFactor * The load factor. * @param concurrencyLevel * The concurrency level. */ public ConcurrentWeakValueCacheWithBatchedUpdates(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. * @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 ConcurrentWeakValueCacheWithBatchedUpdates(final int queueCapacity, final float loadFactor, final int concurrencyLevel, final boolean removeClearedReferences ) { this(new HardReferenceQueue<V>(null/* listener */, queueCapacity), loadFactor, concurrencyLevel, removeClearedReferences); } /** * Defaults the initial capacity of the map based on the capacity of the * {@link IHardReferenceQueue}. * * @param queue * The {@link IHardReferenceQueue}. * @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 ConcurrentWeakValueCacheWithBatchedUpdates( 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, Math.max(16, queue.capacity() / 2),// initialCapacity loadFactor, // concurrencyLevel, // removeClearedReferences // ); } /** * Uses the specified values. * * @param queue * The backing {@link IHardReferenceQueue}. This will be wrapped * with a {@link HardReferenceQueueWithBatchingUpdates} instance. * @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 ConcurrentWeakValueCacheWithBatchedUpdates( final IHardReferenceQueue<V> queue, final int initialCapacity, final float loadFactor, final int concurrencyLevel, final boolean removeClearedReferences) { if (queue == null) throw new IllegalArgumentException(); this.queue = new HardReferenceQueueWithBatchingUpdates<V>(// BigdataStatics.threadLocalBuffers,// threadLocalBuffers 16, // concurrencyLevel queue, // sharedQueue 10, // threadLocalQueueNScan 64, // threadLocalQueueCapacity 32, // threadLocalTryLockSize new IBatchedUpdateListener<V>() { /** * Since processing the ReferenceQueue requires a lock, we * let the thread which batches the access order updates * remove entries for cleared references. [Historically this * task was handled by the thread modifying the map in put() * and putIfAbsent(), which caused contention for the lock * inside of the ReferenceQueue.] */ public void didBatchUpdates() { removeClearedEntries(); } } ); /* * 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. * * @see http://en.wikipedia.org/wiki/Lock-free_and_wait-free_algorithms * @see http://www.audiomulch.com/~rossb/code/lockfree/ */ 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. */ 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. */ 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. */ 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; } } 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 } 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 polls the {@link ReferenceQueue}, but the * {@link ReferenceQueue} uses an internal lock so this does impose * synchronization on the callers. For that reason, this method is now * invoked from an {@link IBatchedUpdateListener} which is called when the * access order updates are batched for a given thread. This does not * eliminate the possibility of synchronization costs inside of * {@link ReferenceQueue}, but it should dramatically reduce those costs. * </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 (log.isInfoEnabled()) { 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. */ 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. */ 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> * @version $Id$ * @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; } } }