java

mjw 2023-04-10T11:07:28.397329Z

As someone who comes from the land of single threadedness, I have been playing around with multi-threaded examples in my spare time to grow my understanding and uncover my assumptions about multi-threaded environments. One such experiment is a cache for CompletableFutures. If I just use ConcurrentHashMap#computeIfAbsent, my machine can run one billion iterations of a simple completable future in about 22 seconds. However, the moment I add eviction logic, that time increases to about 36 minutes. Is there any way I can make the following more efficient? As far as I can tell, synchronization is required for this logic, so I’m not sure that it can be improved (at least not by much):

public CompletableFuture<T> removeIfExpired(String key) {
	synchronized(cache) {
		CacheEntry<T> cached = cache.get(key);
		if (cached == null || cached.getExpiresInMs() == null) {
			return null;
		}

		if (Duration.between(cached.getCreatedAt(), Instant.now()).toMillis() >= cached.getExpiresInMs()) {
			CacheEntry<T> removed = cache.remove(key);
			if (removed != cached) {
				throw new IllegalStateException("Concurrency error: item removed from cache not the original item checked for expiration.");
			}
			return removed.getValue();
		}

		return null;
	}
}

2023-04-10T20:58:55.942849Z

If you use the version of remove that takes a key and value then I don't believe you need the synchronize block

2023-04-10T20:59:48.897539Z

But it may just be that when you add eviction so you have to recompute things that have been evicted it takes longer

mjw 2023-04-10T22:35:53.547409Z

Oh! Thanks! I’d missed that particular overload when looking at the java docs. You’re right that eviction just adds to the time, but with remove(key, value), that time is reduced from 36min for a billion records to 75 seconds (on my machine).