From 759fc0bfa415cdcd4cbb790403ed235562986ec1 Mon Sep 17 00:00:00 2001 From: John DeRegnaucourt Date: Sun, 23 Jun 2024 14:37:33 -0400 Subject: [PATCH] LRUCache now implements both a locking and a threaded caching strategy, user selectable. --- .../java/com/cedarsoftware/util/LRUCache.java | 1 - .../util/cache/ThreadedLRUCacheStrategy.java | 59 ++++++++----------- .../util/cache/LRUCacheTest.java | 12 +++- 3 files changed, 34 insertions(+), 38 deletions(-) diff --git a/src/main/java/com/cedarsoftware/util/LRUCache.java b/src/main/java/com/cedarsoftware/util/LRUCache.java index 9e326d61..0a3d92cc 100644 --- a/src/main/java/com/cedarsoftware/util/LRUCache.java +++ b/src/main/java/com/cedarsoftware/util/LRUCache.java @@ -3,7 +3,6 @@ import java.util.Collection; import java.util.Map; import java.util.Set; -import java.util.concurrent.ExecutorService; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; diff --git a/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java b/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java index 0ceb8540..ba87fd45 100644 --- a/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java +++ b/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java @@ -10,7 +10,6 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; -import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ForkJoinPool; import java.util.concurrent.ScheduledExecutorService; @@ -19,12 +18,12 @@ /** * This class provides a thread-safe Least Recently Used (LRU) cache API that will evict the least recently used items, - * once a threshold is met. It implements the Map interface for convenience. + * once a threshold is met. It implements the Map interface for convenience. *

- * LRUCache is thread-safe via usage of ConcurrentHashMap for internal storage. The .get(), .remove(), and .put() APIs - * operate in O(1) without blocking. When .put() is called, a background cleanup task is schedule to ensure - * {@code cache.size <= capacity}. This maintains cache size to capacity, even during bursty loads. It is not immediate, - * the LRUCache can exceed the capacity during a rapid load, however, it will quickly reduce to max capacity. + * LRUCache is thread-safe via usage of ConcurrentHashMap for internal storage. The .get(), .remove(), and .put() APIs + * operate in O(1) without blocking. When .put() is called, a background cleanup task is scheduled to ensure + * {@code cache.size <= capacity}. This maintains cache size to capacity, even during bursty loads. It is not immediate; + * the LRUCache can exceed the capacity during a rapid load; however, it will quickly reduce to max capacity. *

* LRUCache supports null for key or value. *

@@ -52,7 +51,7 @@ public class ThreadedLRUCacheStrategy implements Map { private final AtomicBoolean cleanupScheduled = new AtomicBoolean(false); private final ScheduledExecutorService scheduler; private final ForkJoinPool cleanupPool; - private boolean isDefaultScheduler; + private final boolean isDefaultScheduler; private static class Node { final K key; @@ -71,31 +70,20 @@ void updateTimestamp() { } /** - * Create a LRUCache with the maximum capacity of 'capacity.' Note, the LRUCache could temporarily exceed the - * capacity, however, it will quickly reduce to that amount. This time is configurable via the cleanupDelay + * Create a LRUCache with the maximum capacity of 'capacity.' Note, the LRUCache could temporarily exceed the + * capacity; however, it will quickly reduce to that amount. This time is configurable via the cleanupDelay * parameter and custom scheduler and executor services. - * @param capacity int maximum size for the LRU cache. + * + * @param capacity int maximum size for the LRU cache. * @param cleanupDelayMillis int milliseconds before scheduling a cleanup (reduction to capacity if the cache currently - * exceeds it). - * @param scheduler ScheduledExecutorService for scheduling cleanup tasks. - * @param cleanupPool ForkJoinPool for executing cleanup tasks. + * exceeds it). + * @param scheduler ScheduledExecutorService for scheduling cleanup tasks. + * @param cleanupPool ForkJoinPool for executing cleanup tasks. */ public ThreadedLRUCacheStrategy(int capacity, int cleanupDelayMillis, ScheduledExecutorService scheduler, ForkJoinPool cleanupPool) { - isDefaultScheduler = false; - if (scheduler == null) { - this.scheduler = Executors.newScheduledThreadPool(1); - isDefaultScheduler = true; - } else { - this.scheduler = scheduler; - isDefaultScheduler = false; - } - - if (cleanupPool == null) { - this.cleanupPool = ForkJoinPool.commonPool(); - } else { - this.cleanupPool = cleanupPool; - } - + this.isDefaultScheduler = scheduler == null; + this.scheduler = isDefaultScheduler ? Executors.newScheduledThreadPool(1) : scheduler; + this.cleanupPool = cleanupPool == null ? ForkJoinPool.commonPool() : cleanupPool; this.capacity = capacity; this.cache = new ConcurrentHashMap<>(capacity); this.cleanupDelayMillis = cleanupDelayMillis; @@ -259,11 +247,10 @@ public String toString() { // Schedule a delayed cleanup private void scheduleCleanup() { if (cleanupScheduled.compareAndSet(false, true)) { - scheduler.schedule(() -> ForkJoinPool.commonPool().execute(this::cleanup), cleanupDelayMillis, TimeUnit.MILLISECONDS); + scheduler.schedule(() -> cleanupPool.execute(this::cleanup), cleanupDelayMillis, TimeUnit.MILLISECONDS); } } - // Converts a key or value to a cache-compatible item private Object toCacheItem(Object item) { return item == null ? NULL_ITEM : item; @@ -279,13 +266,15 @@ private T fromCacheItem(Object cacheItem) { * Shut down the scheduler if it is the default one. */ public void shutdown() { - scheduler.shutdown(); - try { - if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) { + if (isDefaultScheduler) { + scheduler.shutdown(); + try { + if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) { + scheduler.shutdownNow(); + } + } catch (InterruptedException e) { scheduler.shutdownNow(); } - } catch (InterruptedException e) { - scheduler.shutdownNow(); } } } \ No newline at end of file diff --git a/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java b/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java index d0f0a5be..86bdf0a6 100644 --- a/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java +++ b/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java @@ -12,6 +12,7 @@ import java.util.concurrent.TimeUnit; import com.cedarsoftware.util.LRUCache; +import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.MethodSource; @@ -36,6 +37,13 @@ void setUp(LRUCache.StrategyType strategyType) { lruCache = new LRUCache<>(3, strategyType); } + @AfterEach + void tearDown() { + if (lruCache != null) { + lruCache.shutdown(); + } + } + @ParameterizedTest @MethodSource("strategies") void testPutAndGet(LRUCache.StrategyType strategy) { @@ -429,7 +437,7 @@ void testCacheBlast(LRUCache.StrategyType strategy) { } try { Thread.sleep(100); - System.out.println("Cache size: " + lruCache.size()); + System.out.println(strategy + " cache size: " + lruCache.size()); } catch (InterruptedException ignored) { } } @@ -489,6 +497,6 @@ void testSpeed(LRUCache.StrategyType strategy) { cache.put(i, true); } long endTime = System.currentTimeMillis(); - System.out.println("Speed: " + (endTime - startTime)); + System.out.println(strategy + " speed: " + (endTime - startTime) + "ms"); } }