diff --git a/src/main/java/com/cedarsoftware/util/LRUCache.java b/src/main/java/com/cedarsoftware/util/LRUCache.java
index 621f4e335..9e326d614 100644
--- a/src/main/java/com/cedarsoftware/util/LRUCache.java
+++ b/src/main/java/com/cedarsoftware/util/LRUCache.java
@@ -1,242 +1,118 @@
package com.cedarsoftware.util;
-import java.util.AbstractMap;
-import java.util.LinkedHashMap;
+import java.util.Collection;
import java.util.Map;
import java.util.Set;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
-
-/**
- * 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.
- *
- * LRUCache supports null for key or value.
- *
- * @author John DeRegnaucourt (jdereg@gmail.com)
- *
- * Copyright (c) Cedar Software LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * License
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-public class LRUCache extends AbstractMap implements Map {
- private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values
- private final int capacity;
- private final ConcurrentHashMap> cache;
- private final Node head;
- private final Node tail;
- private final Lock lock = new ReentrantLock();
-
- private static class Node {
- K key;
- V value;
- Node prev;
- Node next;
-
- Node(K key, V value) {
- this.key = key;
- this.value = value;
- }
- }
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.ForkJoinPool;
+import java.util.concurrent.ScheduledExecutorService;
- public LRUCache(int capacity) {
- this.capacity = capacity;
- this.cache = new ConcurrentHashMap<>(capacity);
- this.head = new Node<>(null, null);
- this.tail = new Node<>(null, null);
- head.next = tail;
- tail.prev = head;
- }
+import com.cedarsoftware.util.cache.LockingLRUCacheStrategy;
+import com.cedarsoftware.util.cache.ThreadedLRUCacheStrategy;
- private void moveToHead(Node node) {
- removeNode(node);
- addToHead(node);
- }
+public class LRUCache implements Map {
+ private final Map strategy;
- private void addToHead(Node node) {
- node.next = head.next;
- node.next.prev = node;
- head.next = node;
- node.prev = head;
+ public enum StrategyType {
+ THREADED,
+ LOCKING
}
- private void removeNode(Node node) {
- node.prev.next = node.next;
- node.next.prev = node.prev;
+ public LRUCache(int capacity, StrategyType strategyType) {
+ this(capacity, strategyType, 10, null, null);
}
- private Node removeTail() {
- Node node = tail.prev;
- removeNode(node);
- return node;
+ public LRUCache(int capacity, StrategyType strategyType, int cleanupDelayMillis, ScheduledExecutorService scheduler, ForkJoinPool cleanupPool) {
+ switch (strategyType) {
+ case THREADED:
+ this.strategy = new ThreadedLRUCacheStrategy<>(capacity, cleanupDelayMillis, scheduler, cleanupPool);
+ break;
+ case LOCKING:
+ this.strategy = new LockingLRUCacheStrategy<>(capacity);
+ break;
+ default:
+ throw new IllegalArgumentException("Unknown strategy type");
+ }
}
@Override
public V get(Object key) {
- Object cacheKey = toCacheItem(key);
- Node node = cache.get(cacheKey);
- if (node == null) {
- return null;
- }
- if (lock.tryLock()) {
- try {
- moveToHead(node);
- } finally {
- lock.unlock();
- }
- }
- return fromCacheItem(node.value);
+ return strategy.get(key);
}
- @SuppressWarnings("unchecked")
@Override
public V put(K key, V value) {
- Object cacheKey = toCacheItem(key);
- Object cacheValue = toCacheItem(value);
- lock.lock();
- try {
- Node node = cache.get(cacheKey);
- if (node != null) {
- node.value = (V)cacheValue;
- moveToHead(node);
- return fromCacheItem(node.value);
- } else {
- Node newNode = new Node<>(key, (V)cacheValue);
- cache.put(cacheKey, newNode);
- addToHead(newNode);
- if (cache.size() > capacity) {
- Node tail = removeTail();
- cache.remove(toCacheItem(tail.key));
- }
- return null;
- }
- } finally {
- lock.unlock();
- }
+ return strategy.put(key, value);
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ strategy.putAll(m);
}
@Override
public V remove(Object key) {
- Object cacheKey = toCacheItem(key);
- lock.lock();
- try {
- Node node = cache.remove(cacheKey);
- if (node != null) {
- removeNode(node);
- return fromCacheItem(node.value);
- }
- return null;
- } finally {
- lock.unlock();
- }
+ return strategy.remove((K)key);
}
@Override
public void clear() {
- lock.lock();
- try {
- head.next = tail;
- tail.prev = head;
- cache.clear();
- } finally {
- lock.unlock();
- }
+ strategy.clear();
}
@Override
public int size() {
- return cache.size();
+ return strategy.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return strategy.isEmpty();
}
@Override
public boolean containsKey(Object key) {
- return cache.containsKey(toCacheItem(key));
+ return strategy.containsKey((K)key);
}
@Override
public boolean containsValue(Object value) {
- Object cacheValue = toCacheItem(value);
- lock.lock();
- try {
- for (Node node = head.next; node != tail; node = node.next) {
- if (node.value.equals(cacheValue)) {
- return true;
- }
- }
- return false;
- } finally {
- lock.unlock();
- }
+ return strategy.containsValue((V)value);
}
@Override
public Set> entrySet() {
- lock.lock();
- try {
- Map map = new LinkedHashMap<>();
- for (Node node = head.next; node != tail; node = node.next) {
- map.put(node.key, fromCacheItem(node.value));
- }
- return map.entrySet();
- } finally {
- lock.unlock();
- }
+ return strategy.entrySet();
+ }
+
+ @Override
+ public Set keySet() {
+ return strategy.keySet();
+ }
+
+ @Override
+ public Collection values() {
+ return strategy.values();
}
- @SuppressWarnings("unchecked")
@Override
public String toString() {
- lock.lock();
- try {
- StringBuilder sb = new StringBuilder();
- sb.append("{");
- for (Node node = head.next; node != tail; node = node.next) {
- sb.append((K) fromCacheItem(node.key)).append("=").append((V) fromCacheItem(node.value)).append(", ");
- }
- if (sb.length() > 1) {
- sb.setLength(sb.length() - 2); // Remove trailing comma and space
- }
- sb.append("}");
- return sb.toString();
- } finally {
- lock.unlock();
- }
+ return strategy.toString();
}
@Override
public int hashCode() {
- lock.lock();
- try {
- int hashCode = 1;
- for (Node node = head.next; node != tail; node = node.next) {
- Object key = fromCacheItem(node.key);
- Object value = fromCacheItem(node.value);
- hashCode = 31 * hashCode + (key == null ? 0 : key.hashCode());
- hashCode = 31 * hashCode + (value == null ? 0 : value.hashCode());
- }
- return hashCode;
- } finally {
- lock.unlock();
- }
+ return strategy.hashCode();
}
- private Object toCacheItem(Object item) {
- return item == null ? NULL_ITEM : item;
+ @Override
+ public boolean equals(Object obj) {
+ return strategy.equals(obj);
}
- @SuppressWarnings("unchecked")
- private T fromCacheItem(Object cacheItem) {
- return cacheItem == NULL_ITEM ? null : (T) cacheItem;
+ public void shutdown() {
+ if (strategy instanceof ThreadedLRUCacheStrategy) {
+ ((ThreadedLRUCacheStrategy) strategy).shutdown();
+ }
}
}
diff --git a/src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java b/src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java
new file mode 100644
index 000000000..8f61c30df
--- /dev/null
+++ b/src/main/java/com/cedarsoftware/util/cache/LockingLRUCacheStrategy.java
@@ -0,0 +1,295 @@
+package com.cedarsoftware.util.cache;
+
+import java.util.Collection;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+/**
+ * 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.
+ *
+ * LRUCache supports null for key or value.
+ *
+ * @author John DeRegnaucourt (jdereg@gmail.com)
+ *
+ * Copyright (c) Cedar Software LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * License
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+public class LockingLRUCacheStrategy implements Map {
+ private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values
+ private final int capacity;
+ private final ConcurrentHashMap> cache;
+ private final Node head;
+ private final Node tail;
+ private final Lock lock = new ReentrantLock();
+
+ private static class Node {
+ K key;
+ V value;
+ Node prev;
+ Node next;
+
+ Node(K key, V value) {
+ this.key = key;
+ this.value = value;
+ }
+ }
+
+ public LockingLRUCacheStrategy(int capacity) {
+ this.capacity = capacity;
+ this.cache = new ConcurrentHashMap<>(capacity);
+ this.head = new Node<>(null, null);
+ this.tail = new Node<>(null, null);
+ head.next = tail;
+ tail.prev = head;
+ }
+
+ private void moveToHead(Node node) {
+ removeNode(node);
+ addToHead(node);
+ }
+
+ private void addToHead(Node node) {
+ node.next = head.next;
+ node.next.prev = node;
+ head.next = node;
+ node.prev = head;
+ }
+
+ private void removeNode(Node node) {
+ node.prev.next = node.next;
+ node.next.prev = node.prev;
+ }
+
+ private Node removeTail() {
+ Node node = tail.prev;
+ removeNode(node);
+ return node;
+ }
+
+ @Override
+ public V get(Object key) {
+ Object cacheKey = toCacheItem(key);
+ Node node = cache.get(cacheKey);
+ if (node == null) {
+ return null;
+ }
+ if (lock.tryLock()) {
+ try {
+ moveToHead(node);
+ } finally {
+ lock.unlock();
+ }
+ }
+ return fromCacheItem(node.value);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public V put(K key, V value) {
+ Object cacheKey = toCacheItem(key);
+ Object cacheValue = toCacheItem(value);
+ lock.lock();
+ try {
+ Node node = cache.get(cacheKey);
+ if (node != null) {
+ node.value = (V) cacheValue;
+ moveToHead(node);
+ return fromCacheItem(node.value);
+ } else {
+ Node newNode = new Node<>(key, (V) cacheValue);
+ cache.put(cacheKey, newNode);
+ addToHead(newNode);
+ if (cache.size() > capacity) {
+ Node tail = removeTail();
+ cache.remove(toCacheItem(tail.key));
+ }
+ return null;
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ lock.lock();
+ try {
+ for (Map.Entry extends K, ? extends V> entry : m.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public V remove(Object key) {
+ Object cacheKey = toCacheItem(key);
+ lock.lock();
+ try {
+ Node node = cache.remove(cacheKey);
+ if (node != null) {
+ removeNode(node);
+ return fromCacheItem(node.value);
+ }
+ return null;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public void clear() {
+ lock.lock();
+ try {
+ head.next = tail;
+ tail.prev = head;
+ cache.clear();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public int size() {
+ return cache.size();
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return size() == 0;
+ }
+
+ @Override
+ public boolean containsKey(Object key) {
+ return cache.containsKey(toCacheItem(key));
+ }
+
+ @Override
+ public boolean containsValue(Object value) {
+ Object cacheValue = toCacheItem(value);
+ lock.lock();
+ try {
+ for (Node node = head.next; node != tail; node = node.next) {
+ if (node.value.equals(cacheValue)) {
+ return true;
+ }
+ }
+ return false;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public Set> entrySet() {
+ lock.lock();
+ try {
+ Map map = new LinkedHashMap<>();
+ for (Node node = head.next; node != tail; node = node.next) {
+ map.put(node.key, fromCacheItem(node.value));
+ }
+ return map.entrySet();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public Set keySet() {
+ lock.lock();
+ try {
+ Map map = new LinkedHashMap<>();
+ for (Node node = head.next; node != tail; node = node.next) {
+ map.put(node.key, fromCacheItem(node.value));
+ }
+ return map.keySet();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public Collection values() {
+ lock.lock();
+ try {
+ Map map = new LinkedHashMap<>();
+ for (Node node = head.next; node != tail; node = node.next) {
+ map.put(node.key, fromCacheItem(node.value));
+ }
+ return map.values();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof Map)) return false;
+ Map, ?> other = (Map, ?>) o;
+ return entrySet().equals(other.entrySet());
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public String toString() {
+ lock.lock();
+ try {
+ StringBuilder sb = new StringBuilder();
+ sb.append("{");
+ for (Node node = head.next; node != tail; node = node.next) {
+ sb.append((K) fromCacheItem(node.key)).append("=").append((V) fromCacheItem(node.value)).append(", ");
+ }
+ if (sb.length() > 1) {
+ sb.setLength(sb.length() - 2); // Remove trailing comma and space
+ }
+ sb.append("}");
+ return sb.toString();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ lock.lock();
+ try {
+ int hashCode = 1;
+ for (Node node = head.next; node != tail; node = node.next) {
+ Object key = fromCacheItem(node.key);
+ Object value = fromCacheItem(node.value);
+ hashCode = 31 * hashCode + (key == null ? 0 : key.hashCode());
+ hashCode = 31 * hashCode + (value == null ? 0 : value.hashCode());
+ }
+ return hashCode;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private Object toCacheItem(Object item) {
+ return item == null ? NULL_ITEM : item;
+ }
+
+ @SuppressWarnings("unchecked")
+ private T fromCacheItem(Object cacheItem) {
+ return cacheItem == NULL_ITEM ? null : (T) cacheItem;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/cedarsoftware/util/LRUCache2.java b/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java
similarity index 82%
rename from src/main/java/com/cedarsoftware/util/LRUCache2.java
rename to src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java
index aae253364..0ceb8540a 100644
--- a/src/main/java/com/cedarsoftware/util/LRUCache2.java
+++ b/src/main/java/com/cedarsoftware/util/cache/ThreadedLRUCacheStrategy.java
@@ -1,4 +1,4 @@
-package com.cedarsoftware.util;
+package com.cedarsoftware.util.cache;
import java.util.AbstractMap;
import java.util.ArrayList;
@@ -44,15 +44,14 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-public class LRUCache2 extends AbstractMap implements Map {
- private static final ScheduledExecutorService defaultScheduler = Executors.newScheduledThreadPool(1);
+public class ThreadedLRUCacheStrategy implements Map {
private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values
private final long cleanupDelayMillis;
private final int capacity;
private final ConcurrentMap> cache;
private final AtomicBoolean cleanupScheduled = new AtomicBoolean(false);
private final ScheduledExecutorService scheduler;
- private final ExecutorService cleanupExecutor;
+ private final ForkJoinPool cleanupPool;
private boolean isDefaultScheduler;
private static class Node {
@@ -71,29 +70,6 @@ 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 and defaults to 10ms.
- * @param capacity int maximum size for the LRU cache.
- */
- public LRUCache2(int capacity) {
- this(capacity, 10, defaultScheduler, ForkJoinPool.commonPool());
- isDefaultScheduler = true;
- }
-
- /**
- * 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.
- * @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).
- */
- public LRUCache2(int capacity, int cleanupDelayMillis) {
- this(capacity, cleanupDelayMillis, defaultScheduler, ForkJoinPool.commonPool());
- isDefaultScheduler = true;
- }
-
/**
* 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
@@ -102,15 +78,27 @@ public LRUCache2(int capacity, int cleanupDelayMillis) {
* @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 cleanupExecutor ExecutorService for executing cleanup tasks.
+ * @param cleanupPool ForkJoinPool for executing cleanup tasks.
*/
- public LRUCache2(int capacity, int cleanupDelayMillis, ScheduledExecutorService scheduler, ExecutorService cleanupExecutor) {
+ 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.capacity = capacity;
this.cache = new ConcurrentHashMap<>(capacity);
this.cleanupDelayMillis = cleanupDelayMillis;
- this.scheduler = scheduler;
- this.cleanupExecutor = cleanupExecutor;
- isDefaultScheduler = false;
}
@SuppressWarnings("unchecked")
@@ -158,6 +146,18 @@ public V put(K key, V value) {
return null;
}
+ @Override
+ public void putAll(Map extends K, ? extends V> m) {
+ for (Map.Entry extends K, ? extends V> entry : m.entrySet()) {
+ put(entry.getKey(), entry.getValue());
+ }
+ }
+
+ @Override
+ public boolean isEmpty() {
+ return cache.isEmpty();
+ }
+
@Override
public V remove(Object key) {
Object cacheKey = toCacheItem(key);
@@ -259,10 +259,11 @@ public String toString() {
// Schedule a delayed cleanup
private void scheduleCleanup() {
if (cleanupScheduled.compareAndSet(false, true)) {
- scheduler.schedule(() -> cleanupExecutor.execute(this::cleanup), cleanupDelayMillis, TimeUnit.MILLISECONDS);
+ scheduler.schedule(() -> ForkJoinPool.commonPool().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;
@@ -278,8 +279,13 @@ private T fromCacheItem(Object cacheItem) {
* Shut down the scheduler if it is the default one.
*/
public void shutdown() {
- if (isDefaultScheduler) {
- scheduler.shutdown();
+ scheduler.shutdown();
+ try {
+ if (!scheduler.awaitTermination(1, TimeUnit.SECONDS)) {
+ scheduler.shutdownNow();
+ }
+ } catch (InterruptedException e) {
+ scheduler.shutdownNow();
}
}
}
\ No newline at end of file
diff --git a/src/test/java/com/cedarsoftware/util/LRUCacheTest.java b/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java
similarity index 56%
rename from src/test/java/com/cedarsoftware/util/LRUCacheTest.java
rename to src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java
index 7bd69081c..d0f0a5be3 100644
--- a/src/test/java/com/cedarsoftware/util/LRUCacheTest.java
+++ b/src/test/java/com/cedarsoftware/util/cache/LRUCacheTest.java
@@ -1,6 +1,8 @@
-package com.cedarsoftware.util;
+package com.cedarsoftware.util.cache;
import java.security.SecureRandom;
+import java.util.Arrays;
+import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
@@ -9,8 +11,9 @@
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.Test;
+import com.cedarsoftware.util.LRUCache;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.MethodSource;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
@@ -18,34 +21,25 @@
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
-/**
- * @author John DeRegnaucourt (jdereg@gmail.com)
- *
- * Copyright (c) Cedar Software LLC
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * License
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
public class LRUCacheTest {
private LRUCache lruCache;
- @BeforeEach
- void setUp() {
- lruCache = new LRUCache<>(3);
+ static Collection strategies() {
+ return Arrays.asList(
+ LRUCache.StrategyType.LOCKING,
+ LRUCache.StrategyType.THREADED
+ );
}
- @Test
- void testPutAndGet() {
+ void setUp(LRUCache.StrategyType strategyType) {
+ lruCache = new LRUCache<>(3, strategyType);
+ }
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testPutAndGet(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
@@ -55,43 +49,47 @@ void testPutAndGet() {
assertEquals("C", lruCache.get(3));
}
- @Test
- void testEvictionPolicy() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testEvictionPolicy(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
lruCache.get(1);
lruCache.put(4, "D");
- // Wait for the background cleanup thread to perform the eviction
long startTime = System.currentTimeMillis();
- long timeout = 5000; // 5 seconds timeout
+ long timeout = 5000;
while (System.currentTimeMillis() - startTime < timeout) {
if (!lruCache.containsKey(2) && lruCache.containsKey(1) && lruCache.containsKey(4)) {
break;
}
try {
- Thread.sleep(100); // Check every 100ms
+ Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
- // Assert the expected cache state
assertNull(lruCache.get(2), "Entry for key 2 should be evicted");
assertEquals("A", lruCache.get(1), "Entry for key 1 should still be present");
assertEquals("D", lruCache.get(4), "Entry for key 4 should be present");
}
-
- @Test
- void testSize() {
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testSize(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
assertEquals(2, lruCache.size());
}
- @Test
- void testIsEmpty() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testIsEmpty(LRUCache.StrategyType strategy) {
+ setUp(strategy);
assertTrue(lruCache.isEmpty());
lruCache.put(1, "A");
@@ -99,32 +97,40 @@ void testIsEmpty() {
assertFalse(lruCache.isEmpty());
}
- @Test
- void testRemove() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testRemove(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.remove(1);
assertNull(lruCache.get(1));
}
- @Test
- void testContainsKey() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testContainsKey(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
assertTrue(lruCache.containsKey(1));
assertFalse(lruCache.containsKey(2));
}
- @Test
- void testContainsValue() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testContainsValue(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
assertTrue(lruCache.containsValue("A"));
assertFalse(lruCache.containsValue("B"));
}
- @Test
- void testKeySet() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testKeySet(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
@@ -132,8 +138,10 @@ void testKeySet() {
assertTrue(lruCache.keySet().contains(2));
}
- @Test
- void testValues() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testValues(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
@@ -141,8 +149,10 @@ void testValues() {
assertTrue(lruCache.values().contains("B"));
}
- @Test
- void testClear() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testClear(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.clear();
@@ -150,8 +160,10 @@ void testClear() {
assertTrue(lruCache.isEmpty());
}
- @Test
- void testPutAll() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testPutAll(LRUCache.StrategyType strategy) {
+ setUp(strategy);
Map map = new LinkedHashMap<>();
map.put(1, "A");
map.put(2, "B");
@@ -161,28 +173,31 @@ void testPutAll() {
assertEquals("B", lruCache.get(2));
}
- @Test
- void testEntrySet() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testEntrySet(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
assertEquals(2, lruCache.entrySet().size());
}
- @Test
- void testPutIfAbsent() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testPutIfAbsent(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.putIfAbsent(1, "A");
lruCache.putIfAbsent(1, "B");
assertEquals("A", lruCache.get(1));
}
- @Test
- void testSmallSizes()
- {
- // Testing with different sizes
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testSmallSizes(LRUCache.StrategyType strategy) {
for (int capacity : new int[]{1, 3, 5, 10}) {
- LRUCache cache = new LRUCache<>(capacity);
+ LRUCache cache = new LRUCache<>(capacity, strategy);
for (int i = 0; i < capacity; i++) {
cache.put(i, "Value" + i);
}
@@ -193,17 +208,18 @@ void testSmallSizes()
cache.remove(i);
}
- assert cache.isEmpty();
+ assertTrue(cache.isEmpty());
cache.clear();
}
}
-
- @Test
- void testConcurrency() throws InterruptedException {
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testConcurrency(LRUCache.StrategyType strategy) throws InterruptedException {
+ setUp(strategy);
ExecutorService service = Executors.newFixedThreadPool(3);
- lruCache = new LRUCache<>(10000);
+ lruCache = new LRUCache<>(10000, strategy);
- // Perform a mix of put and get operations from multiple threads
int max = 10000;
int attempts = 0;
Random random = new SecureRandom();
@@ -214,15 +230,11 @@ void testConcurrency() throws InterruptedException {
service.submit(() -> lruCache.put(key, value));
service.submit(() -> lruCache.get(key));
service.submit(() -> lruCache.size());
- service.submit(() -> {
- lruCache.keySet().remove(random.nextInt(max));
- });
- service.submit(() -> {
- lruCache.values().remove("V" + random.nextInt(max));
- });
+ service.submit(() -> lruCache.keySet().remove(random.nextInt(max)));
+ service.submit(() -> lruCache.values().remove("V" + random.nextInt(max)));
final int attemptsCopy = attempts;
service.submit(() -> {
- Iterator i = lruCache.entrySet().iterator();
+ Iterator> i = lruCache.entrySet().iterator();
int walk = random.nextInt(attemptsCopy);
while (i.hasNext() && walk-- > 0) {
i.next();
@@ -240,45 +252,45 @@ void testConcurrency() throws InterruptedException {
assertTrue(service.awaitTermination(1, TimeUnit.MINUTES));
}
- @Test
- public void testConcurrency2() throws InterruptedException {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testConcurrency2(LRUCache.StrategyType strategy) throws InterruptedException {
+ setUp(strategy);
int initialEntries = 100;
- lruCache = new LRUCache<>(initialEntries);
+ lruCache = new LRUCache<>(initialEntries, strategy);
ExecutorService executor = Executors.newFixedThreadPool(10);
- // Add initial entries
for (int i = 0; i < initialEntries; i++) {
lruCache.put(i, "true");
}
SecureRandom random = new SecureRandom();
- // Perform concurrent operations
for (int i = 0; i < 100000; i++) {
final int key = random.nextInt(100);
executor.submit(() -> {
- lruCache.put(key, "true"); // Add
- lruCache.remove(key); // Remove
- lruCache.put(key, "false"); // Update
+ lruCache.put(key, "true");
+ lruCache.remove(key);
+ lruCache.put(key, "false");
});
}
executor.shutdown();
assertTrue(executor.awaitTermination(1, TimeUnit.MINUTES));
- // Check some values to ensure correctness
for (int i = 0; i < initialEntries; i++) {
final int key = i;
assertTrue(lruCache.containsKey(key));
}
- assert lruCache.size() == 100;
assertEquals(initialEntries, lruCache.size());
}
- @Test
- void testEquals() {
- LRUCache cache1 = new LRUCache<>(3);
- LRUCache cache2 = new LRUCache<>(3);
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testEquals(LRUCache.StrategyType strategy) {
+ setUp(strategy);
+ LRUCache cache1 = new LRUCache<>(3, strategy);
+ LRUCache cache2 = new LRUCache<>(3, strategy);
cache1.put(1, "A");
cache1.put(2, "B");
@@ -300,10 +312,12 @@ void testEquals() {
assertTrue(cache1.equals(cache1));
}
- @Test
- void testHashCode() {
- LRUCache cache1 = new LRUCache<>(3);
- LRUCache cache2 = new LRUCache<>(3);
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testHashCode(LRUCache.StrategyType strategy) {
+ setUp(strategy);
+ LRUCache cache1 = new LRUCache<>(3, strategy);
+ LRUCache cache2 = new LRUCache<>(3, strategy);
cache1.put(1, "A");
cache1.put(2, "B");
@@ -319,23 +333,27 @@ void testHashCode() {
assertNotEquals(cache1.hashCode(), cache2.hashCode());
}
- @Test
- void testToString() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testToString(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
- assert lruCache.toString().contains("1=A");
- assert lruCache.toString().contains("2=B");
- assert lruCache.toString().contains("3=C");
+ assertTrue(lruCache.toString().contains("1=A"));
+ assertTrue(lruCache.toString().contains("2=B"));
+ assertTrue(lruCache.toString().contains("3=C"));
- Map cache = new LRUCache(100);
- assert cache.toString().equals("{}");
- assert cache.size() == 0;
+ Map cache = new LRUCache<>(100, strategy);
+ assertEquals("{}", cache.toString());
+ assertEquals(0, cache.size());
}
- @Test
- void testFullCycle() {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testFullCycle(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
lruCache.put(3, "C");
@@ -344,7 +362,7 @@ void testFullCycle() {
lruCache.put(6, "F");
long startTime = System.currentTimeMillis();
- long timeout = 5000; // 5 seconds timeout
+ long timeout = 5000;
while (System.currentTimeMillis() - startTime < timeout) {
if (lruCache.size() == 3 &&
lruCache.containsKey(4) &&
@@ -356,7 +374,7 @@ void testFullCycle() {
break;
}
try {
- Thread.sleep(100); // Check every 100ms
+ Thread.sleep(100);
} catch (InterruptedException ignored) {
}
}
@@ -374,45 +392,43 @@ void testFullCycle() {
lruCache.remove(4);
assertEquals(0, lruCache.size(), "Cache should be empty after removing all elements");
}
-
- @Test
- void testCacheWhenEmpty() {
- // The cache is initially empty, so any get operation should return null
+
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testCacheWhenEmpty(LRUCache.StrategyType strategy) {
+ setUp(strategy);
assertNull(lruCache.get(1));
}
- @Test
- void testCacheClear() {
- // Add elements to the cache
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testCacheClear(LRUCache.StrategyType strategy) {
+ setUp(strategy);
lruCache.put(1, "A");
lruCache.put(2, "B");
-
- // Clear the cache
lruCache.clear();
- // The cache should be empty, so any get operation should return null
assertNull(lruCache.get(1));
assertNull(lruCache.get(2));
}
- @Test
- void testCacheBlast() {
- // Jam 10M items to the cache
- lruCache = new LRUCache<>(1000);
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testCacheBlast(LRUCache.StrategyType strategy) {
+ lruCache = new LRUCache<>(1000, strategy);
for (int i = 0; i < 10000000; i++) {
lruCache.put(i, "" + i);
}
- // Wait until the cache size stabilizes to 1000
int expectedSize = 1000;
long startTime = System.currentTimeMillis();
- long timeout = 10000; // wait up to 10 seconds (will never take this long)
+ long timeout = 10000;
while (System.currentTimeMillis() - startTime < timeout) {
if (lruCache.size() <= expectedSize) {
break;
}
try {
- Thread.sleep(100); // Check every 100ms
+ Thread.sleep(100);
System.out.println("Cache size: " + lruCache.size());
} catch (InterruptedException ignored) {
}
@@ -421,51 +437,55 @@ void testCacheBlast() {
assertEquals(1000, lruCache.size());
}
- @Test
- void testNullValue()
- {
- lruCache = new LRUCache<>(100, 1);
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testNullValue(LRUCache.StrategyType strategy) {
+ setUp(strategy);
+ lruCache = new LRUCache<>(100, strategy);
lruCache.put(1, null);
- assert lruCache.containsKey(1);
- assert lruCache.containsValue(null);
- assert lruCache.toString().contains("1=null");
- assert lruCache.hashCode() != 0;
+ assertTrue(lruCache.containsKey(1));
+ assertTrue(lruCache.containsValue(null));
+ assertTrue(lruCache.toString().contains("1=null"));
+ assertNotEquals(0, lruCache.hashCode());
}
- @Test
- void testNullKey()
- {
- lruCache = new LRUCache<>(100, 1);
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testNullKey(LRUCache.StrategyType strategy) {
+ setUp(strategy);
+ lruCache = new LRUCache<>(100, strategy);
lruCache.put(null, "true");
- assert lruCache.containsKey(null);
- assert lruCache.containsValue("true");
- assert lruCache.toString().contains("null=true");
- assert lruCache.hashCode() != 0;
+ assertTrue(lruCache.containsKey(null));
+ assertTrue(lruCache.containsValue("true"));
+ assertTrue(lruCache.toString().contains("null=true"));
+ assertNotEquals(0, lruCache.hashCode());
}
- @Test
- void testNullKeyValue()
- {
- lruCache = new LRUCache<>(100, 1);
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testNullKeyValue(LRUCache.StrategyType strategy) {
+ setUp(strategy);
+ lruCache = new LRUCache<>(100, strategy);
lruCache.put(null, null);
- assert lruCache.containsKey(null);
- assert lruCache.containsValue(null);
- assert lruCache.toString().contains("null=null");
- assert lruCache.hashCode() != 0;
+ assertTrue(lruCache.containsKey(null));
+ assertTrue(lruCache.containsValue(null));
+ assertTrue(lruCache.toString().contains("null=null"));
+ assertNotEquals(0, lruCache.hashCode());
- LRUCache cache1 = new LRUCache<>(3);
+ LRUCache cache1 = new LRUCache<>(3, strategy);
cache1.put(null, null);
- LRUCache cache2 = new LRUCache<>(3);
+ LRUCache cache2 = new LRUCache<>(3, strategy);
cache2.put(null, null);
- assert cache1.equals(cache2);
+ assertTrue(cache1.equals(cache2));
}
- @Test
- void testSpeed()
- {
+ @ParameterizedTest
+ @MethodSource("strategies")
+ void testSpeed(LRUCache.StrategyType strategy) {
+ setUp(strategy);
long startTime = System.currentTimeMillis();
- LRUCache cache = new LRUCache<>(30000000);
- for (int i = 0; i < 30000000; i++) {
+ LRUCache cache = new LRUCache<>(10000000, strategy);
+ for (int i = 0; i < 10000000; i++) {
cache.put(i, true);
}
long endTime = System.currentTimeMillis();