package org.commons.jconfig.internal; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicLongArray; /** * Thread safe Least Recently Used cache. The implementation allows for a LRU * cache logic, but the default is to evict the elements in FIFO order. This * reduces the computation and improves reading performance form the cache. * * To implement the LRU logic it is necessary to call method touch(key) or call * method put(key, value) with the same [key, value] pair. * * maxSize is an approximation. * * * @param <Key> * @param <Value> */ public class ConcurrentLRUCache<Key, Value> { private static final int MISS = 0; private static final int HIT = 1; private static final int REMOVED_KEYS = 2; private static final int REUSED_KEYS = 3; private final AtomicInteger mMaxSize = new AtomicInteger(); private final ConcurrentHashMap<Key, Value> map; private final ConcurrentLinkedQueue<Key> queue; // Stats private final AtomicLongArray stats = new AtomicLongArray(4); /** * @param maxSize */ public ConcurrentLRUCache(final int maxSize) { setMaxSize(maxSize); map = new ConcurrentHashMap<Key, Value>(maxSize); queue = new ConcurrentLinkedQueue<Key>(); } /** * This constructor can be used to clone, reduce or increase the cache size. * * @param maxSize * @param cache */ public ConcurrentLRUCache(final int maxSize, final ConcurrentLRUCache<Key, Value> cache) { this(maxSize); for (Key key : cache.queue) { Value value = cache.get(key); if (null != value) { this.put(key, value); } } } /** * set the maxSize queue max size, to a bigger value * * @param maxSize */ public void setMaxSize(int maxSize) { if (maxSize <= 1) { throw new IllegalArgumentException("Value " + maxSize + " has to be greater than zero."); } this.mMaxSize.set(maxSize); } /** * Touch a element in the cache, marks the element as recently used. On a * single thread maxSize will not be exceeded. * * @param key * - null key is not supported */ public void touch(final Key key) { if (map.containsKey(key)) { synchronized (this) { // update queue age for passed key queue.remove(key); queue.add(key); stats.incrementAndGet(REUSED_KEYS); } } } /** * Insert a element in the cache. The tries to use the maxSize as an * approximation, the cache can grow a bit more than maxSize. The higher * concurrency on puts, the higher the probability of maxSize will be * exceeded. On a single thread maxSize will not be exceeded. * * @param key * - null key is not supported * @param val * value Object */ public void put(final Key key, final Value val) { if (map.containsKey(key)) { synchronized (this) { // update queue age for passed key queue.remove(key); queue.add(key); map.put(key, val); stats.incrementAndGet(REUSED_KEYS); } return; } // remove old keys to match the current Max Size. int maxSize = mMaxSize.get(); while (queue.size() >= maxSize) { synchronized (this) { Key oldestKey = queue.poll(); if (null != oldestKey) { map.remove(oldestKey); stats.incrementAndGet(REMOVED_KEYS); } } } synchronized (this) { queue.add(key); map.put(key, val); } } /** * Retrieve a value from cache * * @param key * - null key is not supported * @return key value */ public Value get(final Key key) { Value v = map.get(key); if (v == null) { stats.incrementAndGet(MISS); } else { stats.incrementAndGet(HIT); } return v; } /** * Retrieve current size * * @return size */ public int size() { synchronized (this) { return queue.size(); } } /** * clear cache * * @return size */ public void clear() { synchronized (this) { queue.clear(); map.clear(); // reset stats for (int i = 0; i < stats.length(); i++) { stats.set(i, 0); } } } /** * Returns a string with the LRU stats. This method is for testing only, * should be used as part of the api. * * @return - a string with stats on the cache */ public String getStats() { return "SIZE:" + queue.size() + ", MAX_SIZE:" + mMaxSize + ", HIT:" + stats.get(HIT) + ", MISS:" + stats.get(MISS) + ", REUSED_KEYS:" + stats.get(REUSED_KEYS) + ", REMOVED_KEYS:" + stats.get(REMOVED_KEYS); } /* * (non-Javadoc) * * @see java.lang.Object#toString() */ @Override public String toString() { return getStats(); } }