View Javadoc
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  
13  import java.text.MessageFormat;
14  import java.util.ArrayList;
15  import java.util.Collections;
16  import java.util.Comparator;
17  import java.util.List;
18  import java.util.Map;
19  import java.util.concurrent.ConcurrentHashMap;
20  import java.util.concurrent.locks.Lock;
21  import java.util.concurrent.locks.ReentrantLock;
22  
23  import org.eclipse.jgit.annotations.NonNull;
24  import org.eclipse.jgit.internal.JGitText;
25  
26  /**
27   * Simple limited size cache based on ConcurrentHashMap purging entries in LRU
28   * order when reaching size limit
29   *
30   * @param <K>
31   *            the type of keys maintained by this cache
32   * @param <V>
33   *            the type of mapped values
34   *
35   * @since 5.1.9
36   */
37  public class SimpleLruCache<K, V> {
38  
39  	private static class Entry<K, V> {
40  
41  		private final K key;
42  
43  		private final V value;
44  
45  		// pseudo clock timestamp of the last access to this entry
46  		private volatile long lastAccessed;
47  
48  		private long lastAccessedSorting;
49  
50  		Entry(K key, V value, long lastAccessed) {
51  			this.key = key;
52  			this.value = value;
53  			this.lastAccessed = lastAccessed;
54  		}
55  
56  		void copyAccessTime() {
57  			lastAccessedSorting = lastAccessed;
58  		}
59  
60  		@SuppressWarnings("nls")
61  		@Override
62  		public String toString() {
63  			return "Entry [lastAccessed=" + lastAccessed + ", key=" + key
64  					+ ", value=" + value + "]";
65  		}
66  	}
67  
68  	private Lock lock = new ReentrantLock();
69  
70  	private Map<K, Entry<K,V>> map = new ConcurrentHashMap<>();
71  
72  	private volatile int maximumSize;
73  
74  	private int purgeSize;
75  
76  	// pseudo clock to implement LRU order of access to entries
77  	private volatile long time = 0L;
78  
79  	private static void checkPurgeFactor(float purgeFactor) {
80  		if (purgeFactor <= 0 || purgeFactor >= 1) {
81  			throw new IllegalArgumentException(
82  					MessageFormat.format(JGitText.get().invalidPurgeFactor,
83  							Float.valueOf(purgeFactor)));
84  		}
85  	}
86  
87  	private static int purgeSize(int maxSize, float purgeFactor) {
88  		return (int) ((1 - purgeFactor) * maxSize);
89  	}
90  
91  	/**
92  	 * Create a new cache
93  	 *
94  	 * @param maxSize
95  	 *            maximum size of the cache, to reduce need for synchronization
96  	 *            this is not a hard limit. The real size of the cache could be
97  	 *            slightly above this maximum if multiple threads put new values
98  	 *            concurrently
99  	 * @param purgeFactor
100 	 *            when the size of the map reaches maxSize the oldest entries
101 	 *            will be purged to free up some space for new entries,
102 	 *            {@code purgeFactor} is the fraction of {@code maxSize} to
103 	 *            purge when this happens
104 	 */
105 	public SimpleLruCache(int maxSize, float purgeFactor) {
106 		checkPurgeFactor(purgeFactor);
107 		this.maximumSize = maxSize;
108 		this.purgeSize = purgeSize(maxSize, purgeFactor);
109 	}
110 
111 	/**
112 	 * Returns the value to which the specified key is mapped, or {@code null}
113 	 * if this map contains no mapping for the key.
114 	 *
115 	 * <p>
116 	 * More formally, if this cache contains a mapping from a key {@code k} to a
117 	 * value {@code v} such that {@code key.equals(k)}, then this method returns
118 	 * {@code v}; otherwise it returns {@code null}. (There can be at most one
119 	 * such mapping.)
120 	 *
121 	 * @param key
122 	 *            the key
123 	 *
124 	 * @throws NullPointerException
125 	 *             if the specified key is null
126 	 *
127 	 * @return value mapped for this key, or {@code null} if no value is mapped
128 	 */
129 	@SuppressWarnings("NonAtomicVolatileUpdate")
130 	public V get(Object key) {
131 		Entry<K, V> entry = map.get(key);
132 		if (entry != null) {
133 			entry.lastAccessed = tick();
134 			return entry.value;
135 		}
136 		return null;
137 	}
138 
139 	/**
140 	 * Maps the specified key to the specified value in this cache. Neither the
141 	 * key nor the value can be null.
142 	 *
143 	 * <p>
144 	 * The value can be retrieved by calling the {@code get} method with a key
145 	 * that is equal to the original key.
146 	 *
147 	 * @param key
148 	 *            key with which the specified value is to be associated
149 	 * @param value
150 	 *            value to be associated with the specified key
151 	 * @return the previous value associated with {@code key}, or {@code null}
152 	 *         if there was no mapping for {@code key}
153 	 * @throws NullPointerException
154 	 *             if the specified key or value is null
155 	 */
156 	@SuppressWarnings("NonAtomicVolatileUpdate")
157 	public V put(@NonNull K key, @NonNull V value) {
158 		map.put(key, new Entry<>(key, value, tick()));
159 		if (map.size() > maximumSize) {
160 			purge();
161 		}
162 		return value;
163 	}
164 
165 	@SuppressWarnings("NonAtomicVolatileUpdate")
166 	private long tick() {
167 		return ++time;
168 	}
169 
170 	/**
171 	 * Returns the current size of this cache
172 	 *
173 	 * @return the number of key-value mappings in this cache
174 	 */
175 	public int size() {
176 		return map.size();
177 	}
178 
179 	/**
180 	 * Reconfigures the cache. If {@code maxSize} is reduced some entries will
181 	 * be purged.
182 	 *
183 	 * @param maxSize
184 	 *            maximum size of the cache
185 	 *
186 	 * @param purgeFactor
187 	 *            when the size of the map reaches maxSize the oldest entries
188 	 *            will be purged to free up some space for new entries,
189 	 *            {@code purgeFactor} is the fraction of {@code maxSize} to
190 	 *            purge when this happens
191 	 */
192 	public void configure(int maxSize, float purgeFactor) {
193 		lock.lock();
194 		try {
195 			checkPurgeFactor(purgeFactor);
196 			this.maximumSize = maxSize;
197 			this.purgeSize = purgeSize(maxSize, purgeFactor);
198 			if (map.size() >= maximumSize) {
199 				purge();
200 			}
201 		} finally {
202 			lock.unlock();
203 		}
204 	}
205 
206 	private void purge() {
207 		// don't try to compete if another thread already has the lock
208 		if (lock.tryLock()) {
209 			try {
210 				List<Entry> entriesToPurge = new ArrayList<>(map.values());
211 				// copy access times to avoid other threads interfere with
212 				// sorting
213 				for (Entry e : entriesToPurge) {
214 					e.copyAccessTime();
215 				}
216 				Collections.sort(entriesToPurge,
217 						Comparator.comparingLong(o -> -o.lastAccessedSorting));
218 				for (int index = purgeSize; index < entriesToPurge
219 						.size(); index++) {
220 					map.remove(entriesToPurge.get(index).key);
221 				}
222 			} finally {
223 				lock.unlock();
224 			}
225 		}
226 	}
227 }