Java tutorial
//package com.aliasi.util; //import com.aliasi.util.AbstractExternalizable; import java.lang.ref.SoftReference; import java.io.Serializable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput; import java.util.AbstractMap; import java.util.Map; import java.util.Set; import java.util.HashSet; /** * A <code>FastCache</code> is a map implemented with soft references, * optimistic copy-on-write updates, and approximate count-based * pruning. It is intended for scalable multi-threaded caches. It * sacrifices recall of mappings for speed of put and get operations * along with conservative memory guarantees. * * <p><i>Note:</i>The class {@link HardFastCache} is nearly identical * to this class, but with no soft references around hash buckets. * * <p>The basis of the cache is a fixed-size hash map, based on values * returned by objects' <code>hashCode()</code> and * <code>equals(Object)</code> methods. * * <p>The map entries in the hash map are stored in buckets held by * soft references. Thus entries in the mapping may be garbage * collected. In the implementation, whole hash buckets are * collected, which is safe and highly efficient but may require some * additional recomputation of values that were removed by pruning. * * <p>Entries are stored in the map using a very optimistic update. * No synchronization at all is performed on the cache entries or * their counts. A copy-on-write strategy coupled with Java's memory * model for references ensures that the cache remains consistent, if * not complete. What this means is that multiple threads may both * try to cache a mapping and only one will be saved and/or * incremented in count. * * <p>When the table approximately exceeds the specified load factor, * the thread performing the add will perform a garbage collection by * reducing reference counts by half, rounding down, and removing * entries with zero counts. Pruning is subject to the caveats * mentioned in the last paragraph. Counts are not guaranteed to be * accurate. Pruning itself is synchronized and more conservatively * copy-on-write. By setting the load factor to * <code>Double.POSITIVE_INFINITY</code> there will be never be any * pruning done by this class; all pruning will take place by soft * reference garbage collection. * * <p>A fast cache acts as a mapping with copy-on-write semantics. * Equality and iteration are defined as usual, with the caveat that * the snapshot taken of the elements may not be up to date. Iterators * may be used concurrently, but their remove methods do not affect * the underlying cache. * * <p><b>Serialization</b></p> * * <p>A fast cache may be serialized if the keys and values it * contains are serializable. It may always be serialized if * it is first cleared using {@link #clear()}. * * <p><b>References</b></p> * * <p>For more information on soft references, see: * * <ul> * <li> Peter Hagar. 2002. <a href="http://www-128.ibm.com/developerworks/library/j-refs/">Guidelines for using the Java 2 reference classes</a>. IBM DeveloperWorks. * <li> Jeff Friesen. 2002. <a href="http://www.javaworld.com/javaworld/jw-01-2002/jw-0104-java101.html">Trash talk, part 2: reference objects API</a>. JavaWorld. * </ul> * * For information on copy-on-write and optimistic updates, * see section 2.4 of: * * <ul> * <li> Doug Lea. 2000. * <i>Concurrent Programming in Java. Second Edition.</i> * Addison-Wesley. * </ul> * * @author Bob Carpenter * @version 3.8.3 * @since LingPipe2.2 * @param <K> the type of keys in the map * @param <V> the type of values in the map */ public class FastCache<K, V> extends AbstractMap<K, V> implements Serializable { static final long serialVersionUID = 3003326726041067827L; private static final double DEFAULT_LOAD_FACTOR = 0.5; private final SoftReference<Record<K, V>>[] mBuckets; private volatile int mNumEntries = 0; private int mMaxEntries; /** * Constrcut a fast cache of the specified size and default load * factor. The default load factor is 0.5. See {@link * #FastCache(int,double)} for more information. * * @param size Number of buckets in cache * @throws IllegalArgumentException if the size is less than 2. */ public FastCache(int size) { this(size, DEFAULT_LOAD_FACTOR); } FastCache(int maxEntries, int numBuckets, boolean ignoreMe) { mMaxEntries = maxEntries; // cut-and-paste from below, must be in-constructor for final @SuppressWarnings({ "unchecked", "rawtypes" }) SoftReference<Record<K, V>>[] bucketsTemp = (SoftReference<Record<K, V>>[]) new SoftReference[numBuckets]; mBuckets = bucketsTemp; } /** * Construct a fast cache of the specified size (measured in * number of hash buckets) and load factor. The size times the * load factor must be greater than or equal to 1. When the * (approximate) number of entries exceeds the load factor times * the size, the cache is pruned. * * @param size Number of buckets in the cache. * @param loadFactor Load factor of the cache. * @throws IllegalArgumentException If the size is less than one or the load * factor is not a positive finite value. */ public FastCache(int size, double loadFactor) { if (size < 1) { String msg = "Cache size must be at least 1." + " Found cache size=" + size; throw new IllegalArgumentException(msg); } if (loadFactor < 0.0 || Double.isNaN(loadFactor) || Double.isInfinite(loadFactor)) { String msg = "Load factor must be finite and positive." + " found loadFactor=" + loadFactor; throw new IllegalArgumentException(msg); } mMaxEntries = (int) (loadFactor * (double) size); if (mMaxEntries < 1) { String msg = "size * loadFactor must be > 0." + " Found size=" + size + " loadFactor=" + loadFactor; throw new IllegalArgumentException(msg); } // required for array @SuppressWarnings({ "unchecked", "rawtypes" }) SoftReference<Record<K, V>>[] bucketsTemp = (SoftReference<Record<K, V>>[]) new SoftReference[size]; mBuckets = bucketsTemp; } Record<K, V> getFirstRecord(int bucketId) { SoftReference<Record<K, V>> ref = mBuckets[bucketId]; return ref == null ? null : ref.get(); } void setFirstRecord(int bucketId, Record<K, V> record) { SoftReference<Record<K, V>> ref = new SoftReference<Record<K, V>>(record); mBuckets[bucketId] = ref; } /** * Returns the value of the specified key or <code>null</code> if * there is no value attached. Note that the argument is not * the generic <code><K></code> key type, but <code>Object</code> * to match the requirements of <code>java.util.Map</code>. * * <p><i>Warning:</i> Because of the approximate cache-like * behavior of this class, key-value pairs previously added * by the {@link #put(Object,Object)} method may disappear. * * @param key Mapping key. * @return The value for the specified key. */ @Override public V get(Object key) { int bucketId = bucketId(key); for (Record<K, V> record = getFirstRecord(bucketId); record != null; record = record.mNextRecord) { if (record.mKey.equals(key)) { ++record.mCount; return record.mValue; } } return null; } int bucketId(Object key) { return java.lang.Math.abs(key.hashCode() % mBuckets.length); } /** * Sets the value of the specified key to the specified value. * If there is already a value for the specified key, the count * is incremented, but the value is not replaced. * * <p><i>Warning:</i> Because of the approximate cache-like * behavior of this class, setting the value of a key with this * method is not guaranteed to replace older values or remain in * the mapping until the next call to {@link #get(Object)}. * * @param key Mapping key. * @param value New value for the specified key. * @return <code>null</code>, even if there is an existing * value for the specified key. */ @Override public V put(K key, V value) { int bucketId = bucketId(key); Record<K, V> firstRecord = getFirstRecord(bucketId); for (Record<K, V> record = firstRecord; record != null; record = record.mNextRecord) { if (record.mKey.equals(key)) { ++record.mCount; // increment instead return null; // already there } } prune(); firstRecord = getFirstRecord(bucketId); // may've been pruned Record<K, V> record = new Record<K, V>(key, value, firstRecord); setFirstRecord(bucketId, record); ++mNumEntries; return null; } /** * Removes all entries from this cache. */ public void clear() { synchronized (this) { for (SoftReference<Record<K, V>> ref : mBuckets) if (ref != null) ref.clear(); } } /** * Prunes this cache by (approximately) dividing the counts of * entries by two and removing the ones with zero counts. This * operation is approximate in the sense that the optimistic * update strategy applied is not guaranteed to actually do any * pruning or decrements of counts. */ public void prune() { // only synchronized versus other prunes; // other puts, etc. may interfere, which is OK synchronized (this) { if (mNumEntries < mMaxEntries) return; int count = 0; for (int i = 0; i < mBuckets.length; ++i) { Record<K, V> record = getFirstRecord(i); Record<K, V> prunedRecord = prune(record); setFirstRecord(i, prunedRecord); for (Record<K, V> r = prunedRecord; r != null; r = r.mNextRecord) ++count; } mNumEntries = count; } } final Record<K, V> prune(Record<K, V> inRecord) { Record<K, V> record = inRecord; while (record != null && (record.mCount = (record.mCount >>> 1)) == 0) record = record.mNextRecord; if (record == null) return null; record.mNextRecord = prune(record.mNextRecord); return record; } /** * Returns a snapshot of the entries in this map. * This set is not backed by this cache, so that changes * to the cache do not affect the cache and vice-versa. * * @return The set of entries in this cache. */ @Override public Set<Map.Entry<K, V>> entrySet() { HashSet<Map.Entry<K, V>> entrySet = new HashSet<Map.Entry<K, V>>(); for (int i = 0; i < mBuckets.length; ++i) for (Record<K, V> record = getFirstRecord(i); record != null; record = record.mNextRecord) entrySet.add(record); return entrySet; } static final class Record<K, V> implements Map.Entry<K, V> { final K mKey; final V mValue; volatile Record<K, V> mNextRecord; volatile int mCount; Record(K key, V value) { this(key, value, null); } Record(K key, V value, Record<K, V> nextRecord) { this(key, value, nextRecord, 1); } Record(K key, V value, Record<K, V> nextRecord, int count) { mKey = key; mValue = value; mNextRecord = nextRecord; mCount = count; } public K getKey() { return mKey; } public V getValue() { return mValue; } @Override public int hashCode() { return (mKey == null ? 0 : mKey.hashCode()) ^ (mValue == null ? 0 : mValue.hashCode()); } @Override @SuppressWarnings("rawtypes") // for instanceof public boolean equals(Object o) { if (!(o instanceof Map.Entry)) return false; Map.Entry<?, ?> e2 = (Map.Entry<?, ?>) o; return (mKey == null ? e2.getKey() == null : mKey.equals(e2.getKey())) && (mValue == null ? e2.getValue() == null : mValue.equals(e2.getValue())); } public V setValue(V value) { String msg = "Cache records may not be set."; throw new UnsupportedOperationException(msg); } } }