Skip to content

Commit

Permalink
Adding two LRUCache-ing strategies - locking and threaded.
Browse files Browse the repository at this point in the history
  • Loading branch information
jdereg committed Jun 23, 2024
1 parent ad97229 commit a3d6b02
Show file tree
Hide file tree
Showing 4 changed files with 568 additions and 371 deletions.
246 changes: 61 additions & 185 deletions src/main/java/com/cedarsoftware/util/LRUCache.java
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* LRUCache supports null for key or value.
* <p>
* @author John DeRegnaucourt ([email protected])
* <br>
* Copyright (c) Cedar Software LLC
* <br><br>
* 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
* <br><br>
* <a href="http://www.apache.org/licenses/LICENSE-2.0">License</a>
* <br><br>
* 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<K, V> extends AbstractMap<K, V> implements Map<K, V> {
private static final Object NULL_ITEM = new Object(); // Sentinel value for null keys and values
private final int capacity;
private final ConcurrentHashMap<Object, Node<K, V>> cache;
private final Node<K, V> head;
private final Node<K, V> tail;
private final Lock lock = new ReentrantLock();

private static class Node<K, V> {
K key;
V value;
Node<K, V> prev;
Node<K, V> 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<K, V> node) {
removeNode(node);
addToHead(node);
}
public class LRUCache<K, V> implements Map<K, V> {
private final Map<K, V> strategy;

private void addToHead(Node<K, V> node) {
node.next = head.next;
node.next.prev = node;
head.next = node;
node.prev = head;
public enum StrategyType {
THREADED,
LOCKING
}

private void removeNode(Node<K, V> 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<K, V> removeTail() {
Node<K, V> 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<K, V> 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<K, V> node = cache.get(cacheKey);
if (node != null) {
node.value = (V)cacheValue;
moveToHead(node);
return fromCacheItem(node.value);
} else {
Node<K, V> newNode = new Node<>(key, (V)cacheValue);
cache.put(cacheKey, newNode);
addToHead(newNode);
if (cache.size() > capacity) {
Node<K, V> 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<K, V> 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<K, V> 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<Map.Entry<K, V>> entrySet() {
lock.lock();
try {
Map<K, V> map = new LinkedHashMap<>();
for (Node<K, V> 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<K> keySet() {
return strategy.keySet();
}

@Override
public Collection<V> values() {
return strategy.values();
}

@SuppressWarnings("unchecked")
@Override
public String toString() {
lock.lock();
try {
StringBuilder sb = new StringBuilder();
sb.append("{");
for (Node<K, V> 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<K, V> 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> T fromCacheItem(Object cacheItem) {
return cacheItem == NULL_ITEM ? null : (T) cacheItem;
public void shutdown() {
if (strategy instanceof ThreadedLRUCacheStrategy) {
((ThreadedLRUCacheStrategy<K, V>) strategy).shutdown();
}
}
}
Loading

0 comments on commit a3d6b02

Please sign in to comment.