You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

SimpleLruCache.java 6.2KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227
  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. }