-
Notifications
You must be signed in to change notification settings - Fork 109
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Adding two LRUCache-ing strategies - locking and threaded.
- Loading branch information
Showing
4 changed files
with
568 additions
and
371 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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(); | ||
} | ||
} | ||
} |
Oops, something went wrong.