Fork me on GitHub
#java
<
2023-04-10
>
mjw11:04:28

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;
	}
}

hiredman20:04:55

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

hiredman20:04:48

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

mjw22:04:53

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).