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

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

The newest version!
package com.cedarsoftware.util.cache;

import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

import com.cedarsoftware.util.ConcurrentHashMapNullSafe;

/**
 * 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 Locking strategy allows for O(1) access for get(), put(), and remove(). For put(), remove(), and many other * methods, a write-lock is obtained. For get(), it attempts to lock but does not lock unless it can obtain it right away. * This 'try-lock' approach ensures that the get() API is never blocking, but it also means that the LRU order is not * perfectly maintained under heavy load. *

* LRUCache supports null for both key and value. * @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 LockingLRUCacheStrategy implements Map { private final int capacity; private final ConcurrentHashMapNullSafe> 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; } } /** * Constructs a new LRU cache with the specified maximum capacity. * * @param capacity the maximum number of entries the cache can hold * @throws IllegalArgumentException if capacity is less than 1 */ public LockingLRUCacheStrategy(int capacity) { if (capacity < 1) { throw new IllegalArgumentException("Capacity must be at least 1."); } this.capacity = capacity; this.cache = new ConcurrentHashMapNullSafe<>(capacity); this.head = new Node<>(null, null); this.tail = new Node<>(null, null); head.next = tail; tail.prev = head; } /** * Moves the specified node to the head of the doubly linked list. * This operation must be performed under a lock. * * @param node the node to be moved to the head */ private void moveToHead(Node node) { if (node.prev == null || node.next == null) { // Node has been evicted; skip reordering return; } removeNode(node); addToHead(node); } /** * Adds a node to the head of the doubly linked list. * This operation must be performed under a lock. * * @param node the node to be added to the head */ private void addToHead(Node node) { node.next = head.next; node.next.prev = node; head.next = node; node.prev = head; } /** * Removes a node from the doubly linked list. * This operation must be performed under a lock. * * @param node the node to be removed */ private void removeNode(Node node) { if (node.prev != null && node.next != null) { node.prev.next = node.next; node.next.prev = node.prev; } } /** * Removes and returns the least recently used node from the tail of the list. * This operation must be performed under a lock. * * @return the removed node, or null if the list is empty */ private Node removeTail() { Node node = tail.prev; if (node != head) { removeNode(node); node.prev = null; // Null out links to avoid GC nepotism node.next = null; // Null out links to avoid GC nepotism } return node; } /** * @return the maximum number of entries in the cache. */ public int getCapacity() { return capacity; } /** * Returns the value associated with the specified key in this cache. * If the key exists, attempts to move it to the front of the LRU list * using a non-blocking try-lock approach. * * @param key the key whose associated value is to be returned * @return the value associated with the specified key, or null if no mapping exists */ @Override public V get(Object key) { Node node = cache.get(key); if (node == null) { return null; } // Ben Manes suggestion - use exclusive 'try-lock' if (lock.tryLock()) { try { moveToHead(node); } finally { lock.unlock(); } } return node.value; } /** * Associates the specified value with the specified key in this cache. * If the cache previously contained a mapping for the key, the old value * is replaced and moved to the front of the LRU list. If the cache is at * capacity, removes the least recently used item before adding the new item. * * @param key the key with which the specified value is to be associated * @param value the value to be associated with the specified key * @return the previous value associated with key, or null if there was no mapping */ public V put(K key, V value) { lock.lock(); try { Node node = cache.get(key); if (node != null) { V oldValue = node.value; node.value = value; moveToHead(node); return oldValue; } else { Node newNode = new Node<>(key, value); cache.put(key, newNode); addToHead(newNode); if (cache.size() > capacity) { Node tailToRemove = removeTail(); cache.remove(tailToRemove.key); } return null; } } finally { lock.unlock(); } } /** * Copies all mappings from the specified map to this cache. * These operations will be performed atomically under a single lock. * * @param m mappings to be stored in this cache * @throws NullPointerException if the specified map is null */ public void putAll(Map m) { lock.lock(); try { for (Map.Entry entry : m.entrySet()) { put(entry.getKey(), entry.getValue()); } } finally { lock.unlock(); } } /** * Removes the mapping for the specified key from this cache if present. * * @param key key whose mapping is to be removed from the cache * @return the previous value associated with key, or null if there was no mapping */ @Override public V remove(Object key) { lock.lock(); try { Node node = cache.remove(key); if (node != null) { removeNode(node); return node.value; } return null; } finally { lock.unlock(); } } /** * Removes all mappings from this cache. * The cache will be empty after this call returns. */ @Override public void clear() { lock.lock(); try { head.next = tail; tail.prev = head; cache.clear(); } finally { lock.unlock(); } } /** * Returns the number of key-value mappings in this cache. * * @return the number of key-value mappings in this cache */ @Override public int size() { return cache.size(); } /** * Returns true if this cache contains no key-value mappings. * * @return true if this cache contains no key-value mappings */ @Override public boolean isEmpty() { return size() == 0; } /** * Returns true if this cache contains a mapping for the specified key. * * @param key key whose presence in this cache is to be tested * @return true if this cache contains a mapping for the specified key */ @Override public boolean containsKey(Object key) { return cache.containsKey(key); } /** * Returns true if this cache maps one or more keys to the specified value. * This operation requires a full traversal of the cache under a lock. * * @param value value whose presence in this cache is to be tested * @return true if this cache maps one or more keys to the specified value */ @Override public boolean containsValue(Object value) { lock.lock(); try { for (Node node = head.next; node != tail; node = node.next) { if (Objects.equals(node.value, value)) { return true; } } return false; } finally { lock.unlock(); } } /** * Returns a Set view of the mappings contained in this cache. *

* The returned set is a snapshot of the cache contents at the time * of the call. Modifying the set or its iterator does not affect the * underlying cache. Iterator removal operates only on the snapshot. * The snapshot preserves LRU ordering via a temporary {@link LinkedHashMap}. * This operation requires a full traversal under a lock. * * @return a snapshot set of the mappings contained in this cache */ @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, node.value); } return map.entrySet(); } finally { lock.unlock(); } } /** * Returns a Set view of the keys contained in this cache. *

* Like {@link #entrySet()}, this method returns a snapshot. The set is * independent of the cache and retains the current LRU ordering. Removing * elements from the returned set does not remove them from the cache. * This operation requires a full traversal under a lock. * * @return a snapshot set of the keys contained in this cache */ @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, node.value); } return map.keySet(); } finally { lock.unlock(); } } /** * Returns a Collection view of the values contained in this cache. *

* The collection is a snapshot with values ordered from most to least * recently used. Changes to the returned collection or its iterator do not * affect the cache. Iterator removal only updates the snapshot. * This operation requires a full traversal under a lock. * * @return a snapshot collection of the values contained in this cache */ @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, node.value); } return map.values(); } finally { lock.unlock(); } } /** * Compares the specified object with this cache for equality. * Returns true if the given object is also a map and the two maps * represent the same mappings. * * @param o object to be compared for equality with this cache * @return true if the specified object is equal to this cache */ @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()); } /** * Returns a string representation of this cache. * The string representation consists of a list of key-value mappings * in LRU order (most recently used first) enclosed in braces ("{}"). * Adjacent mappings are separated by the characters ", ". * * @return a string representation of this cache */ @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(formatElement(node.key)) .append("=") .append(formatElement(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(); } } /** * Helper method to format an element, replacing self-references with a placeholder. * * @param element The element to format. * @return The string representation of the element, or a placeholder if it's a self-reference. */ private String formatElement(Object element) { if (element == this) { return "(this Collection)"; } return String.valueOf(element); } /** * Returns the hash code value for this cache. * The hash code is computed by iterating over all entries in LRU order * and combining their hash codes. * * @return the hash code value for this cache */ @Override public int hashCode() { lock.lock(); try { int hashCode = 1; for (Node node = head.next; node != tail; node = node.next) { 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; } finally { lock.unlock(); } } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy