SimpleLruCache.java

  1. /*
  2.  * Copyright (C) 2019, Marc Strapetz <marc.strapetz@syntevo.com>
  3.  * Copyright (C) 2019, Matthias Sohn <matthias.sohn@sap.com> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */
  11. package org.eclipse.jgit.util;

  12. import java.text.MessageFormat;
  13. import java.util.ArrayList;
  14. import java.util.Collections;
  15. import java.util.Comparator;
  16. import java.util.List;
  17. import java.util.Map;
  18. import java.util.concurrent.ConcurrentHashMap;
  19. import java.util.concurrent.locks.Lock;
  20. import java.util.concurrent.locks.ReentrantLock;

  21. import org.eclipse.jgit.annotations.NonNull;
  22. import org.eclipse.jgit.internal.JGitText;

  23. /**
  24.  * Simple limited size cache based on ConcurrentHashMap purging entries in LRU
  25.  * order when reaching size limit
  26.  *
  27.  * @param <K>
  28.  *            the type of keys maintained by this cache
  29.  * @param <V>
  30.  *            the type of mapped values
  31.  *
  32.  * @since 5.1.9
  33.  */
  34. public class SimpleLruCache<K, V> {

  35.     private static class Entry<K, V> {

  36.         private final K key;

  37.         private final V value;

  38.         // pseudo clock timestamp of the last access to this entry
  39.         private volatile long lastAccessed;

  40.         private long lastAccessedSorting;

  41.         Entry(K key, V value, long lastAccessed) {
  42.             this.key = key;
  43.             this.value = value;
  44.             this.lastAccessed = lastAccessed;
  45.         }

  46.         void copyAccessTime() {
  47.             lastAccessedSorting = lastAccessed;
  48.         }

  49.         @SuppressWarnings("nls")
  50.         @Override
  51.         public String toString() {
  52.             return "Entry [lastAccessed=" + lastAccessed + ", key=" + key
  53.                     + ", value=" + value + "]";
  54.         }
  55.     }

  56.     private Lock lock = new ReentrantLock();

  57.     private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>();

  58.     private volatile int maximumSize;

  59.     private int purgeSize;

  60.     // pseudo clock to implement LRU order of access to entries
  61.     private volatile long time = 0L;

  62.     private static void checkPurgeFactor(float purgeFactor) {
  63.         if (purgeFactor <= 0 || purgeFactor >= 1) {
  64.             throw new IllegalArgumentException(
  65.                     MessageFormat.format(JGitText.get().invalidPurgeFactor,
  66.                             Float.valueOf(purgeFactor)));
  67.         }
  68.     }

  69.     private static int purgeSize(int maxSize, float purgeFactor) {
  70.         return (int) ((1 - purgeFactor) * maxSize);
  71.     }

  72.     /**
  73.      * Create a new cache
  74.      *
  75.      * @param maxSize
  76.      *            maximum size of the cache, to reduce need for synchronization
  77.      *            this is not a hard limit. The real size of the cache could be
  78.      *            slightly above this maximum if multiple threads put new values
  79.      *            concurrently
  80.      * @param purgeFactor
  81.      *            when the size of the map reaches maxSize the oldest entries
  82.      *            will be purged to free up some space for new entries,
  83.      *            {@code purgeFactor} is the fraction of {@code maxSize} to
  84.      *            purge when this happens
  85.      */
  86.     public SimpleLruCache(int maxSize, float purgeFactor) {
  87.         checkPurgeFactor(purgeFactor);
  88.         this.maximumSize = maxSize;
  89.         this.purgeSize = purgeSize(maxSize, purgeFactor);
  90.     }

  91.     /**
  92.      * Returns the value to which the specified key is mapped, or {@code null}
  93.      * if this map contains no mapping for the key.
  94.      *
  95.      * <p>
  96.      * More formally, if this cache contains a mapping from a key {@code k} to a
  97.      * value {@code v} such that {@code key.equals(k)}, then this method returns
  98.      * {@code v}; otherwise it returns {@code null}. (There can be at most one
  99.      * such mapping.)
  100.      *
  101.      * @param key
  102.      *            the key
  103.      *
  104.      * @throws NullPointerException
  105.      *             if the specified key is null
  106.      *
  107.      * @return value mapped for this key, or {@code null} if no value is mapped
  108.      */
  109.     @SuppressWarnings("NonAtomicVolatileUpdate")
  110.     public V get(Object key) {
  111.         Entry<K, V> entry = map.get(key);
  112.         if (entry != null) {
  113.             entry.lastAccessed = tick();
  114.             return entry.value;
  115.         }
  116.         return null;
  117.     }

  118.     /**
  119.      * Maps the specified key to the specified value in this cache. Neither the
  120.      * key nor the value can be null.
  121.      *
  122.      * <p>
  123.      * The value can be retrieved by calling the {@code get} method with a key
  124.      * that is equal to the original key.
  125.      *
  126.      * @param key
  127.      *            key with which the specified value is to be associated
  128.      * @param value
  129.      *            value to be associated with the specified key
  130.      * @return the previous value associated with {@code key}, or {@code null}
  131.      *         if there was no mapping for {@code key}
  132.      * @throws NullPointerException
  133.      *             if the specified key or value is null
  134.      */
  135.     @SuppressWarnings("NonAtomicVolatileUpdate")
  136.     public V put(@NonNull K key, @NonNull V value) {
  137.         map.put(key, new Entry<>(key, value, tick()));
  138.         if (map.size() > maximumSize) {
  139.             purge();
  140.         }
  141.         return value;
  142.     }

  143.     @SuppressWarnings("NonAtomicVolatileUpdate")
  144.     private long tick() {
  145.         return ++time;
  146.     }

  147.     /**
  148.      * Returns the current size of this cache
  149.      *
  150.      * @return the number of key-value mappings in this cache
  151.      */
  152.     public int size() {
  153.         return map.size();
  154.     }

  155.     /**
  156.      * Reconfigures the cache. If {@code maxSize} is reduced some entries will
  157.      * be purged.
  158.      *
  159.      * @param maxSize
  160.      *            maximum size of the cache
  161.      *
  162.      * @param purgeFactor
  163.      *            when the size of the map reaches maxSize the oldest entries
  164.      *            will be purged to free up some space for new entries,
  165.      *            {@code purgeFactor} is the fraction of {@code maxSize} to
  166.      *            purge when this happens
  167.      */
  168.     public void configure(int maxSize, float purgeFactor) {
  169.         lock.lock();
  170.         try {
  171.             checkPurgeFactor(purgeFactor);
  172.             this.maximumSize = maxSize;
  173.             this.purgeSize = purgeSize(maxSize, purgeFactor);
  174.             if (map.size() >= maximumSize) {
  175.                 purge();
  176.             }
  177.         } finally {
  178.             lock.unlock();
  179.         }
  180.     }

  181.     private void purge() {
  182.         // don't try to compete if another thread already has the lock
  183.         if (lock.tryLock()) {
  184.             try {
  185.                 List<Entry> entriesToPurge = new ArrayList<>(map.values());
  186.                 // copy access times to avoid other threads interfere with
  187.                 // sorting
  188.                 for (Entry e : entriesToPurge) {
  189.                     e.copyAccessTime();
  190.                 }
  191.                 Collections.sort(entriesToPurge,
  192.                         Comparator.comparingLong(o -> -o.lastAccessedSorting));
  193.                 for (int index = purgeSize; index < entriesToPurge
  194.                         .size(); index++) {
  195.                     map.remove(entriesToPurge.get(index).key);
  196.                 }
  197.             } finally {
  198.                 lock.unlock();
  199.             }
  200.         }
  201.     }
  202. }