基于时间的Java映射/缓存,具有过期键

321

你们中有没有人知道一个Java Map或类似的标准数据存储器,可以在给定的超时时间后自动清除条目?这意味着老旧的过期条目会自动“过时”。

我知道如何自己实现这个功能,并且在过去已经做过几次,所以我不是在寻求关于这方面的建议,而是想要指向一个好的参考实现的指针。

基于WeakReference的解决方案,例如WeakHashMap不是一个选项,因为我的键很可能是非内部字符串,并且我希望有一个可配置的超时时间,这不依赖于垃圾回收机制。

Ehcache也是一个选项,但我不想依赖它,因为它需要外部配置文件。我正在寻找一个只有代码实现的解决方案。


这个问题正在meta上讨论关闭的原因。 - Elikill58
8个回答

375

是的。Google Collections,或者现在被称为Guava,有一个叫做MapMaker的东西可以做到这一点。

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);
         }
       });

更新:

自guava 10.0(发布于2011年9月28日)起,这些MapMaker方法中的许多已被弃用,推荐使用新的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);
          }
        });

13
从版本10开始,您应该使用CacheBuilder(http://guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/common/cache/CacheBuilder.html)而不是MapMaker,因为在MapMaker中,过期等功能已被弃用。 - wwadge
70
警告!使用weakKeys()意味着键是使用“==”语义进行比较,而不是equals()。我浪费了30分钟来弄清楚为什么我的基于字符串键的缓存无法工作:) - Laurent Grégoire
3
你如何使用基于简单映射的方法实现createExpensiveGraph(),同时还支持.put()? - neu242
4
各位,@Laurent提到的weakKeys()是很重要的。90%的情况下不需要使用weakKeys() - Manu Manjunath
3
@ShervinAsgari 为了初学者的方便(包括我自己),你能否将更新后的guava示例更改为使用Cache而不是LoadingCache?这将更符合问题的要求(因为LoadingCache具有超出具有过期条目的映射的功能,并且创建起来更加复杂)。请参见https://github.com/google/guava/wiki/CachesExplained#from-a-callable - Jeutnarg
显示剩余9条评论

45

这是我为同样的需求编写的示例实现,且并发性能良好。可能对某些人有用。

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 代码库链接(监听器实现)

https://github.com/vivekjustthink/WeakConcurrentHashMap

祝好!


为什么您只有一半的时间执行 cleanMap() 函数? - EliuX
因为它确保密钥过期(被删除)并避免线程极端循环。 - Vivek
1
能否这样做,在get中检查值是否过时,如果是则返回null,而不是使用清理线程。 就像这样... https://dev59.com/dOPxs4cB2Jgan1zn5-xq#63189871 - Vikash
@Vikash 正如之前的评论中提到的那样,清理线程旨在改善 get 的性能,并且值不会在内存中持续时间超过所需。如果在 get 中实现并且这些值未被访问,则它们将永久保存在内存中。 - Vivek
@Vivek 使用WeakHashMap,数据会被删除:
  1. 如果cleanMap检测到某个项已经超过了其逐出时间。
  2. 如果GC检测到可以删除的项。 因此,取决于哪个先发生,我们不能保证数据的存在。 这不是OP所要求的。
- Nishit
显示剩余3条评论

34

Apache Commons有一个Map的装饰器用于过期条目:PassiveExpiringMap,比Guava的缓存更简单。

P.S. 注意,它没有同步。


6
它很简单,但仅在您访问条目后检查过期时间。 - ahmdx
1
根据Javadoc:在调用涉及访问整个映射内容的方法(即containsKey(Object),entrySet()等)时,此装饰器在实际完成调用之前删除所有过期条目。 - dutoitns
如果你想查看这个库(Apache commons commons-collections4)的最新版本,这里是mvnrepository上相关库的链接 - dutoitns
2
如果在使用PassiveExpiringMap的同时与Collections.synchronizedMap一起使用,请注意访问映射集合(values、keySet、entrySet)将不会触发过期条目的淘汰。原因是values、keySet和entrySet在SynchronizedMap中被“缓存”了。 - seb foucault
实现提示:它具有惰性/被动驱逐进程,因此在执行方法之前需要 removeAllExpired,所以如果您不调用任何方法,则过期的项将不会被删除,直到您在地图上调用任何方法。 - Ahmed Nabil

22
你可以尝试使用我实现的自动过期哈希映射(链接)。该实现不使用线程来删除过期的条目,而是使用DelayQueue(链接)来自动清除每个操作的队列。请注意保留HTML标记。

我更喜欢Guava的版本,但是因为增加了完整性,所以还是点个赞。 - Sean Patrick Floyd
@piero86我认为在expireKey(ExpiringKey<K> delayedKey)方法中调用delayQueue.poll()是错误的。你可能会失去一个任意的ExpiringKey,它不能在cleanup()中被利用 - 这是一种泄漏。 - Stefan Zobel
1
另一个问题是:您无法使用不同的生命周期两次放置相同的键。在执行a) put(1, 1, shortLived)之后,再执行b) put(1, 2, longLived),键1的Map条目将在shortLived毫秒后消失,无论longLived有多长。 - Stefan Zobel
谢谢您的见解。您能否将这些问题作为评论报告在gist中呢? - pcan
请注意,Guava 在写入和读取操作期间也执行清理操作。这并不意味着 Guava 更好,只是它的工作方式相同(事实上,我更倾向于简单性,而 Guava 则非常庞大)。 - NateS
显示剩余3条评论

4
听起来ehcache对你想要的东西来说有点过头了,但请注意它不需要外部配置文件。
通常最好将配置移到声明性配置文件中(这样当新安装需要不同的到期时间时就不需要重新编译),但这并不是必需的,你仍然可以通过编程方式进行配置。 http://www.ehcache.org/documentation/user-guide/configuration

3

如果有人需要一个简单的东西,下面是一个简单的过期键集合。它可以很容易地转换为映射。

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();
        }
    }
}

2
这对于单线程情况来说还可以,但在并发情况下会彻底崩溃。 - Sean Patrick Floyd
@SeanPatrickFloyd 你的意思是像LinkedHashMap本身一样吗?就像LinkedHashMap、HashMap等,它必须在外部同步...你说得对。 - palindrom
是的,像所有这些一样,但不像Guava的缓存(被接受的答案)。 - Sean Patrick Floyd
此外,考虑使用 System.nanoTime() 来计算时间差,因为 System.currentTimeMillis() 不一致,它取决于系统时间,可能不连续。 - Ercksen

2
通常,缓存应该在一段时间内保留对象,并在稍后释放它们。"保留对象的好时机" 取决于具体的使用情况。我希望这个东西简单易用,不需要线程或调度器。这种方法适合我。与 "SoftReference" 不同,对象被保证在一定的时间内可用。然而,它们不会一直驻留在内存中,直到太阳变成红巨星。
例如,考虑一个响应速度较慢的系统,它应该能够检查是否最近已经完成了某个请求,如果是,则不执行请求操作两次,即使用户按下按钮多次也是如此。但是,如果稍后再次请求相同的操作,则应再次执行该操作。
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();
            }
        }
    }
}

1
HashMap不是线程安全的,由于竞态条件,map.put操作或调整map大小可能会导致数据损坏。请参见此处:https://mailinator.blogspot.com/2009/06/beautiful-race-condition.html - Eugene Maysyuk
没错。实际上,大多数的Java类都不是线程安全的。如果你需要线程安全性,你需要检查你设计中涉及到的每个类,看它们是否符合要求。 - Matthias Ronge

1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接