从Guava RemovalListener重新插入条目是否安全?

时间:2012-01-06 20:56:40

标签: java caching concurrency guava

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

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

  2. 如果是这样,它是否是线程安全的,这样CacheLoaderRemovalListener回调仍然在另一个线程中发生的时候,MapMaker无法为该键生成第二个值吗?

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

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

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

4 个答案:

答案 0 :(得分:2)

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

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

答案 1 :(得分:1)

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

请注意,正如Javadoc所述,使用softValues()确实会带来权衡。

答案 2 :(得分:0)

我昨天发现,当我升级到Guava 11时, 是一种将条目锁定到缓存中的方法:使用maximumWeight作为唯一的删除方法,让你的Weigher返回对于应保持锁定在缓存中的条目的权重为0。我没有通过测试验证这一点,但CacheBuilder.weigher()州的文档

  

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

答案 3 :(得分:0)

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

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

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

  
      
  1. 如果是这样,它是否是线程安全的,这样,当RemovealListener回调仍然在另一个线程中发生时,CacheLoader没有可能产生该键的第二个值?
  2.   

不,由GridView触发的突变对于相关的删除不是原子的。不仅另一个线程可以在删除结束和被通知的监听器之间访问缓存,这两个操作之间可能存在显着的延迟。正如concurrent mutations提到“删除侦听器操作是在[缓存维护期间”同步执行的“)。它the wiki表示维护任务是小批量执行的,并且不会保证何时(或隐含地)实际执行它们。

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

就像你说的,这不是一个真正的缓存(或更具体地说是一个驱逐缓存)。如果存在您不想被驱逐的对象,则不要将它们存储在驱逐缓存中。就像路易斯建议使用双数据结构设置和正在进行的流程的地图以及完成流程的驱逐缓存一样,可以完全满足您的需求。