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

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

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

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

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

我也会提出一个function请求 – 这可以解决问题:允许锁定单个条目以防止驱逐(然后解锁)。

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

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

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

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

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

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

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

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

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

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

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

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

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

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