A cache that purges values according to their frequency and recency of use and other qualitative values.
/**
* 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;
}
}
}
Related examples in the same category