com.cedarsoftware.util.cache.ThreadedLRUCacheStrategy Maven / Gradle / Ivy
package com.cedarsoftware.util.cache;
import java.util.AbstractMap;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.lang.ref.WeakReference;
import com.cedarsoftware.util.ConcurrentHashMapNullSafe;
import com.cedarsoftware.util.ConcurrentSet;
/**
* This class provides a thread-safe Least Recently Used (LRU) cache API that evicts the least recently used items
* once a threshold is met. It implements the Map
interface for convenience.
*
* The Threaded strategy allows for O(1) access for get(), put(), and remove() without blocking. It uses a ConcurrentHashMapNullSafe
* internally. To ensure that the capacity is honored, whenever put() is called, a scheduled cleanup task is triggered
* to remove the least recently used items if the cache exceeds the capacity.
*
* LRUCache supports null
for both key and value.
*
* Note: This implementation uses a shared scheduler for all cache instances to optimize resource usage.
*
* @param the type of keys maintained by this cache
* @param the type of mapped values
*
* @author John DeRegnaucourt ([email protected])
*
* 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 ThreadedLRUCacheStrategy implements Map {
private final long cleanupDelayMillis;
private final int capacity;
private final ConcurrentMap> cache;
private final AtomicBoolean cleanupScheduled = new AtomicBoolean(false);
// Shared ScheduledExecutorService for all cache instances
private static final ScheduledExecutorService scheduler = Executors.newSingleThreadScheduledExecutor();
/**
* Inner class representing a cache node with a key, value, and timestamp for LRU tracking.
*/
private static class Node {
final K key;
volatile V value;
volatile long timestamp;
Node(K key, V value) {
this.key = key;
this.value = value;
this.timestamp = System.nanoTime();
}
void updateTimestamp() {
this.timestamp = System.nanoTime();
}
}
/**
* Inner class for the purging task.
* Uses a WeakReference to avoid preventing garbage collection of cache instances.
*/
private static class PurgeTask implements Runnable {
private final WeakReference> cacheRef;
PurgeTask(WeakReference> cacheRef) {
this.cacheRef = cacheRef;
}
@Override
public void run() {
ThreadedLRUCacheStrategy cache = cacheRef.get();
if (cache != null) {
cache.cleanup();
}
// If cache is null, it has been garbage collected; no action needed
}
}
/**
* Create an LRUCache with the maximum capacity of 'capacity.'
* The cleanup task is scheduled to run after 'cleanupDelayMillis' milliseconds.
*
* @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 ThreadedLRUCacheStrategy(int capacity, int cleanupDelayMillis) {
if (capacity < 1) {
throw new IllegalArgumentException("Capacity must be at least 1.");
}
if (cleanupDelayMillis < 10) {
throw new IllegalArgumentException("cleanupDelayMillis must be at least 10 milliseconds.");
}
this.capacity = capacity;
this.cache = new ConcurrentHashMapNullSafe<>(capacity);
this.cleanupDelayMillis = cleanupDelayMillis;
// Schedule the purging task for this cache
schedulePurgeTask();
}
/**
* Schedules the purging task for this cache using the shared scheduler.
*/
private void schedulePurgeTask() {
WeakReference> cacheRef = new WeakReference<>(this);
PurgeTask purgeTask = new PurgeTask<>(cacheRef);
scheduler.scheduleAtFixedRate(purgeTask, cleanupDelayMillis, cleanupDelayMillis, TimeUnit.MILLISECONDS);
}
/**
* Cleanup method that removes least recently used entries to maintain the capacity.
*/
private void cleanup() {
int size = cache.size();
if (size > capacity) {
int nodesToRemove = size - capacity;
Node[] nodes = cache.values().toArray(new Node[0]);
Arrays.sort(nodes, Comparator.comparingLong(node -> node.timestamp));
for (int i = 0; i < nodesToRemove; i++) {
Node node = nodes[i];
cache.remove(node.key, node);
}
cleanupScheduled.set(false); // Reset the flag after cleanup
// Check if another cleanup is needed after the current one
if (cache.size() > capacity) {
scheduleImmediateCleanup();
}
}
}
/**
* Schedules an immediate cleanup if not already scheduled.
*/
private void scheduleImmediateCleanup() {
if (cleanupScheduled.compareAndSet(false, true)) {
scheduler.schedule(this::cleanup, cleanupDelayMillis, TimeUnit.MILLISECONDS);
}
}
@Override
public V get(Object key) {
Node node = cache.get(key);
if (node != null) {
node.updateTimestamp();
return node.value;
}
return null;
}
@Override
public V put(K key, V value) {
Node newNode = new Node<>(key, value);
Node oldNode = cache.put(key, newNode);
if (oldNode != null) {
newNode.updateTimestamp();
return oldNode.value;
} else if (size() > capacity) {
scheduleImmediateCleanup();
}
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) {
Node node = cache.remove(key);
if (node != null) {
return node.value;
}
return null;
}
@Override
public void clear() {
cache.clear();
}
@Override
public int size() {
return cache.size();
}
@Override
public boolean containsKey(Object key) {
return cache.containsKey(key);
}
@Override
public boolean containsValue(Object value) {
for (Node node : cache.values()) {
if (Objects.equals(node.value, value)) {
return true;
}
}
return false;
}
@Override
public Set> entrySet() {
Set> entrySet = new ConcurrentSet<>();
for (Node node : cache.values()) {
entrySet.add(new AbstractMap.SimpleEntry<>(node.key, node.value));
}
return Collections.unmodifiableSet(entrySet);
}
@Override
public Set keySet() {
Set keySet = new ConcurrentSet<>();
for (Node node : cache.values()) {
keySet.add(node.key);
}
return Collections.unmodifiableSet(keySet);
}
@Override
public Collection values() {
Collection values = new ArrayList<>();
for (Node node : cache.values()) {
values.add(node.value);
}
return Collections.unmodifiableCollection(values);
}
@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());
}
@Override
public int hashCode() {
int hashCode = 1;
for (Node node : cache.values()) {
Object key = node.key;
Object value = node.value;
hashCode = 31 * hashCode + (key == null ? 0 : key.hashCode());
hashCode = 31 * hashCode + (value == null ? 0 : value.hashCode());
}
return hashCode;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("{");
Iterator> it = entrySet().iterator();
while (it.hasNext()) {
Map.Entry entry = it.next();
// Format and append the key
sb.append(formatElement(entry.getKey()));
sb.append("=");
// Format and append the value
sb.append(formatElement(entry.getValue()));
// Append comma and space if not the last entry
if (it.hasNext()) {
sb.append(", ");
}
}
sb.append("}");
return sb.toString();
}
/**
* Helper method to format an element by checking for self-references.
*
* @param element The element to format.
* @return A string representation of the element, replacing self-references with a placeholder.
*/
private String formatElement(Object element) {
if (element == this) {
return "(this Map)";
}
return String.valueOf(element);
}
/**
* Shuts down the shared scheduler. Call this method when your application is terminating.
*/
public static void shutdown() {
scheduler.shutdown();
try {
if (!scheduler.awaitTermination(5, TimeUnit.SECONDS)) {
scheduler.shutdownNow();
}
} catch (InterruptedException e) {
scheduler.shutdownNow();
Thread.currentThread().interrupt();
}
}
}