具有过期密钥的基于Java时间的地图/缓存[关闭]


253

您是否知道在给定超时后会自动清除条目的Java Map或类似标准数据存储?这意味着老化,旧的过期条目会自动“老化”。

最好在可通过Maven访问的开源库中?

我知道自己实现该功能的方法,并且过去已经做过几次,所以我不是在这方面寻求建议,而是寻求指向良好参考实现的指针。

WeakHashMap这样的基于WeakReference的解决方案不是一个选择,因为我的密钥很可能是非interintern字符串,并且我希望可配置的超时时间不依赖于垃圾回收器。

我也不想依靠Ehcache,因为它需要外部配置文件。我正在寻找仅代码解决方案。


1
查看Google收藏夹(现称为Guava)。它具有可以自动使条目超时的映射。
dty 2010年

3
因不符合指导原则而被关闭,具有253个投票和17.6万个视图的问题(在该主题中的搜索引擎中排名很高)的问题多么奇怪
布赖恩

Answers:


320

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

5
太棒了,我知道Guava有一个答案,但我找不到!(+1)
肖恩·帕特里克·弗洛伊德

12
从v10开始,您应该改用CacheBuilder(guava-libraries.googlecode.com/svn/trunk/javadoc/com/google/…),因为MapMaker已弃用了到期时间
wwadge 2011年

49
警告!使用weakKeys()表示键是使用==语义而不是进行比较的equals()。我花了30分钟的时间弄清楚了为什么我的String-keyed缓存无法正常工作:)
LaurentGrégoire13年

3
伙计们,@ Laurent提到的事情weakKeys()很重要。weakKeys()不需要90%的时间。
Manu Manjunath

3
为了初学者(包括我本人),@ ShervinAsgari是否可以将更新的番石榴示例切换为使用Cache而不是LoadingCache的示例?它将更好地匹配该问题(因为LoadingCache具有的功能超过了带有过期条目的地图,并且创建起来要复杂得多),请参见github.com/google/guava/wiki/CachesExplained#from-a-callable
Jeutnarg '17

29

这是我为相同要求所做的示例实现,并发效果很好。可能对某人有用。

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链接(带有监听器实现)

https://github.com/vivekjustthink/WeakConcurrentHashMap

干杯!!


为什么要执行cleanMap()指定时间的一半?
EliuX

Bcoz确保密钥已过期(已删除),并避免线程极端循环。
Vivek

@Vivek,但使用此实现,最大数量(expiryInMillis / 2)条目已过期但仍存在于缓存中。由于线程在expireyInMillis / 2期后删除条目
rishi007bansod

19

您可以尝试的自过期哈希图实现。此实现不使用线程来删除过期的条目,而是使用DelayQueue,该延迟在每次操作时都会自动清除。


我更喜欢Guava的版本,但为图片增添完整性时+1
Sean Patrick Floyd

@ piero86我想说对方法expireKey(ExpiringKey <K> delayKey)中的delayQueue.poll()的调用是错误的。您可以松开一个任意的ExpiringKey,以后不能在cleanup()中使用它-泄漏。
Stefan Zobel

1
另一个问题:您不能将相同的密钥两次放入不同的生存期。在a)put(1,1,shortLived)之后,然后b)put(1,2,longLived)之后,无论longLived有多久,密钥1的Map条目都会在shortLived ms之后消失。
Stefan Zobel

感谢您的见解。您能否将这些问题报告为要点?
pcan

根据您的建议进行修复。谢谢。
pcan

19

Apache Commons具有Map的装饰器以使条目过期:PassiveExpiringMap 它比Guava的缓存更简单。

PS小心,它不同步。


1
这很简单,但是它仅在您访问条目后才检查到期时间。
Badie

按照Javadoc的规定当调用涉及访问整个地图内容的方法(即,containsKey(Object),entrySet()等)时,此修饰器会在实际完成调用之前删除所有过期的条目。
NS du Toit

如果要查看此库的最新版本(Apache commons commons-collections4),请访问mvnrepository上相关库的链接
NS du Toit

3

听起来ehcache对于您想要的功能来说过于强大,但是请注意,它不需要外部配置文件。

通常,将配置移动到声明性配置文件中是个好主意(因此,当新安装需要不同的到期时间时,您无需重新编译),但是完全不需要,您仍然可以通过编程方式配置它。 http://www.ehcache.org/documentation/user-guide/configuration


2

Google馆藏(番石榴)具有MapMaker,您可以在其中设置时间限制(有效期),还可以在使用工厂方法的情况下选择使用软引用还是弱引用来创建您选择的实例。



2

如果有人需要简单的东西,下面是一个简单的密钥过期集合。它可能会轻松转换为地图。

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
这对于单线程情况很好,但是在并发情况下会惨遭破坏。
肖恩·帕特里克·弗洛伊德

@SeanPatrickFloyd您的意思是喜欢LinkedHashMap本身吗?就像LinkedHashMap,HashMap一样,“它必须在外部同步”。
palindrom

是的,像所有这些一样,但是与番石榴的缓存(已接受的答案)不同
肖恩·帕特里克·弗洛伊德

另外,请考虑使用System.nanoTime()System.currentTimeMillis()来计算时间差,因为它取决于系统时间并且可能不是连续的,因此不一致。
Ercksen

2

通常,缓存应将对象保留大约一段时间,并应在一段时间后将其公开。什么是用以保持物体的好时机取决于使用情况。我希望这件事很简单,没有线程或调度程序。这种方法对我有用。与SoftReferences 不同,对象可以保证在最短时间内可用。但是,在太阳变成红色巨人之前,不要呆在记忆中。

作为一个使用示例,考虑一个响应速度缓慢的系统,该系统应能够检查请求是否在最近才完成,即使忙碌的用户多次按下按钮,该请求也不会执行两次所请求的操作。但是,如果稍后再请求执行相同的操作,则应再次执行。

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

好的,谢谢
bigbadmouse19年

1
HashMap不是线程安全的,由于竞争条件,map.put操作或调整地图大小可能会导致数据损坏。看到这里:mailinator.blogspot.com/2009/06/beautiful-race-condition.html
尤金·

确实如此。实际上,大多数Java类都不是线程安全的。如果需要线程安全性,则需要检查设计中每个受影响的类,以查看其是否满足要求。
Matthias Ronge

1

番石榴缓存易于实现。我们可以使用番石榴缓存按时使密钥过期。我已经阅读了全文,下面给出了我的学习重点。

cache = CacheBuilder.newBuilder().refreshAfterWrite(2,TimeUnit.SECONDS).
              build(new CacheLoader<String, String>(){
                @Override
                public String load(String arg0) throws Exception {
                    // TODO Auto-generated method stub
                    return addcache(arg0);
                }

              }

参考:番石榴缓存示例


1
请更新链接,因为它现在无法使用
smaiakov
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.