【问题标题】:Is it safe to reinsert the entry from Guava RemovalListener?从 Guava RemovalListener 重新插入条目是否安全?
【发布时间】:2012-01-06 20:56:40
【问题描述】:

我有一个 Guava Cache(或者更确切地说,我正在从 MapMaker 迁移到 Cache),这些值代表长期运行的工作。我想将expireAfterAccess 行为添加到缓存中,因为这是清理它的最佳方式;但是,即使在一段时间内没有通过缓存访问该作业,它也可能仍在运行,在这种情况下,我需要防止它从缓存中删除。我有三个问题:

  1. 重新插入在RemovalListener回调期间被删除的缓存条目是否安全?

  2. 如果是,它是否是线程安全的,这样CacheLoader 不可能为该键生成第二个值,而RemovalListener 回调仍在另一个线程中发生?

    李>
  3. 有没有更好的方法来实现我想要的?这不仅仅是/只是一个“缓存” - 每个键使用一个且只有一个值是至关重要的 - 但我也想在它所代表的作业完成后将条目缓存一段时间。我之前使用过MapMaker,现在我需要的行为在该类中已弃用。在作业运行时定期 ping 地图是不优雅的,在我的情况下,是不可行的。也许正确的解决方案是拥有两张地图,一张没有驱逐,一张有,并在完成时迁移它们。

我也会提出功能请求 - 这将解决问题:允许锁定单个条目以防止驱逐(然后解锁)。

[编辑以添加一些细节]:此映射中的键是指数据文件。这些值可以是正在运行的写入作业、已完成的写入作业,或者 - 如果没有正在运行的作业 - 一个只读的、查找时生成的对象,其中包含从文件中读取的信息。重要的是每个文件恰好有零个或一个条目。我可以为这两件事使用单独的地图,但必须在每个密钥的基础上进行协调,以确保一次只存在一个或另一个。就获得正确的并发性而言,使用单个映射使其更简单。

【问题讨论】:

  • 我不得不承认我倾向于使用两张地图的解决方案,但我会使用一张地图和一张缓存。将地图用于仍在进行中的作业,或者使用缓存的 RemovalListener 从地图中删除条目?
  • 有趣的想法@Louis。我一直对两个地图解决方案持怀疑态度,因为我不想在两个不同的地图中查找内容,并处理围绕它的并发问题。如果我的Cache 只包含已完成的工作,我可以改用expireAfterWrite,这样我可以将它们保留一段时间,RemovalListener 会将它们从主地图中删除。没有什么会使用Cache。尽管采用这种方法,但使用Cache 确实没有任何意义。我真正需要的只是使用ScheduledThreadExecutor 安排删除时间。
  • 转念一想,我不能那样做,因为这个缓存实际上是一个缓存 - 大多数条目没有执行作业,而只是需要在一段时间后过期的只读引用时期。我真正需要的是锁定其中一些的能力——我最初关于通过RemovalListener 重新插入的问题只是实现锁定的一种方式。
  • 我真的不清楚在什么情况下你会锁定某些东西,在什么情况下你不应该。
  • 如果映射表示本地服务器上正在运行的作业,它可能存在于另一个线程中,那么弱引用会提供您需要的自动清理吗?没有足够的信息来建议最佳策略,恕我直言。

标签: java caching concurrency guava


【解决方案1】:

我对确切的问题并不完全清楚,但另一种解决方案是使用 softValues() 的缓存而不是最大大小或到期时间。每次访问缓存值时(在您的示例中,开始计算),您应该在其他地方维护状态,并对该值进行强引用。这将防止该值被 GCed。每当此值的使用降至零(在您的示例中,计算结束并且该值消失即可),您可以删除所有强引用。例如,您可以使用带有 Cache 值的 AtomicLongMap 作为 AtomicLongMap 键,并在地图上定期调用 removeAllZeros()

请注意,正如 Javadoc 所述,softValues() 的使用确实需要权衡取舍。

【讨论】:

  • 这会起作用并且可能比我所做的更优雅,因为它不涉及计时器线程,而且软引用更适合实际缓存。我最终使用了具有基于时间的过期的辅助映射,然后在该映射的删除侦听器中决定是否从主映射中逐出,或者如果没有,则重新插入辅助映射。两种解决方案都需要第二次收集,但其他方面都很简单。
【解决方案2】:

我查看了 Guava 代码,发现 CustomConcurrentHashMapCacheBuilder 用作底层实现)将删除通知添加到内部队列并在稍后处理它们。因此,从删除回调中将条目重新插入映射在技术上是“安全的”,但会打开一个窗口,在此期间条目不在映射中,因此另一个线程可以通过CacheLoader 生成备用条目。

我在上面的评论中使用@Louis 的建议解决了这个问题。主映射根本没有过期时间,但每次我在该映射中查找某些内容时,我也会将一个条目添加到具有expireAfterAccess 的辅助缓存中。在该二级缓存的删除侦听器中,我决定是否从主映射中删除该条目。没有其他东西使用二级缓存。这似乎是有条件驱逐的一个优雅的解决方案,并且它正在工作。 (我真的仍在使用 Guava r09 MapMaker 来解决这个问题,但它应该同样适用于 Guava 11。)

【讨论】:

    【解决方案3】:

    要直接解决您帖子中的前两个问题,删除侦听器会在某个任意时间点触发 元素从缓存中删除后,因此您不能使用删除侦听器以原子方式重新插入一个值。

    1. 重新插入在 RemovalListener 回调期间被删除的缓存条目是否安全?

    是的,这是安全的;在RemovalListener 中调用.put() 不会以任何方式破坏缓存。从广义上讲,Cacheconcurrent mutations 没有问题。

    1. 如果是这样,它是否是线程安全的,这样当 RemovalListener 回调仍在另一个线程中发生时,CacheLoader 不可能为该键生成第二个值?

    不,RemovalListener 触发的突变相对于相关的删除不是原子的。另一个线程不仅可以在删除完成和通知侦听器之间访问缓存,而且这两个操作之间可能存在显着延迟。正如the wiki 提到的“删除侦听器操作在缓存维护期间同步执行”。 goes on 说维护任务是小批量执行的,并且不保证何时(或隐含地是否)它们将实际执行。

    1. 有没有更好的方法来实现我想要的?这不仅是严格意义上的/仅是“缓存” - 每个键都使用一个且只有一个值至关重要 - 但我还希望在它所代表的作业完成后将条目缓存一段时间。

    就像你说的,这并不是真正的缓存(或者更具体地说,是驱逐缓存)。如果有你不想被驱逐的对象,就不要将它们存储在驱逐缓存中。像 Louis 一样,建议使用双数据结构设置,其中包含用于正在进行的进程的映射和用于已完成进程的驱逐缓存,这正是您所需要的。

    【讨论】:

      【解决方案4】:

      昨天我升级到 Guava 11 时突然想到,一种方法可以将条目锁定到缓存中:使用 maximumWeight 作为唯一的删除方法,并让您的称重器返回 0应该保持锁定在缓存中的条目的权重。我还没有通过测试验证这一点,但CacheBuilder.weigher() 的文档指出,

      “当一个条目的权重为零时,它不会被考虑基于大小的驱逐(尽管它仍然可以通过其他方式驱逐)。”

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2018-08-08
        • 2011-04-18
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多