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

com.pippsford.util.LRUCache Maven / Gradle / Ivy

The newest version!
package com.pippsford.util;

import java.util.ArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.ForkJoinTask;
import java.util.function.Function;

/**
 * A concurrent cache which uses a Least-Recently-Used eviction algorithm.
 *
 * @author Simon Greatrix on 2019-03-07.
 */
public class LRUCache {

  /** Default concurrency. Should be a prime number. */
  private static final int DEFAULT_CONCURRENCY = 17;



  static class LRU {

    final K key;

    final V value;

    LRU next;

    LRU previous;


    LRU() {
      key = null;
      value = null;
      previous = this;
      next = this;
    }


    @SuppressWarnings("squid:S2445")
    LRU(LRU head, K key, V value) {
      this.key = key;
      this.value = value;

      next = head.next;
      next.previous = this;

      previous = head;
      head.next = this;
    }


    @SuppressWarnings("squid:S2445")
    void remove() {
      previous.next = next;
      next.previous = previous;

      next = null;
      previous = null;
    }


    @SuppressWarnings("squid:S2445")
    void touch(LRU head) {
      if (head == previous) {
        return;
      }
      previous.next = next;
      next.previous = previous;

      next = head.next;
      next.previous = this;

      previous = head;
      head.next = this;
    }

  }



  class Segment {

    final LRU head;

    int size;


    Segment() {
      size = 0;
      head = new LRU<>();
    }


    Segment(Segment old) {
      size = old.size;
      head = new LRU<>();
      LRU oldHead = old.head;
      LRU oldEntry = oldHead.previous;
      while (oldEntry != oldHead) {
        LRU newEntry = new LRU<>(head, oldEntry.key, oldEntry.value);
        map.put(newEntry.key, newEntry);
        oldEntry = oldEntry.previous;
      }
    }


    synchronized V get(K key, Function valueSource) {
      LRU lru = map.get(key);
      if (lru != null) {
        lru.touch(head);
        return lru.value;
      }

      V value = valueSource.apply(key);
      put(key, value);
      return value;
    }


    synchronized void put(K key, V value) {
      LRU newLRU = new LRU<>(head, key, value);
      LRU oldLRU = map.put(key, newLRU);
      if (oldLRU != null) {
        // size has not changed
        oldLRU.remove();
        return;
      }

      size++;
      if (size <= maxSegment) {
        // size is OK
        return;
      }

      // Need to remove LRU
      LRU leastUsed = head.previous;
      leastUsed.remove();
      map.remove(leastUsed.key);
    }


    synchronized void remove(K key) {
      LRU current = map.remove(key);
      if (current == null) {
        // was not present
        return;
      }

      // Size is reduced
      current.remove();
      size--;
    }

  }



  private final Function baseValueSource;

  private final int concurrency;

  private final ConcurrentHashMap> map;

  private final int maxSegment;

  private final int maxSize;

  private final ArrayList segments;


  public LRUCache(int maxSize, Function valueSource) {
    this(DEFAULT_CONCURRENCY, maxSize, valueSource);
  }


  /**
   * New instance.
   *
   * @param concurrency maximum supported concurrency
   * @param maxSize     maximum number of entries in cache
   * @param valueSource source of values for cache
   */
  public LRUCache(int concurrency, int maxSize, Function valueSource) {
    map = new ConcurrentHashMap<>();
    this.maxSize = maxSize;
    maxSegment = Math.max(2, (int) Math.ceil((double) maxSize / concurrency));
    this.concurrency = concurrency;
    baseValueSource = valueSource;
    segments = new ArrayList<>(concurrency);
    for (int i = 0; i < concurrency; i++) {
      segments.add(i, new Segment());
    }
  }


  /**
   * Create a new instance with the same initial data as an existing instance.
   *
   * @param source      the existing instance
   * @param valueSource the source for new values
   */
  public LRUCache(LRUCache source, Function valueSource) {
    map = new ConcurrentHashMap<>(source.map.size());
    concurrency = source.concurrency;
    maxSize = source.maxSize;
    maxSegment = source.maxSegment;
    baseValueSource = valueSource;

    ArrayList> tasks = new ArrayList<>(concurrency);
    for (int i = 0; i < concurrency; i++) {
      final Segment oldSegment = source.segments.get(i);
      tasks.add(ForkJoinPool.commonPool().submit(() -> new Segment(oldSegment)));
    }

    segments = new ArrayList<>(concurrency);
    for (int i = 0; i < concurrency; i++) {
      segments.add(i, tasks.get(i).join());
    }
  }


  public V get(K key) {
    return getSegment(key).get(key, baseValueSource);
  }


  /**
   * Get a value from this cache, using the provided function to compute a value if none is present.
   *
   * @param key         the key to look up
   * @param valueSource the function to calculate a value
   * @param         the value's type
   *
   * @return the value
   */
  public  V2 get(K key, Function valueSource) {
    @SuppressWarnings("unchecked")
    V2 v2 = (V2) getSegment(key).get(key, valueSource);
    return v2;
  }


  Segment getSegment(K key) {
    return segments.get((key.hashCode() & 0x7fff_ffff) % concurrency);
  }


  public void put(K key, V value) {
    getSegment(key).put(key, value);
  }


  public void remove(K key) {
    getSegment(key).remove(key);
  }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy