/* * This file is part of muCommander, http://www.mucommander.com * Copyright (C) 2002-2016 Maxence Bernard * * muCommander 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; either version 3 of the License, or * (at your option) any later version. * * muCommander 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, see <http://www.gnu.org/licenses/>. */ package com.mucommander.cache; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Map; /** * LRU cache implementation which uses <code>LinkedHashMap</code> which provides fast retrieval and insertion * operations. * * <p>The only area this implemention is slow at, is checking for and removing expired elements which * requires traversing all values and <code>LinkedHashMap</code> is slow at that. * To minimize the impact this could have on performance, this operation is not systematically performed * for each call to <code>get()</code> and <code>set()</code> methods, unless the cache is full. * That means this implementation is not as aggressive as it could be in terms of releasing expired items' memory * but favors performance instead, which is what caches are for.</p> * * @author Maxence Bernard */ public class FastLRUCache<K, V> extends LRUCache<K,V> { /** Cache key->value/expirationDate map */ private LinkedHashMap<K, Object[]> cacheMap; /** Timestamp of last expired items purge */ private long lastExpiredPurge; /** Number of millisecond to wait between 2 expired items purges, if cache is not full */ private final static int PURGE_EXPIRED_DELAY = 1000; public FastLRUCache(int capacity) { super(capacity); this.cacheMap = new LinkedHashMap<K, Object[]>(16, 0.75f, true) { // Override this method to automatically remove eldest entry before insertion when cache is full @Override protected final boolean removeEldestEntry(Map.Entry<K, Object[]> eldest) { return cacheMap.size() > FastLRUCache.this.capacity; } }; } /** * Returns a String representation of this cache. */ public String toString() { String s = super.toString()+" size="+cacheMap.size()+" capacity="+capacity+" eldestExpirationDate="+eldestExpirationDate+"\n"; Object key; Object value[]; int i=0; for(Map.Entry<K, Object[]> mapEntry : cacheMap.entrySet()) { key = mapEntry.getKey(); value = mapEntry.getValue(); s += (i++)+"- key="+key+" value="+value[0]+" expirationDate="+value[1]+"\n"; } if(UPDATE_CACHE_COUNTERS) s += "nbCacheHits="+nbHits+" nbCacheMisses="+nbMisses+"\n"; return s; } /** * Looks for cached items that have a passed expiration date and purge them. */ private void purgeExpiredItems() { long now = System.currentTimeMillis(); // No need to go any further if eldestExpirationDate is in the future. // Also, since iterating on the values is an expensive operation (especially for LinkedHashMap), // wait PURGE_EXPIRED_DELAY between two purges, unless cache is full if(this.eldestExpirationDate>now || (cacheMap.size()<capacity && now-lastExpiredPurge<PURGE_EXPIRED_DELAY)) return; // Look for expired items and remove them and recalculate eldestExpirationDate for next time this.eldestExpirationDate = Long.MAX_VALUE; Long expirationDateL; long expirationDate; Iterator<Object[]> iterator = cacheMap.values().iterator(); // Iterate on all cached values while(iterator.hasNext()) { expirationDateL = (Long)iterator.next()[1]; // No expiration date for this value if(expirationDateL==null) continue; expirationDate = expirationDateL; // Test if the item has an expiration date and check if has passed if(expirationDate<now) { // Remove expired item iterator.remove(); } else if(expirationDate<this.eldestExpirationDate) { // update eldestExpirationDate this.eldestExpirationDate = expirationDate; } } // Set last purge timestamp to now lastExpiredPurge = now; } ///////////////////////////////////// // LRUCache methods implementation // ///////////////////////////////////// @Override public synchronized V get(K key) { // Look for expired items and purge them (if any) purgeExpiredItems(); // Look for a value corresponding to the specified key in the cache map Object[] value = cacheMap.get(key); if(value==null) { // No value matching key, better luck next time! if(UPDATE_CACHE_COUNTERS) nbMisses++; // Increase cache miss counter return null; } // Since expired items purge is not performed on every call to this method for // performance reason, we can end with an expired cached value so we need // to check this Long expirationDateL = (Long)value[1]; if(expirationDateL!=null && System.currentTimeMillis()> expirationDateL) { // Value has expired, let's remove it if(UPDATE_CACHE_COUNTERS) nbMisses++; // Increase cache miss counter cacheMap.remove(key); return null; } if(UPDATE_CACHE_COUNTERS) nbHits++; // Increase cache hit counter return (V)value[0]; } @Override public synchronized void add(K key, V value, long timeToLive) { // Look for expired items and purge them (if any) purgeExpiredItems(); Long expirationDateL; if(timeToLive==-1) { expirationDateL = null; } else { long expirationDate = System.currentTimeMillis()+timeToLive; // Update eledestExpirationDate if new element's expiration date is older if(expirationDate<this.eldestExpirationDate) { // update eldestExpirationDate this.eldestExpirationDate = expirationDate; } expirationDateL = expirationDate; } cacheMap.put(key, new Object[]{value, expirationDateL}); } @Override public synchronized int size() { return cacheMap.size(); } @Override public synchronized void clearAll() { cacheMap.clear(); eldestExpirationDate = Long.MAX_VALUE; } ////////////////// // Test methods // ////////////////// /** * Tests this LRUCache for corruption and throws a RuntimeException if something is wrong. */ @Override protected void testCorruption() throws RuntimeException { Object value[]; long expirationDate; Long expirationDateL; for(K key : cacheMap.keySet()) { value = cacheMap.get(key); if(value==null) throw new RuntimeException("cache corrupted: value could not be found for key="+key); expirationDateL = (Long)value[1]; if(expirationDateL==null) continue; expirationDate = expirationDateL; if(expirationDate<eldestExpirationDate) throw new RuntimeException("cache corrupted: expiration date for key="+key+" older than eldestExpirationDate"); } } }