Can you still use a ConcurrentLinkedHashMap with Caffeine?
Asked Answered
S

1

2

Caffeine provides a great alternative to Guava's caching library, but can you still use just the ConcurrentLinkedHashMap class itself, or a Cache the implements concurrent, in-order iteration? It does not appear in Caffeine anymore that I can see, although I'm sure its using something similar under the hood.

I have tested the Caffeine Cache and it does not iterate over the elements in order of insertion, and I don't see any options on the Caffeine builder that would make me thing this feature still exists. But maybe I'm missing something.

Any ideas?

Strom answered 21/3, 2022 at 18:23 Comment(1)
Did you see this issue: github.com/ben-manes/caffeine/issues/220 ?Schwann
A
2

ConcurrentLinkedHashMap offered access-order iteration via the ascending and descending snapshots. This was useful for cases like persisting the hottest entries for a warm server restart. Otherwise views and iterators were in encounter order by the underlying ConcurrentHashMap.

Caffeine offers snapshots by in the eviction policy's hottest/coldest order and the expiration policy's youngest/oldest order. See Cache.policy().

Insertion-order is not that difficult to implement because writes are rare, exclusive, and items are not being shifted around frequently. The simplest approach is to use a Map computation to maintain a secondary data structure. If you want that in addition to the cache then Caffeine's asMap() view offers atomic versions. This way you can have fast random access reads via a ConcurrentHashMap and ordered iterations for a background process.

var data = new ConcurrentHashMap<String, Integer>();
var order = Collections.synchronizedMap(new LinkedHashMap<String, Integer>());

data.compute("a", (key, oldValue) -> {
  order.put(key, 1);
  return 1;
});

// Locks the map during access, blocking writes; consider snapshotting first
order.forEach((key, value) -> System.out.printf("%s=%s%n", key, value);

Vavr's LinkedHashMap might be useful if you need concurrent iteration or want to avoid the cost of a full copy. That is a persistent data structure, which where writes produce a new immutable version. That could be used above or directly if you don't need any caching concepts.

volatile Map<K, V> data = LinkedHashMap.empty();
final Lock lock = new ReentrantLock();

// Single writer
lock.lock();
try {
  data = data.put(1, 2).put(3, 4);
} finally {
  lock.unlock();
}

// Multiple readers
System.out.println(data);
Ammoniac answered 21/3, 2022 at 19:45 Comment(3)
My use case is a bit different -- it is a high-throughput cache of data packets flowing threw our system, so there are often writes/deletes from it, and a rare need to iterate over those data packets in ascending or descending order. (in other words, not a "loading" cache). So the above wouldn't work - but it does validate my impl which is to make a copy before iteration (in a sync block). I was hoping to avoid the sync and copy, but seems difficult at best.Strom
@Strom If ascending only, the optimal solution would be to wrap value and construct a lock-free queue. That would be an inlined ConcurrentLinkedQueue.Node and related operations, with the Java 7 variant of leaving dead nodes that are lazily removed during iteration. This requires a simpler singly-linked list rather than doubly, which is much more straightforward for lock-free code. Only you can judge if this complexity is needed; I'd be happy to help give you a PoC impl.Ammoniac
@Strom Another idea is to combine the map with a ConcurrentLinkedDeque and have the wrapped value hold a descendingIterator() that was positioned at that element when inserted under a Map computation. Then removal stays O(1), no low-level lock-free magic, and you get bidirectional traversal. However none of these data structures are optimized for heavy writes so you'd have to benchmark to determine if any fit within your performance budget.Ammoniac

© 2022 - 2024 — McMap. All rights reserved.