Java tutorial
/** * SmartCache.java Created Mar 26, 2009 by Andrew Butler, PSL */ //package prisms.util; import java.util.Collection; import java.util.Map; import java.util.Set; import java.util.concurrent.locks.Lock; /** * A cache that purges values according to their frequency and recency of use and other qualitative * values. * * @param <K> The type of key to use for the cache * @param <V> The type of value to cache * @see #shouldRemove(CacheValue, float, float, float) */ public class DemandCache<K, V> implements java.util.Map<K, V> { /** * Allows this cache to assess the quality of a cache item to determine its value to the * accessor * * @param <T> The type of value to be qualitized */ public static interface Qualitizer<T> { /** * @param value The value to assess * @return The quality of the value. Units are undefined but must be consistent. 0 to 1 is * recommended but not required. */ float quality(T value); /** * @param value The value to assess * @return The amount of space the value takes up. Units are undefined but must be * consistent. Bytes is recommended but not required. */ float size(T value); } /** * Access to an object in the same amount as a get operation * * @see #access(Object, int) */ public static final int ACCESS_GET = 1; /** * Access to an object in the same amoutn as a set operation * * @see #access(Object, int) */ public static final int ACCESS_SET = 2; class CacheValue { V value; float demand; } private final Qualitizer<V> theQualitizer; private final java.util.HashMap<K, CacheValue> theCache; private final java.util.concurrent.locks.ReentrantReadWriteLock theLock; private float thePreferredSize; private long theHalfLife; private float theReference; private long theCheckedTime; private long thePurgeTime; /** * Creates a DemandCache with default values */ public DemandCache() { this(null, -1, 5L * 60 * 1000); } /** * Creates a DemandCache * * @param qualitizer The qualitizer to qualitize the values by * @param prefSize The preferred size of this cache, or <=0 if this cache should have no * preferred size * @param halfLife The half life of this cache */ public DemandCache(Qualitizer<V> qualitizer, float prefSize, long halfLife) { if (qualitizer == null) qualitizer = new Qualitizer<V>() { public float quality(V value) { return 1; } public float size(V value) { return 1; } }; theQualitizer = qualitizer; theCache = new java.util.HashMap<K, CacheValue>(); theLock = new java.util.concurrent.locks.ReentrantReadWriteLock(); thePreferredSize = prefSize; theHalfLife = halfLife; theReference = 1; theCheckedTime = System.currentTimeMillis(); thePurgeTime = theCheckedTime; } /** * @return The preferred size of this cache, or <=0 if this cache has no preferred size */ public float getPreferredSize() { return thePreferredSize; } /** * @param prefSize The preferred size for this cache or <=0 if this cache should have no * preferred size */ public void setPreferredSize(float prefSize) { thePreferredSize = prefSize; } /** * @return The approximate half life of items in this cache */ public long getHalfLife() { return theHalfLife; } /** * @param halfLife The half life for items in this cache */ public void setHalfLife(long halfLife) { theHalfLife = halfLife; } /** * @see java.util.Map#get(java.lang.Object) */ public V get(Object key) { Lock lock = theLock.readLock(); lock.lock(); try { CacheValue value = theCache.get(key); if (value == null) return null; _access(value, ACCESS_GET); return value.value; } finally { lock.unlock(); } } /** * @see java.util.Map#put(java.lang.Object, java.lang.Object) */ public V put(K key, V value) { Lock lock = theLock.writeLock(); lock.lock(); try { if (thePurgeTime < System.currentTimeMillis() - 60 * 1000) purge(); CacheValue newValue = new CacheValue(); newValue.value = value; _access(newValue, ACCESS_SET); CacheValue oldValue = theCache.put(key, newValue); return oldValue == null ? null : oldValue.value; } finally { lock.unlock(); } } /** * @see java.util.Map#remove(java.lang.Object) */ public V remove(Object key) { Lock lock = theLock.writeLock(); lock.lock(); try { if (thePurgeTime < System.currentTimeMillis() - 60 * 1000) purge(); CacheValue oldValue = theCache.remove(key); return oldValue == null ? null : oldValue.value; } finally { lock.unlock(); } } /** * @see java.util.Map#clear() */ public void clear() { Lock lock = theLock.writeLock(); lock.lock(); try { theCache.clear(); } finally { lock.unlock(); } } /** * @see java.util.Map#size() */ public int size() { return theCache.size(); } /** * @see java.util.Map#isEmpty() */ public boolean isEmpty() { return theCache.isEmpty(); } /** * @see java.util.Map#containsKey(java.lang.Object) */ public boolean containsKey(Object key) { Lock lock = theLock.readLock(); lock.lock(); try { return theCache.containsKey(key); } finally { lock.unlock(); } } /** * @see java.util.Map#containsValue(java.lang.Object) */ public boolean containsValue(Object value) { throw new UnsupportedOperationException( "The containsValue method is not supported by " + getClass().getName()); } /** * @see java.util.Map#putAll(java.util.Map) */ public void putAll(Map<? extends K, ? extends V> m) { Lock lock = theLock.writeLock(); lock.lock(); try { if (thePurgeTime < System.currentTimeMillis() - 60 * 1000) purge(); for (java.util.Map.Entry<? extends K, ? extends V> entry : m.entrySet()) { CacheValue cv = new CacheValue(); cv.value = entry.getValue(); _access(cv, ACCESS_SET); theCache.put(entry.getKey(), cv); } } finally { lock.unlock(); } } /** * @see java.util.Map#keySet() */ public Set<K> keySet() { Lock lock = theLock.writeLock(); lock.lock(); final Object[] keys; try { if (thePurgeTime < System.currentTimeMillis() - 60 * 1000) purge(); keys = theCache.keySet().toArray(); } finally { lock.unlock(); } return new java.util.AbstractSet<K>() { @Override public java.util.Iterator<K> iterator() { return new java.util.Iterator<K>() { private int index = 0; public boolean hasNext() { return index < keys.length; } public K next() { index++; return (K) keys[index - 1]; } public void remove() { DemandCache.this.remove(keys[index - 1]); } }; } @Override public int size() { return keys.length; } }; } /** * @see java.util.Map#entrySet() */ public Set<java.util.Map.Entry<K, V>> entrySet() { Lock lock = theLock.writeLock(); lock.lock(); final Map.Entry<K, CacheValue>[] entries; try { if (thePurgeTime < System.currentTimeMillis() - 60 * 1000) purge(); entries = theCache.entrySet().toArray(new Map.Entry[0]); } finally { lock.unlock(); } return new java.util.AbstractSet<Map.Entry<K, V>>() { @Override public java.util.Iterator<Map.Entry<K, V>> iterator() { return new java.util.Iterator<Map.Entry<K, V>>() { int index = 0; public boolean hasNext() { return index < entries.length; } public Map.Entry<K, V> next() { Map.Entry<K, V> ret = new Map.Entry<K, V>() { private final int entryIndex = index; public K getKey() { return entries[entryIndex].getKey(); } public V getValue() { return entries[entryIndex].getValue().value; } public V setValue(V value) { V retValue = entries[entryIndex].getValue().value; entries[entryIndex].getValue().value = value; return retValue; } public String toString() { return entries[entryIndex].getKey().toString() + "=" + entries[entryIndex].getValue().value; } }; index++; return ret; } public void remove() { DemandCache.this.remove(entries[index - 1].getKey()); } }; } @Override public int size() { return entries.length; } }; } /** * @see java.util.Map#values() */ public Collection<V> values() { throw new UnsupportedOperationException("The values method is not supported by " + getClass().getName()); } /** * @return The total size of the data in this cache, according to * {@link Qualitizer#size(Object)}. This value will return the same as {@link #size()} * if the qualitizer was not set in the constructor. */ public float getTotalSize() { Lock lock = theLock.readLock(); lock.lock(); try { float ret = 0; for (CacheValue value : theCache.values()) ret += theQualitizer.size(value.value); return ret; } finally { lock.unlock(); } } /** * @return The average quality of the values in this cache, according to * {@link Qualitizer#quality(Object)}. This value will return 1 if the qualitizer was * not set in the constructor. */ public float getOverallQuality() { Lock lock = theLock.readLock(); lock.lock(); try { float ret = 0; for (CacheValue value : theCache.values()) ret += theQualitizer.quality(value.value); return ret / theCache.size(); } finally { lock.unlock(); } } private void _access(CacheValue value, int weight) { if (weight <= 0) return; updateReference(); if (weight > 10) weight = 10; value.demand += theReference * weight; } /** * Performs an access operation on a cache item, causing it to live longer in the cache * * @param key The key of the item to access * @param weight The weight of the access--higher weight will result in a more persistent cache * item. 1-10 are supported. A guideline is that the cache item will survive longer by * {@link #getHalfLife()} <code>weight</code>. * @see #ACCESS_GET * @see #ACCESS_SET */ public void access(K key, int weight) { Lock lock = theLock.readLock(); lock.lock(); try { CacheValue value = theCache.get(key); if (value != null) _access(value, weight); } finally { lock.unlock(); } } /** * Purges the cache of values that are deemed of less use to the accessor. The behavior of this * method depends the behavior of {@link #shouldRemove(CacheValue, float, float, float)} */ public void purge() { Lock lock = theLock.writeLock(); lock.lock(); try { updateReference(); scaleReference(); int count = size(); float totalSize = 0; float totalQuality = 0; for (CacheValue value : theCache.values()) { totalSize += theQualitizer.size(value.value); totalQuality += theQualitizer.quality(value.value); } totalQuality /= count; java.util.Iterator<CacheValue> iter = theCache.values().iterator(); while (iter.hasNext()) { CacheValue next = iter.next(); if (shouldRemove(next, totalSize, totalQuality, count)) iter.remove(); } } finally { lock.unlock(); } thePurgeTime = System.currentTimeMillis(); } /** * Determines whether a cache value should be removed from the cache. The behavior of this * method depends on many variables: * <ul> * <li>How frequently and recently the value has been accessed</li> * <li>The quality of the value according to {@link Qualitizer#quality(Object)} compared to the * average quality of the cache</li> * <li>The size of the value according to {@link Qualitizer#size(Object)} compared to the * average size of the cache's values</li> * <li>The total size of the cache compared to its preferred size (assuming this is set to a * value greater than 0) * </ul> * * @param value The value to determine the quality of * @param totalSize The total size (determined by the Qualitizer) of this cache * @param overallQuality The overall quality of the cache * @param entryCount The number of entries in this cache * @return Whether the value should be removed from the cache */ protected boolean shouldRemove(CacheValue value, float totalSize, float overallQuality, float entryCount) { float quality = theQualitizer.quality(value.value); if (quality == 0) return true; // Remove if the value has no quality float size = theQualitizer.size(value.value); if (size == 0) return false; // Don't remove if the value takes up no space /* Take into account how frequently and recently the value was accessed */ float valueQuality = value.demand / theReference; /* Take into account the inherent quality in the value compareed to the average */ valueQuality *= quality / overallQuality; /* Take into account the value's size compared with the average size */ valueQuality /= size / (totalSize / entryCount); /* Take into account the overall size of this cache compared with the preferred size * (Whether it is too big or has room to spare) */ if (thePreferredSize > 0) valueQuality /= totalSize / thePreferredSize; return valueQuality < 0.5f; } /** * Updates {@link #theReference} to devalue all items in the cache with age. The read lock must * be obtained before calling this method. */ private void updateReference() { long time = System.currentTimeMillis(); if (time - theCheckedTime >= theHalfLife / 100) return; theReference *= Math.pow(2, (time - theCheckedTime) * 1.0 / theHalfLife); theCheckedTime = time; } /** * Scales all {@link CacheValue#demand} values to keep them and {@link #theReference} small. * This allows the cache to be kept for long periods of time. The write lock must be obtained * before calling this method. */ private void scaleReference() { if (theReference > 1e7) { for (CacheValue value : theCache.values()) value.demand /= theReference; theReference = 1; } } }