320

Do any of you know of a Java Map or similar standard data store that automatically purges entries after a given timeout? This means aging, where the old expired entries “age-out” automatically.

I know of ways to implement the functionality myself and have done it several times in the past, so I'm not asking for advice in that respect, but for pointers to a good reference implementation.

WeakReference based solutions like WeakHashMap are not an option, because my keys are likely to be non-interned strings and I want a configurable timeout that's not dependent on the garbage collector.

Ehcache is also an option I wouldn't like to rely on because it needs external configuration files. I am looking for a code-only solution.

Elikill58
  • 4,050
  • 24
  • 23
  • 45
Sean Patrick Floyd
  • 292,901
  • 67
  • 465
  • 588
  • This question is beeing discuted on [meta](https://meta.stackoverflow.com/q/421689/10952503) about the close reason. – Elikill58 Nov 27 '22 at 09:33

8 Answers8

372

Yes. Google Collections, or Guava as it is named now has something called MapMaker which can do exactly that.

ConcurrentMap<Key, Graph> graphs = new MapMaker()
   .concurrencyLevel(4)
   .softKeys()
   .weakValues()
   .maximumSize(10000)
   .expiration(10, TimeUnit.MINUTES)
   .makeComputingMap(
       new Function<Key, Graph>() {
         public Graph apply(Key key) {
           return createExpensiveGraph(key);
         }
       });

Update:

As of guava 10.0 (released September 28, 2011) many of these MapMaker methods have been deprecated in favour of the new CacheBuilder:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
    .maximumSize(10000)
    .expireAfterWrite(10, TimeUnit.MINUTES)
    .build(
        new CacheLoader<Key, Graph>() {
          public Graph load(Key key) throws AnyException {
            return createExpensiveGraph(key);
          }
        });
Community
  • 1
  • 1
Shervin Asgari
  • 23,901
  • 30
  • 103
  • 143
  • I know you have just copied the example from the javadoc, but as I ran into the same requirement, I just want to make sure the map properties are correct: Wouldn't it be better to use `expireAfterAccess` (instead of `expiration`) and `softValues` (instead of `weakValues`)? – Kariem Apr 13 '11 at 16:08
  • 13
    As from v10, you should be using CacheBuilder instead (http://guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/common/cache/CacheBuilder.html) since expiration etc have been deprecated in MapMaker – wwadge Sep 14 '11 at 11:00
  • 69
    *Warning*! Using `weakKeys()` imply that keys are compared using the == semantics, not `equals()`. I lost 30 minutes figuring out why my String-keyed cache was not working :) – Laurent Grégoire Nov 21 '13 at 10:45
  • 3
    How would you implement createExpensiveGraph() with a simple map-based approach that also should support .put() ? – neu242 May 23 '14 at 11:35
  • 1
    how to implement createExpensiveGraph(key)? – JaskeyLam Jul 17 '14 at 14:51
  • @Jaskey Its just an example of a method that adds the content in the cache based on the key – Shervin Asgari Jul 18 '14 at 07:28
  • 1
    Sorry, I still not understand,i guess the load(key key) is the map.get(key) method. Now, I have a map, and i don't want the key-value existing after 15 minutes of last access, would you please implement it in a small example? Also, can I use Cache just like a map? – JaskeyLam Jul 22 '14 at 06:43
  • @Jaskey the return value from the load method is what is put in the Cache. So in this example the createExpensiveGraph(key) method could just as well return new Graph() and it would put the new Graph() in the map with the key as given – Shervin Asgari Aug 06 '14 at 13:24
  • 4
    Folks, thing that @Laurent mentioned about `weakKeys()` is important. `weakKeys()` is not required 90% of the time. – Manu Manjunath Nov 24 '15 at 05:03
  • 3
    @ShervinAsgari for the sake of beginners (myself included), could you switch your updated guava example to one that uses Cache instead of LoadingCache? It would match the question better (since LoadingCache has features that exceed a map with expiring entries and is far more complicated to create) see https://github.com/google/guava/wiki/CachesExplained#from-a-callable – Jeutnarg Jan 20 '17 at 22:02
  • Is there a `graphs.remove("key")` like a map? How do I remove an entry before expiry time? – Aamir Rizwan Dec 31 '17 at 11:18
  • Never mind. Got it `graphs.invalidate("key")`. – Aamir Rizwan Jan 01 '18 at 11:38
  • @Jeutnarg I had the same question as you. As of Guava 27.0.1, if you call `CacheBuilder.build()` with no CacheLoader argument, then the returned type is a Cache, not a CacheLoader. I'm struggling to format this, and it's Kotlin, not Java, but I hope it helps someone. ```val resultCache: Cache = CacheBuilder.newBuilder().expireAfterWrite(500, TimeUnit.MILLISECONDS).build() // elsewhere, when you have the entry you want resultCache.put(myString, myValueObject)``` – Ari Lacenski Feb 24 '19 at 20:27
  • @ShervinAsgari great answer. Do you know if it is possible to set expiration time per key on demand? For instance, I have a `users` cache, so I want regular users to expire in 30min but admin to expire in 10min. Is this doable with Guava or should I have 2 separated caches? – Federico Piazza Nov 20 '19 at 15:26
45

This is a sample implementation that i did for the same requirement and concurrency works well. Might be useful for someone.

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 
 * @author Vivekananthan M
 *
 * @param <K>
 * @param <V>
 */
public class WeakConcurrentHashMap<K, V> extends ConcurrentHashMap<K, V> {

    private static final long serialVersionUID = 1L;

    private Map<K, Long> timeMap = new ConcurrentHashMap<K, Long>();
    private long expiryInMillis = 1000;
    private static final SimpleDateFormat sdf = new SimpleDateFormat("hh:mm:ss:SSS");

    public WeakConcurrentHashMap() {
        initialize();
    }

    public WeakConcurrentHashMap(long expiryInMillis) {
        this.expiryInMillis = expiryInMillis;
        initialize();
    }

    void initialize() {
        new CleanerThread().start();
    }

    @Override
    public V put(K key, V value) {
        Date date = new Date();
        timeMap.put(key, date.getTime());
        System.out.println("Inserting : " + sdf.format(date) + " : " + key + " : " + value);
        V returnVal = super.put(key, value);
        return returnVal;
    }

    @Override
    public void putAll(Map<? extends K, ? extends V> m) {
        for (K key : m.keySet()) {
            put(key, m.get(key));
        }
    }

    @Override
    public V putIfAbsent(K key, V value) {
        if (!containsKey(key))
            return put(key, value);
        else
            return get(key);
    }

    class CleanerThread extends Thread {
        @Override
        public void run() {
            System.out.println("Initiating Cleaner Thread..");
            while (true) {
                cleanMap();
                try {
                    Thread.sleep(expiryInMillis / 2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void cleanMap() {
            long currentTime = new Date().getTime();
            for (K key : timeMap.keySet()) {
                if (currentTime > (timeMap.get(key) + expiryInMillis)) {
                    V value = remove(key);
                    timeMap.remove(key);
                    System.out.println("Removing : " + sdf.format(new Date()) + " : " + key + " : " + value);
                }
            }
        }
    }
}


Git Repo Link (With Listener Implementation)

https://github.com/vivekjustthink/WeakConcurrentHashMap

Cheers!!

Vivek
  • 3,523
  • 2
  • 25
  • 41
  • Why do you do execute `cleanMap()` half the time expecified? – EliuX Jan 18 '17 at 01:29
  • Bcoz it makes sure the keys are expired (removed) and avoids thread from extreme looping. – Vivek Jan 18 '17 at 07:21
  • @Vivek but with this implementation there can be at max (expiryInMillis / 2) number of entries which are already expired but still present in cache. As thread deletes entries after expiryInMillis / 2 period – rishi007bansod Aug 17 '18 at 20:20
  • 1
    Can it be like, in get u check if the value is old than return null. Instead of cleanup thread. as in ... https://stackoverflow.com/a/63189871/3509609 – Vikash Jul 31 '20 at 10:20
  • @rishi007bansod True, but that's compromisable for faster gets. If your requirement demands 100% accurate expiry then you need to check and clean value on every get – Vivek Feb 09 '21 at 15:40
  • @Vikash as mentioned in the previous comment cleanup thread is to improve performance of get, and values don't last in memory for longer than needed. If implemented in get and if the values aren't accessed then it lasts in memory forever. – Vivek Feb 09 '21 at 15:45
  • @Vivek with a weakHashMap, the data gets removed 1. If the cleanMap detects an item to have lived beyond it's eviction time. 2. If GC detects items that can be removed. So, depending on which happens first, we do not guarantee the presence of the data. This is not what the OP asked for. – Nishit May 07 '22 at 18:18
  • SimpleDateFormat is not thread safe. – mancini0 Aug 09 '23 at 18:03
33

Apache Commons has decorator for Map to expire entries: PassiveExpiringMap It's more simple than caches from Guava.

P.S. be careful, it's not synchronized.

Guram Savinov
  • 594
  • 5
  • 10
  • 6
    It is simple, but it checks the expiration time only after you access an entry. – ahmdx Feb 15 '19 at 16:36
  • 1
    As per the [Javadoc](https://commons.apache.org/proper/commons-collections/apidocs/org/apache/commons/collections4/map/PassiveExpiringMap.html): _When invoking methods that involve accessing the entire map contents (i.e containsKey(Object), entrySet(), etc.) this decorator removes all expired entries prior to actually completing the invocation._ – dutoitns Feb 13 '20 at 10:19
  • If you want to see what the latest version of this library (Apache commons commons-collections4) is [here is a link to the relevant library on mvnrepository](https://mvnrepository.com/artifact/org.apache.commons/commons-collections4) – dutoitns Feb 13 '20 at 10:25
  • 2
    If using PassiveExpiringMap along with Collections.synchronizedMap, notice that access to the map collections (values, keySet, entrySet) will not trigger an eviction of the expired entries. Reason being that values, keySet and entrySet are "cached" in the SynchronizedMap. – seb foucault Jan 26 '22 at 08:42
  • implementation hint: it has a **Lazy/Passive Eviction Process**, so it removeAllExpired before performing methods, so if you don't call any methods the expired items will not be removed until you call any method on the map – Ahmed Nabil May 25 '23 at 20:22
22

You can try out my implementation of a self-expiring hash map. This implementation does not make use of threads to remove expired entries, instead it uses DelayQueue that is cleaned up at every operation automatically.

pcan
  • 893
  • 11
  • 24
  • I like Guava's version better, but +1 for adding completeness to the picture – Sean Patrick Floyd Jun 08 '15 at 07:20
  • @piero86 I'd say the call to delayQueue.poll() in method expireKey(ExpiringKey delayedKey) is wrong. You may loose an arbitrary ExpiringKey which can't later be utilized in cleanup() - a leak. – Stefan Zobel Jun 13 '16 at 14:08
  • 1
    Another problem: you can't put the same key twice with different lifetimes. After a) put(1, 1, shortLived), then b) put(1, 2, longLived) the Map entry for key 1 will be gone after shortLived ms no matter how long longLived is. – Stefan Zobel Jun 13 '16 at 16:22
  • Thank you for your insight. Could you report these issues as comments in the gist, please? – pcan Jun 13 '16 at 19:12
  • Fixed according to your suggestions. Thanks. – pcan Jul 13 '16 at 19:24
  • @SeanPatrickFloyd it's completely different. This implementation has TTL for each item, Guava uzes same TTL for all items. On;y problem with this code is that it's not thread safe, but piero86 really thanks a lot, you saved my day. – Bogdan Mart Nov 05 '16 at 13:45
  • Note that Guava is also performing clean up during write and read operations. This is not to say Guava is better, only that it works in the same way (in fact I would lean toward simplicity while Guava is massive). – NateS Mar 28 '20 at 00:10
  • @pcan: Do we need WeakHashMap for expiringKeys? An Entry in a WeakHashMap is automatically GC when its key is no longer in use. In your implementation, keys are removed manually using remove() method which defeats the purpose of WeakHashMap. – Rakesh Chauhan Nov 07 '21 at 18:06
4

Sounds like ehcache is overkill for what you want, however note that it does not need external configuration files.

It is generally a good idea to move configuration into a declarative configuration files ( so you don't need to recompile when a new installation requires a different expiry time ), but it is not at all required, you can still configure it programmatically. http://www.ehcache.org/documentation/user-guide/configuration

dan carter
  • 4,158
  • 1
  • 33
  • 34
3

If anybody needs a simple thing, following is a simple key-expiring set. It might be converted to a map easily.

public class CacheSet<K> {
    public static final int TIME_OUT = 86400 * 1000;

    LinkedHashMap<K, Hit> linkedHashMap = new LinkedHashMap<K, Hit>() {
        @Override
        protected boolean removeEldestEntry(Map.Entry<K, Hit> eldest) {
            final long time = System.currentTimeMillis();
            if( time - eldest.getValue().time > TIME_OUT) {
                Iterator<Hit> i = values().iterator();

                i.next();
                do {
                    i.remove();
                } while( i.hasNext() && time - i.next().time > TIME_OUT );
            }
            return false;
        }
    };


    public boolean putIfNotExists(K key) {
        Hit value = linkedHashMap.get(key);
        if( value != null ) {
            return false;
        }

        linkedHashMap.put(key, new Hit());
        return true;
    }

    private static class Hit {
        final long time;


        Hit() {
            this.time = System.currentTimeMillis();
        }
    }
}
palindrom
  • 18,033
  • 1
  • 21
  • 37
  • 2
    This is fine for a single-thread situation, but it would break miserably in a concurrent situation. – Sean Patrick Floyd Jul 07 '15 at 15:08
  • @SeanPatrickFloyd you mean like LinkedHashMap's itself?! "it must be synchronized externally" just like LinkedHashMap, HashMap ... you name it. – palindrom Jul 08 '15 at 06:54
  • yes, like all those, but unlike Guava's cache (the accepted answer) – Sean Patrick Floyd Jul 08 '15 at 07:18
  • Also, consider using `System.nanoTime()` for computing time differences as System.currentTimeMillis() is not consistent as it depends on the system time and might not be continous. – Ercksen Oct 30 '19 at 13:20
2

Typically, a cache should keep objects around some time and shall expose of them some time later. What is a good time to hold an object depends on the use case. I wanted this thing to be simple, no threads or schedulers. This approach works for me. Unlike SoftReferences, objects are guaranteed to be available some minimum amount of time. However, the do not stay around in memory until the sun turns into a red giant.

As useage example think of a slowly responding system that shall be able to check if a request has been done quite recently, and in that case not to perform the requested action twice, even if a hectic user hits the button several times. But, if the same action is requested some time later, it shall be performed again.

class Cache<T> {
    long avg, count, created, max, min;
    Map<T, Long> map = new HashMap<T, Long>();

    /**
     * @param min   minimal time [ns] to hold an object
     * @param max   maximal time [ns] to hold an object
     */
    Cache(long min, long max) {
        created = System.nanoTime();
        this.min = min;
        this.max = max;
        avg = (min + max) / 2;
    }

    boolean add(T e) {
        boolean result = map.put(e, Long.valueOf(System.nanoTime())) != null;
        onAccess();
        return result;
    }

    boolean contains(Object o) {
        boolean result = map.containsKey(o);
        onAccess();
        return result;
    }

    private void onAccess() {
        count++;
        long now = System.nanoTime();
        for (Iterator<Entry<T, Long>> it = map.entrySet().iterator(); it.hasNext();) {
            long t = it.next().getValue();
            if (now > t + min && (now > t + max || now + (now - created) / count > t + avg)) {
                it.remove();
            }
        }
    }
}
Matthias Ronge
  • 9,403
  • 7
  • 47
  • 63
  • 1
    HashMap is not thread safe, due to race conditions, map.put operation or resizing of map may lead to data corruption. See here: https://mailinator.blogspot.com/2009/06/beautiful-race-condition.html – Eugene Maysyuk Jun 12 '20 at 06:59
  • That's true. Indeed, most Java classes are not thread safe. If you need thread security, you need to check every affected class of your design to see if it meets the requirement. – Matthias Ronge Jun 15 '20 at 07:19
1

you can try Expiring Map http://www.java2s.com/Code/Java/Collections-Data-Structure/ExpiringMap.htm a class from The Apache MINA Project

toby941
  • 378
  • 2
  • 4