【问题标题】:Is it possible to Iterate over a guava Cache in order of insertion/access?是否可以按插入/访问的顺序迭代番石榴缓存?
【发布时间】:2014-03-24 21:02:56
【问题描述】:

我正在尝试使用 Guava 缓存来替代 ConcurrentLinkedHashMap。但是我发现,虽然 ConcurrentLinkedHashMap 允许我按插入顺序遍历地图,但 Guava 的 asMap() 方法不会以任何特定顺序返回元素。是我遗漏了什么,还是这个功能根本不可用?

示例(尝试打印键、值和条目):

Cache<Integer, Integer> cache = CacheBuilder.newBuilder().maximumSize(10).initialCapacity(10)
        .expireAfterAccess(10000, TimeUnit.SECONDS).build();

cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);
cache.put(4, 4);
cache.put(5, 5);
cache.put(6, 6);

Iterator<Integer> iter1 = cache.asMap().keySet().iterator();

System.out.println("Keys");
while (iter1.hasNext())
    System.out.println(iter1.next());

System.out.println("Values");
Iterator<Integer> iter2 = cache.asMap().values().iterator();

while (iter2.hasNext())
    System.out.println(iter2.next());

System.out.println("Entries");
Iterator<Entry<Integer, Integer>> iter3 = cache.asMap().entrySet().iterator();

while (iter3.hasNext()) {
    Entry<Integer,Integer> entry = iter3.next();
    System.out.println(entry.getKey() + " " + entry.getValue());
}

打印:

Keys
2
6
1
4
3
5
Values
2
6
1
4
3
5
Entries
2 2
6 6
1 1
4 4
3 3
5 5

【问题讨论】:

  • 好问题,看起来答案是“否”...您可以实现自己的Cache,但不能在CacheBuilder 中使用它...
  • 实现我自己的缓存意味着复制 Guava 缓存已经保存的订购信息。这似乎很尴尬。
  • Cache 是一个接口。 Guava 的缓存 AFAICS 根本不保证订购信息。我的意思是你可以实现Cache,这样底层映射就是ConcurrentLinkedHashMap;现在它是ConcurrentHashMap 的派生词。但同样,即使你这样做,我也看不出你如何在 CacheBuilder 中使用它,所以......
  • 啊,好吧,我以为你的意思是扩展现有的 LoadingCache。谢谢。

标签: java dictionary caching guava


【解决方案1】:

CacheWriter 将允许在显式写入或删除期间调用您的代码。对于加载缓存,您必须在加载程序中执行相同的工作。这也是在条目的锁定下执行的,因此您可以假设原子性。这应该让您在不依赖缓存的内部数据结构的情况下维护排序。请注意,如果执行有序迭代时的工作成本很高,您可能希望将其复制到锁内部,然后在外部执行工作,以免阻塞缓存写入。

LinkedHashMap<K, V> orderedMap = new LinkedHashMap<>();
LoadingCache<K, V> cache = Caffeine.newBuilder()
    .writer(new CacheWriter<K, V>() {
      public void write(K key, V value) {
        synchronized (orderedMap) {
          orderedMap.put(key, value);
        }
      }
      public void delete(K key, V value, RemovalCause cause) {
        if (cause == RemovalCause.REPLACED) {
          return;
        }
        synchronized (orderedMap) {
          orderedMap.remove(key);
        }
      }
    })
    .maximumSize(1_000)
    .build(key -> {
        V value = ...
        synchronized (orderedMap) {
          orderedMap.put(key, value);
        }
        return value;
    });

cache.put(key1, value); // calls writer under lock
cache.get(key2); // calls loader under lock; not writer
cache.invalidate(key1); // calls writer under lock
cache.policy().eviction().get().setMaximum(0); // calls writer under lock

synchronized (orderedMap) {
  for (K key : orderedMap.keySet()) {
    // do work, but blocks writes!
  }
}

【讨论】:

  • 我明白了,这是一个不错的方法。另外,可以通过使用多个锁来改进锁定,并且可能使用 ReadWrite 锁来允许多个读取器。
  • 不幸的是,读/写锁通常是worse,除非你因为 I/O 而持有很长时间。
  • 当然,我需要进行实际基准测试。
  • 花了一段时间,但我终于对一堆 Java 锁定机制进行了基准测试 (github.com/maltalex/JavaLockBenchmark)。你对ReentrantReadWriteLock 的性能是正确的,但是在Java 8+ 中,如果重入锁定不重要,ReentrantReadWriteLock 可以被性能更高的StampedLock 取代,它优于所有其他锁定机制。跨度>
  • @Malt StampedLock 肯定要好得多,但乐观且不会阻塞。这对于内存调用来说是完美的,对于昂贵的 I/O 调用来说是浪费的。无论哪种情况,您都必须注意读取没有副作用,例如LRU 在访问时重新排序,因此读/写锁不起作用,因为它始终是不安全的突变。
【解决方案2】:

(回答我自己的问题)

看来fge的回答是正确的,Guava Cache不能按照插入的顺序进行迭代。作为一种解决方法,我使用了前面提到的 ConcurrentLinkedHashMap,它的功能较少,但允许有序迭代。

我仍然很感谢 Guava 团队的某个人的官方回答,因为这似乎表明 ConcurrentLinkedHashMap 没有完全集成到 Guava 中(与 ConcurrentLinkedHashMap 文档相反)

【讨论】:

  • 官方回答:我们没有计划将这个功能添加到 Guava - 抱歉!
  • 好的,很公平。谢谢!
  • 现在可以通过 policy api 在 Java 8 重写 Caffeine 中使用。
  • @BenManes 我刚刚再次遇到相同的用例并回忆了这个问题。 Caffeine 如何提供插入顺序迭代?我无法在 API 中找到它。
  • @Malt 请参阅cache.policy() 了解驱逐或到期命令等低级详细信息。您可以改为维护自己的订单,因为它不是严格访问或插入的订单
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-06-25
  • 2012-04-27
  • 1970-01-01
相关资源
最近更新 更多