/** Copyright (C) SYSTAP, LLC DBA Blazegraph 2006-2016. All rights reserved. Contact: SYSTAP, LLC DBA Blazegraph 2501 Calvert ST NW #106 Washington, DC 20008 licenses@blazegraph.com This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; version 2 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA */ /* * Created on Apr 19, 2006 */ package com.bigdata.cache; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.NoSuchElementException; import java.util.concurrent.CopyOnWriteArraySet; import org.apache.log4j.Logger; /** * Hard reference hash map with Least Recently Used ordering over entries. The * keys are object identifiers. The values are cache entries wrapping the * identified objects. * * @version $Id$ * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson * </a> * * @todo Consider removing synchronization for use in a single threaded context. * * @todo This can be replaced by a hard reference ring buffer that scans the * last N entries to minimize churn. See {@link HardReferenceQueue}. This * will change the delegation interfaces since the ring buffer does NOT * support random access by the identifier. */ public class LRUCache<K,T> implements ICachePolicy<K,T> { protected static final Logger log = Logger.getLogger(LRUCache.class); protected static final boolean INFO = log.isInfoEnabled(); /** * The maximum capacity of the cache. */ private final int capacity; /** * The hash map from keys to entries wrapping cached object references. */ private final Map<K,Entry<K,T>> map; /** * The entry which is first in the ordering (the * <em>least recently used</em>) and <code>null</code> iff the cache is * empty. */ private Entry<K,T> first = null; /** * The entry which is last in the ordering (the <em>most recently used</em>) * and <code>null</code> iff the cache is empty. */ private Entry<K,T> last = null; /** * The load factor for the internal hash table. */ private final float loadFactor; private long highTide = 0, ninserts = 0, ntests = 0, nsuccess = 0; public double getHitRatio() { return ((double) nsuccess / ntests); } public long getInsertCount() { return ninserts; } public long getTestCount() { return ntests; } public long getSuccessCount() { return nsuccess; } /** * The cache eviction listener. */ private ICacheListener<K,T> _listener = null; /** * Create an LRU cache with a default load factor of <code>0.75</code>. * * @param capacity * The capacity of the cache. */ public LRUCache( int capacity ) { this( capacity, 0.75f ); } /** * Create an LRU cache with the specific capacity and load factor. * * @param capacity * The capacity of the cache (must be positive). * @param loadFactor * The load factor for the internal hash table. */ public LRUCache( int capacity, float loadFactor ) { if( capacity <= 0 ) { throw new IllegalArgumentException(); } this.capacity = capacity; this.loadFactor = loadFactor; this.map = new HashMap<K,Entry<K,T>>( capacity, loadFactor ); } public void setListener(ICacheListener<K,T> listener) { _listener = listener; } public ICacheListener<K,T> getCacheListener() { return _listener; } /** * <p> * Visits objects in the cache in LRU ordering (the least recently used * object is visited first). * </p> * <p> * The returned iterator is NOT thread safe. It supports removal but does * NOT support concurrent modification of the cache state. Normally the * iterator is used during a commit and the framework guarantees that * concurrent * </p> */ public Iterator<T> iterator() { return new LRUIterator<K,T>( this, true ); } /** * <p> * Visits {@link ICacheEntry entries} in the cache in LRU ordering (the * least recently used object is visited first). * </p> * <p> * The returned iterator is NOT thread safe. It supports removal but does * NOT support concurrent modification of the cache state. Normally the * iterator is used during a commit and the framework guarantees that * concurrent * </p> */ public Iterator<ICacheEntry<K,T>> entryIterator() { return new LRUIterator<K,T>( this, false ); } public void clear() { map.clear(); first = last = null; resetStatistics(); } public void resetStatistics() { highTide = ninserts = ntests = nsuccess = 0; } /** * The #of entries in the cache. */ synchronized public int size() { return map.size(); } /** * The capacity of the cache. */ public int capacity() { // this is final data and does not need to be synchronized. return capacity; } /** * Writes cache performance statistics. */ protected void finalize() throws Throwable { if (INFO) log.info(getStatistics()); } public String getStatistics() { return "LRUCache" + // ": capacity=" + capacity + // ", loadFactor=" + loadFactor + // ", highTide=" + highTide + // ", ninserts=" + ninserts + // ", ntests=" + ntests + // ", nsuccess=" + nsuccess + // ", hitRatio=" + ((double) nsuccess / ntests)// ; } /** * <p> * Add the object to the hash map under the key if it is not already there * and update the entry ordering (this can be used to touch an entry). * </p> * <p> * Cache evictions are only performed at or <i>over</i> capacity, but not * for reentrant invocations. If a cache eviction causes a nested * {@link #put(long, Object, boolean)} cache enters a temporary * <em>over capacity</em> condition. The nested eviction is effectively * deferred and a new cache entry is created for the incoming object rather * than recycling the LRU cache entry. This temporary over capacity state * exists until the primary eviction event has been handled, at which point * entries are purged from the cache until it has one free entry. That free * entry is then used to cache the incoming object which triggered the outer * eviction event. * </p> * <p> * This is not the only coherent manner in which nested eviction events * could be handled, but it is perhaps the simplest. This technique MUST NOT * be used with an open array hash table since the temporary over capacity * condition would not be supported. * </p> * * @param key * The object identifier. * * @param obj * The object. */ synchronized public void put(K key, T obj, boolean dirty ) { assert key != null; reentrantPutCounter++; try { Entry<K,T> entry = map.get(key); if (entry == null) { if (map.size() >= capacity && reentrantPutCounter == 1) { /* * Purge the the LRU position until the cache is just under * capacity. */ while (map.size() >= capacity) { // entry in the LRU position. entry = first; if (_listener != null) { // Notify listener of cache eviction. _listener.objectEvicted(entry); } // remove LRU entry from ordering. removeEntry(entry); // remove entry from hash map under that key. map.remove(entry.key); } /* * Recycle the last cache entry that we purged. */ // set key and object on LRU entry. entry.key = key; entry.obj = obj; entry.dirty = dirty; // add entry into the hash map. map.put(key, entry); // add entry into MRU position in ordering. addEntry(entry); } else { /* * Create a new entry and link into the MRU position. * * Note: This also handles the case of a reentrant * invocation, in which case the cache will be temporarily * over capacity. */ entry = new Entry<K,T>(key, obj, dirty); map.put(key, entry); addEntry(entry); int count = map.size(); if (count > highTide) { highTide = count; } } ninserts++; } else { if (entry.obj != obj) { throw new IllegalStateException( "can not change object under key"); } // Update entry ordering. touchEntry(entry); // Update dirty flag. entry.dirty = dirty; } } finally { reentrantPutCounter--; } } /** * Used to track and handle reentrant calls to put(). The value of the * counter is the number of times that put() occurs in the stack frame. * E.g., the counter value will be zero when put() is not in the stack * frame, a non-recursive invocation will show a counter value of 1; and a * counter value of 2 or more indicates a reentrant invocation is in * progress. * * @see #put(long, Object, boolean) */ private int reentrantPutCounter = 0; synchronized public T get( K key ) { assert key != null; ntests++; Entry<K,T> entry = map.get( key ); if( entry == null ) { return null; } touchEntry( entry ); nsuccess++; return entry.getObject(); } // synchronized public boolean isDirty( long key ) { // // ntests++; // // Entry entry = (Entry) map.get( new Long( key ) ); // // if( entry == null ) { // // return false; // // } // // return entry.isDirty(); // // } synchronized public T remove( K key ) { assert key != null; Entry<K,T> entry = map.remove( key ); if( entry == null ) return null; removeEntry( entry ); return entry.getObject(); } /** * Registers a listener for removeEntry events. This is used by the * {@link LRUIterator} to handle concurrent modifications of the cache * ordering during traversal. */ synchronized protected void addCacheOrderChangeListener( ICacheOrderChangeListener<K,T> l ) { if( _cacheOrderChangeListeners == null ) { _cacheOrderChangeListeners = new CopyOnWriteArraySet<ICacheOrderChangeListener<K,T>>(); } _cacheOrderChangeListeners.add(l); } /** * Unregister the listener. This is safe to invoke when the listener is not * registered. * * @param l * The listener. */ synchronized protected void removeCacheOrderChangeListener( ICacheOrderChangeListener<K,T> l ) { if (_cacheOrderChangeListeners == null) return; _cacheOrderChangeListeners.remove(l); if( _cacheOrderChangeListeners.size() == 0 ) { _cacheOrderChangeListeners = null; } } private void fireCacheOrderChangeEvent( boolean removed, ICacheEntry<K,T> entry ) { if( _cacheOrderChangeListeners.size() == 0 ) return; // ICacheOrderChangeListener<T>[] listeners = _cacheOrderChangeListeners // .toArray(new ICacheOrderChangeListener[] {}); // for( int i=0; i<listeners.length; i++ ) { // ICacheOrderChangeListener<T> l = listeners[ i ]; Iterator<ICacheOrderChangeListener<K,T>> itr = _cacheOrderChangeListeners.iterator(); while( itr.hasNext() ) { ICacheOrderChangeListener<K,T> l = itr.next(); if( removed ) { l.willRemove( entry ); } else { throw new UnsupportedOperationException(); // feature is not implemented. } } } /** * Lazily allocated and eagerly freed. */ private CopyOnWriteArraySet<ICacheOrderChangeListener<K,T>> _cacheOrderChangeListeners = null; protected static interface ICacheOrderChangeListener<K,T> { public void willRemove( ICacheEntry<K,T> entry ); // public void didAdd(ICacheEntryEntry); } /** * Add an Entry to the tail of the linked list (the MRU position). */ private void addEntry(Entry<K,T> entry) { if (first == null) { first = entry; last = entry; } else { last.next = entry; entry.prior = last; last = entry; } } /** * Remove an {@link Entry} from linked list that maintains the LRU ordering. * The {@link Entry} is modified but not deleted. The key and value fields * on the {@link Entry} are not modified. The {@link #first} and {@link #last} * fields are updated as necessary. This method is used when the LRU entry is * being evicted and the {@link Entry} will be recycled into the MRU position * in the ordering. You must also remove the entry under that key from the hash * map. */ private void removeEntry(Entry<K,T> entry) { if( _cacheOrderChangeListeners != null ) { fireCacheOrderChangeEvent(true, entry); } Entry<K,T> prior = entry.prior; Entry<K,T> next = entry.next; if (entry == first) { first = next; } if (last == entry) { last = prior; } if (prior != null) { prior.next = next; } if (next != null) { next.prior = prior; } entry.prior = null; entry.next = null; } /** * Move the entry to the end of the linked list (the MRU position). */ private void touchEntry(Entry<K,T> entry) { if (last == entry) { return; } removeEntry(entry); addEntry(entry); } /** * Wraps an object with metadata to maintain the LRU ordering. * * @version $Id$ * @author thompsonbry */ final static class Entry<K,T> implements ICacheEntry<K,T> { private K key; private T obj; private boolean dirty; private Entry<K,T> prior; private Entry<K,T> next; Entry( K key, T obj, boolean dirty ) { this.key = key; this.obj = obj; this.dirty = dirty; } public boolean isDirty() { return dirty; } public void setDirty(boolean dirty) { this.dirty = dirty; } public K getKey() { return key; } public T getObject() { return obj; } /** * The prior entry (less recently used). * * @return The next entry or <code>null</code>. */ public Entry<K,T> getPrior() { return prior; } /** * The next entry (more recently used). * * @return The next entry or <code>null</code>. */ public Entry<K,T> getNext() { return next; } /** * Human readable representation used for debugging in test cases. */ public String toString() { return "Entry{key=" + key + ",obj=" + obj + ",dirty=" + dirty + ",prior=" + (prior == null ? "N/A" : "" + prior.key) + ",next=" + (next == null ? "N/A" : "" + next.key)+ "}"; } } /** * <p> * Visits entries in the {@link LRUCache} in their natural ordering from LRU * to MRU. The iterator will optionally resolve the entries to the * application objects associated with each entry. The iterator supports * removal but is NOT thread-safe and does not support concurrent * modification of the {@link LRUCache}. * </p> * <p> * This class provide fast visitation from LRU to MRU by chasing references. * In order to support concurrent modification of the cache order during * traversal, an instance uses a protocol by it is informed of changes in * the cache order. There are two basic operations that effect the cache * order: addEntry and removeEntry. addEntry always inserts the entry in the * MRU position, so it can not effect the visitation order. However, * removeEntry could unlink the entry that the iterator will use to reach * the next entry (via its next reference). The iterator must therefore * receive notice when a cache entry is about to be removed. If the entry is * the same entry that the iterator would visit next in the LRU to MRU * ordering, then the iterator advances its state to the next entry that it * would visit. When removeEntry then removes the entry from the cache * ordering, the iterator correctly visits the next cache entry in the new * ordering. * </p> * * @version $Id$ * @author <a href="mailto:thompsonbry@users.sourceforge.net">Bryan Thompson * </a> * * @todo In order to support generics, we might need to break this down into * two implementations so that we can have a strongly typed iterator * for both T and ICacheEntry<T>. */ static class LRUIterator<K,T> implements Iterator, ICacheOrderChangeListener<K,T> { private final LRUCache<K,T> cache; private final boolean resolveObjects; private Entry<K,T> next; private Entry<K,T> lastVisited = null; /** * An iterator that traverses the {@link LRUCache} in the its natural * ordering. * * @param cache * The {@link LRUCache}. * * @param resolveObjects * When true, the iterator will visit the application objects * in the cache. When false it will visit the {@link ICache} * entries themselves. */ LRUIterator( LRUCache<K,T> cache, boolean resolveObjects ) { this.cache = cache; this.next = cache.first; this.resolveObjects = resolveObjects; cache.addCacheOrderChangeListener(this); } public boolean hasNext() { return next != null; } public Object next() { if( next == null ) { removeListener(); throw new NoSuchElementException(); } /* Optionally resolve the application object vs the entry for that * object. */ Object ret = (resolveObjects ? next.obj : next); /* * Advance the internal state of the iterator. */ lastVisited = next; next = next.next; if( next == null ) { removeListener(); } /* * Return either the cache entry or the resolve application object. */ return ret; } /** * Removes the last visited entry from the cache. * * @exception IllegalStateException * if no entry has been visited yet. */ public void remove() { if( lastVisited == null ) { throw new IllegalStateException(); } cache.map.remove( lastVisited.key ); cache.removeEntry( lastVisited ); } public void willRemove(ICacheEntry entry) { if( entry == next ) { next = next.next; } } /** * Unregister the iterator as a cache order change listener. This is * safe to invoke multiple times. */ private void removeListener() { cache.removeCacheOrderChangeListener(this); } } }