如何运行一个后台线程来定期清理列表中的一些元素?

Posted

技术标签:

【中文标题】如何运行一个后台线程来定期清理列表中的一些元素?【英文标题】:How can I run a background thread that cleans up some elements in list regularly? 【发布时间】:2012-03-10 07:20:14 【问题描述】:

我目前正在实施缓存。我已经完成了基本的实现,如下所示。我想做的是运行一个线程来删除满足某些条件的条目。

class Cache 
    int timeLimit = 10; //how long each entry needs to be kept after accessed(marked)
    int maxEntries = 10; //maximum number of Entries
    HashSet<String> set = new HashSet<String>();   
    public void add(Entry t)
        ....
    

    public Entry access(String key)
        //mark Entry that it has been used
        //Since it has been marked, background thread should remove this entry after timeLimit seconds.
        return set.get(key);
    
    ....

我的问题是,我应该如何实现后台线程,以便该线程将绕过 set 中的条目并删除已为 marked &amp;&amp; (last access time - now)&gt;timeLimit 的条目?

编辑

以上只是简化版的代码,我没有写同步语句。

【问题讨论】:

您的Cache 似乎不是线程安全的;如果你想从多个线程访问它,你最好先处理它。 另一种方法是从add 方法调用清理,那么您不需要额外的线程。是否适合您取决于性能要求。 我想你最好使用 ConcurrentHashMap 或将所有方法设为synchronized(目前为accessadd,以后可能还有cleanup)。 【参考方案1】:

为什么要重新发明***? EhCache(以及任何体面的缓存实现)将为您执行此操作。还有更轻量级的 MapMaker Cache from Guava 可以自动删除旧条目。

如果你真的想自己实现这个,其实没那么简单。

    记住同步。您应该使用ConcurrentHashMapsynchronized 关键字来存储条目。这可能真的很棘手。

    您必须以某种方式存储每个条目的最后访问时间。每次访问条目时,都必须更新该时间戳

    考虑驱逐政策。如果您的缓存中有多个maxEntries,首先要删除哪些?

    你真的需要后台线程吗?

    这令人惊讶,但 EhCache(企业就绪并经过验证)不使用后台线程来使旧条目无效)。相反,它会等到地图已满并懒惰地删除条目。这看起来是一个很好的权衡,因为线程很昂贵。

    如果你有一个后台线程,应该每个缓存一个还是全局一个?您是在创建新缓存时启动新线程还是拥有所有缓存的全局列表?这比你想象的要难......

回答完所有这些问题后,实现就相当简单了:每隔一秒左右遍历所有条目,如果满足您已经编写的条件,则删除条目。

【讨论】:

更不用说,一旦你有一个线程,它就会持有一个会造成泄漏的强引用(除非你确保在它休眠时只持有一个弱引用) MapMaker 已经严重过时了;更喜欢较新的Cache @LouisWasserman:谢谢,我还没有真正使用过番石榴。我更新了我的答案。【参考方案2】:

我个人会为此使用Guava 的Cache 类型。它已经是线程安全的,并且内置了根据时间限制从缓存中逐出的方法。如果你想让一个线程定期扫描它,你可以这样做:

    new Thread(new Runnable() 
        public void run() 
            cache.cleanUp();
            try  Thread.sleep(MY_SLEEP_DURATION);  catch (Exception e) ;
        
    ).start();

【讨论】:

那么线程只会运行一次就停止了?还是每次访问缓存时都应该启动它导致资源不足? 0_o【参考方案3】:

我不认为您真的需要后台线程。相反,您可以在执行查找之前或之后删除过期条目。这简化了整个实现,而且很难区分。

顺便说一句:如果您使用 LinkedHashMap,则可以通过覆盖 removeEldestEntry 将其用作 LRU 缓存(请参阅其 javadocs 以获取示例)

【讨论】:

是的,我发现removeEldestEntry 非常可行。但我只是想尝试消除满足问题最后一句中描述的两个条件的条目。但是,谢谢你的信息! 您也可以通过覆盖 get() 来添加检查。【参考方案4】:

首先,您提供的代码不完整,因为HashSet 上没有get(key)(所以我假设您的意思是某种Map)并且您的代码没有提及任何“标记”。进行缓存的方法也有很多,如果不知道要缓存什么以及为什么要缓存,就很难选出最佳解决方案。

在实现缓存时,通常假设数据结构将被多个线程同时访问。因此,您需要做的第一件事是使用线程安全的后备数据结构。 HashMap 不是线程安全的,但 ConcurrentHashMap 是。还有许多其他并发的Map 实现,即Guava、Javolution 和high-scale lib。除了地图之外,还有其他构建缓存的方法,它们的用途取决于您的用例。无论如何,您很可能需要使后备数据结构线程安全,即使您决定不需要后台线程而是在尝试从缓存中检索过期对象时驱逐过期对象。或者让 GC 使用 SoftReferences 删除条目。

一旦您的缓存内部实现了线程安全,您就可以简单地启动一个新的(很可能是守护进程)线程,该线程会定期扫描/迭代缓存并删除旧条目。线程将在一个循环中执行此操作(直到被中断,如果您希望能够再次停止它),然后在每次扫描后休眠一段时间。

但是,您应该考虑是否值得自己构建自己的缓存实现。编写线程安全的代码并不容易,我建议您在尝试编写自己的缓存实现之前先学习一下。我可以推荐 Java Concurrency in Practice 一书。

当然,更简单的方法是使用现有的缓存实现。 Java 领域有许多可用的选项,所有选项都有自己独特的权衡。

EhCache 和 JCS 都是通用缓存,可以满足典型“企业”应用程序中的大多数缓存需求。 Infinispan 是一种针对分布式使用进行了优化的缓存,因此可以缓存比单台机器所能容纳的更多的数据。我也喜欢它基于 ConcurrentMap 的 API。 正如其他人所提到的,Google 的 Guava 库有一个 Cache API,这对于小型内存缓存非常有用。

由于您想限制缓存中的条目数,您可能对对象池而不是缓存感兴趣。

Apache Commons-Pool 被广泛使用,其 API 与您尝试自己构建的类似。 另一方面,Stormpot 有一个相当不同的 API,我几乎只是因为我编写了它才提到它。这可能不是您想要的,但是在不知道您要缓存什么以及为什么要缓存的情况下,谁能确定呢?

【讨论】:

【参考方案5】:

首先,访问您的收藏集synchronized 或使用 ConcurrentHashSet 一个基于 ConcurrentHashMapSet,如下面的 cmets 所示。

其次,编写您的新线程,并将其实现为一个无限循环,周期性地迭代先前的集合并删除元素。您应该以在构造函数中使用正确集合初始化的方式编写此类,这样您就不必担心“我如何访问正确的集合”。

【讨论】:

那就是Collections.newSetFromMap(new ConcurrentHashMap&lt;String, Boolean&gt;())

以上是关于如何运行一个后台线程来定期清理列表中的一些元素?的主要内容,如果未能解决你的问题,请参考以下文章

从 asp.net API 中的方法返回后,如何保持线程运行?

在 iOS 中定期在后台线程中运行任务

总是在后台运行线程/进程?

在没有服务的情况下杀死应用程序后如何在后台线程中运行代码?

python的日志,如何做到一天是单独一个日志,并且定期清理?

Android:如何在所有后台线程完成后启用按钮