All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.cedarsoftware.util.cache.ThreadedLRUCacheStrategy Maven / Gradle / Ivy

There is a newer version: 2.18.0
Show newest version
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 m) { for (Map.Entry 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(); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy