javaadvanced

LRU Cache — Thread-Safe Implementation

Implement a thread-safe LRU cache with configurable size, TTL expiry, and statistics tracking.

java
import java.util.*;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.time.Instant;

public class LRUCache<K, V> {
    private final int maxSize;
    private final long ttlMillis;
    private final LinkedHashMap<K, CacheEntry<V>> map;
    private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
    private long hits = 0, misses = 0;

    record CacheEntry<V>(V value, Instant expiresAt) {
        boolean isExpired() { return Instant.now().isAfter(expiresAt); }
    }

    public LRUCache(int maxSize, long ttlMillis) {
        this.maxSize = maxSize;
        this.ttlMillis = ttlMillis;
        this.map = new LinkedHashMap<>(maxSize, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, CacheEntry<V>> eldest) {
                return size() > maxSize || eldest.getValue().isExpired();
            }
        };
    }

    public V get(K key) {
        lock.readLock().lock();
        try {
            CacheEntry<V> entry = map.get(key);
            if (entry == null || entry.isExpired()) {
                misses++;
                if (entry != null) map.remove(key);
                return null;
            }
            hits++;
            return entry.value();
        } finally {
            lock.readLock().unlock();
        }
    }

    public void put(K key, V value) {
        lock.writeLock().lock();
        try {
            map.put(key, new CacheEntry<>(value,
                Instant.now().plusMillis(ttlMillis)));
        } finally {
            lock.writeLock().unlock();
        }
    }

    public V getOrCompute(K key, java.util.function.Function<K, V> compute) {
        V cached = get(key);
        if (cached != null) return cached;
        lock.writeLock().lock();
        try {
            // Double-check after acquiring write lock
            CacheEntry<V> entry = map.get(key);
            if (entry != null && !entry.isExpired()) return entry.value();
            V value = compute.apply(key);
            put(key, value);
            return value;
        } finally {
            lock.writeLock().unlock();
        }
    }

    public void evict(K key) {
        lock.writeLock().lock();
        try { map.remove(key); }
        finally { lock.writeLock().unlock(); }
    }

    public int size() { return map.size(); }

    public String stats() {
        long total = hits + misses;
        double rate = total == 0 ? 0 : (double) hits / total * 100;
        return String.format("hits=%d misses=%d rate=%.1f%% size=%d", hits, misses, rate, size());
    }

    public static void main(String[] args) throws Exception {
        LRUCache<String, String> cache = new LRUCache<>(3, 5000);

        cache.put("a", "Apple");
        cache.put("b", "Banana");
        cache.put("c", "Cherry");
        cache.get("a"); // access "a" (now most recent)
        cache.put("d", "Date"); // evicts "b" (least recently used)

        System.out.println(cache.get("b")); // null
        System.out.println(cache.get("a")); // Apple
        System.out.println(cache.stats());
    }
}

Use Cases

  • Application-level caching without external systems
  • Reducing expensive computation with memoization
  • API response caching with TTL expiry

Tags

Related Snippets

Similar patterns you can reuse in the same workflow.