package com.aliasi.util; import java.lang.ref.SoftReference; import java.util.AbstractMap; import java.util.Map; import java.util.Set; import java.util.HashSet; /** * A FastCache 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. * *

The basis of the cache is a fixed-size hash map, based on values * returned by objects' hashCode() and * equals(Object) methods. * *

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. * *

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. * *

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 * Double.POSITIVE_INFINITY there will be never be any * pruning done by this class; all pruning will take place by soft * reference garbage collection. * *

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. * * For more information on soft references, see: * *

* * For information on copy-on-write and optimistic updates, * see section 2.4 of: * * * * @author Bob Carpenter * @version 3.0 * @since LingPipe2.2 */ public class FastCache extends AbstractMap { private static final double DEFAULT_LOAD_FACTOR = 0.5; private final SoftReference[] 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. * * @param size Size of cache. * @throws IllegalArgumentException if the size is less than 2. */ public FastCache(int size) { this(size,DEFAULT_LOAD_FACTOR); } /** * Construct a fast cache of the specified size 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 Size of the cache. * @param loadFactor Load factor of the cache. * @throws IllegalArgumentException If the size times the load * factor is less than one. */ public FastCache(int size, double loadFactor) { mMaxEntries = (int) (loadFactor * (double) size); if (mMaxEntries < 1) { String msg = "size * loadFactor must be > 0." + " Found size=" + size + " loadFactor=" + loadFactor; throw new IllegalArgumentException(msg); } // using following causes cache to hang. // mBuckets // = (SoftReference>[]) // java.lang.reflect.Array // .newInstance(new SoftReference>(new Record(null,null)).getClass(),5); mBuckets = new SoftReference[size]; } Record getFirstRecord(int bucketId) { SoftReference ref = (SoftReference) mBuckets[bucketId]; if (ref == null) return null; Record record = (Record) ref.get(); if (record == null) return null; return (Record) record; } void setFirstRecord(int bucketId, Record record) { SoftReference ref = new SoftReference(record); mBuckets[bucketId] = ref; } /** * Returns the value of the specified key or null if * there is no value attached. * *

Warning: 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. */ public V get(Object key) { int bucketId = bucketId(key); for (Record 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. * *

Warning: 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 null, even if there is an existing * value for the specified key. */ public V put(K key, V value) { int bucketId = bucketId(key); Record firstRecord = getFirstRecord(bucketId); for (Record record = firstRecord; record != null; record = record.mNextRecord) { if (record.mKey.equals(key)) { ++record.mCount; // increment instead return null; // already there } } prune(); Record record = new Record(key,value,firstRecord); setFirstRecord(bucketId,record); ++mNumEntries; return null; } /** * 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() { synchronized (this) { if (mNumEntries < mMaxEntries) return; int count = 0; for (int i = 0; i < mBuckets.length; ++i) { Record record = getFirstRecord(i); Record prunedRecord = prune(record); setFirstRecord(i,prunedRecord); for (Record r = prunedRecord; r != null; r = r.mNextRecord) ++count; } mNumEntries = count; } } final Record prune(Record inRecord) { Record record = inRecord; while (record != null && (record.mCount >>> 2) == 0) record = record.mNextRecord; if (record == null) return null; Record tail = prune(record.mNextRecord); return new Record(record.mKey, record.mValue, tail, record.mCount); } /** * 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. */ public Set> entrySet() { HashSet> entrySet = new HashSet>(); for (int i = 0; i < mBuckets.length; ++i) for (Record record = getFirstRecord(i); record != null; record = record.mNextRecord) entrySet.add(record); return entrySet; } static final class Record implements Map.Entry { final K mKey; final V mValue; final Record mNextRecord; int mCount; Record(K key, V value) { this(key,value,null); } Record(K key, V value, Record nextRecord) { this(key,value,nextRecord,1); } Record(K key, V value, Record nextRecord, int count) { mKey = key; mValue = value; mNextRecord = nextRecord; mCount = count; } public K getKey() { return mKey; } public V getValue() { return mValue; } public int hashCode() { return (mKey==null ? 0 : mKey.hashCode()) ^ (mValue==null ? 0 : mValue.hashCode()); } 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); } } }