1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19 package org.apache.shiro.lang.util;
20
21 import java.lang.ref.ReferenceQueue;
22 import java.lang.ref.SoftReference;
23
24 import java.util.ArrayList;
25 import java.util.Collection;
26 import java.util.Collections;
27 import java.util.HashMap;
28 import java.util.Map;
29 import java.util.Queue;
30 import java.util.Set;
31 import java.util.concurrent.ConcurrentHashMap;
32 import java.util.concurrent.ConcurrentLinkedQueue;
33 import java.util.concurrent.locks.ReentrantLock;
34
35
36 /**
37 * A <code><em>Soft</em>HashMap</code> is a memory-constrained map that stores its <em>values</em> in
38 * {@link SoftReference SoftReference}s. (Contrast this with the JDK's
39 * {@link java.util.WeakHashMap WeakHashMap}, which uses weak references for its <em>keys</em>, which is of little value if you
40 * want the cache to auto-resize itself based on memory constraints).
41 * <p/>
42 * Having the values wrapped by soft references allows the cache to automatically reduce its size based on memory
43 * limitations and garbage collection. This ensures that the cache will not cause memory leaks by holding strong
44 * references to all of its values.
45 * <p/>
46 * This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney Redelinghuys's
47 * <a href="http://www.javaspecialists.eu/archive/Issue015.html">publicly posted version (with their approval)</a>, with
48 * continued modifications.
49 * <p/>
50 * This implementation is thread-safe and usable in concurrent environments.
51 *
52 * @param <K> K
53 * @param <V> V
54 * @since 1.0
55 */
56 public class SoftHashMap<K, V> implements Map<K, V> {
57
58 /**
59 * The default value of the RETENTION_SIZE attribute, equal to 100.
60 */
61 private static final int DEFAULT_RETENTION_SIZE = 100;
62
63 /**
64 * The internal HashMap that will hold the SoftReference.
65 */
66 private final Map<K, SoftValue<V, K>> map;
67
68 /**
69 * The number of strong references to hold internally, that is, the number of instances to prevent
70 * from being garbage collected automatically (unlike other soft references).
71 */
72 private final int retentionSize;
73
74 /**
75 * The FIFO list of strong references (not to be garbage collected), order of last access.
76 */
77 //guarded by 'strongReferencesLock'
78 private final Queue<V> strongReferences;
79 private final ReentrantLock strongReferencesLock;
80
81 /**
82 * Reference queue for cleared SoftReference objects.
83 */
84 private final ReferenceQueue<? super V> queue;
85
86 /**
87 * Creates a new SoftHashMap with a default retention size size of
88 * {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
89 *
90 * @see #SoftHashMap(int)
91 */
92 public SoftHashMap() {
93 this(DEFAULT_RETENTION_SIZE);
94 }
95
96 /**
97 * Creates a new SoftHashMap with the specified retention size.
98 * <p/>
99 * The retention size (n) is the total number of most recent entries in the map that will be strongly referenced
100 * (i.e.'retained') to prevent them from being eagerly garbage collected. That is, the point of a SoftHashMap is to
101 * allow the garbage collector to remove as many entries from this map as it desires, but there will always be (n)
102 * elements retained after a GC due to the strong references.
103 * <p/>
104 * Note that in a highly concurrent environments the exact total number of strong references may differ slightly
105 * than the actual <code>retentionSize</code> value. This number is intended to be a best-effort retention low
106 * water mark.
107 *
108 * @param retentionSize the total number of most recent entries in the map that will be strongly referenced
109 * (retained), preventing them from being eagerly garbage collected by the JVM.
110 */
111 @SuppressWarnings({"unchecked"})
112 public SoftHashMap(int retentionSize) {
113 super();
114 this.retentionSize = Math.max(0, retentionSize);
115 queue = new ReferenceQueue<V>();
116 strongReferencesLock = new ReentrantLock();
117 map = new ConcurrentHashMap<K, SoftValue<V, K>>();
118 strongReferences = new ConcurrentLinkedQueue<V>();
119 }
120
121 /**
122 * Creates a {@code SoftHashMap} backed by the specified {@code source}, with a default retention
123 * size of {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries).
124 *
125 * @param source the backing map to populate this {@code SoftHashMap}
126 * @see #SoftHashMap(java.util.Map, int)
127 */
128 public SoftHashMap(Map<K, V> source) {
129 this(DEFAULT_RETENTION_SIZE);
130 putAll(source);
131 }
132
133 /**
134 * Creates a {@code SoftHashMap} backed by the specified {@code source}, with the specified retention size.
135 * <p/>
136 * The retention size (n) is the total number of most recent entries in the map that will be strongly referenced
137 * (i.e.'retained') to prevent them from being eagerly garbage collected. That is, the point of a SoftHashMap is to
138 * allow the garbage collector to remove as many entries from this map as it desires, but there will always be (n)
139 * elements retained after a GC due to the strong references.
140 * <p/>
141 * Note that in a highly concurrent environments the exact total number of strong references may differ slightly
142 * than the actual <code>retentionSize</code> value. This number is intended to be a best-effort retention low
143 * water mark.
144 *
145 * @param source the backing map to populate this {@code SoftHashMap}
146 * @param retentionSize the total number of most recent entries in the map that will be strongly referenced
147 * (retained), preventing them from being eagerly garbage collected by the JVM.
148 */
149 public SoftHashMap(Map<K, V> source, int retentionSize) {
150 this(retentionSize);
151 putAll(source);
152 }
153
154 public V get(Object key) {
155 processQueue();
156
157 V result = null;
158 SoftValue<V, K> value = map.get(key);
159
160 if (value != null) {
161 //unwrap the 'real' value from the SoftReference
162 result = value.get();
163 if (result == null) {
164 //The wrapped value was garbage collected, so remove this entry from the backing map:
165 //noinspection SuspiciousMethodCalls
166 map.remove(key);
167 } else {
168 //Add this value to the beginning of the strong reference queue (FIFO).
169 addToStrongReferences(result);
170 }
171 }
172 return result;
173 }
174
175 private void addToStrongReferences(V result) {
176 strongReferencesLock.lock();
177 try {
178 strongReferences.add(result);
179 trimStrongReferencesIfNecessary();
180 } finally {
181 strongReferencesLock.unlock();
182 }
183
184 }
185
186 //Guarded by the strongReferencesLock in the addToStrongReferences method
187
188 private void trimStrongReferencesIfNecessary() {
189 //trim the strong ref queue if necessary:
190 while (strongReferences.size() > retentionSize) {
191 strongReferences.poll();
192 }
193 }
194
195 /**
196 * Traverses the ReferenceQueue and removes garbage-collected SoftValue objects from the backing map
197 * by looking them up using the SoftValue.key data member.
198 */
199 @SuppressWarnings({"unchecked", "SuspiciousMethodCalls"})
200 private void processQueue() {
201 SoftValue sv;
202 while ((sv = (SoftValue) queue.poll()) != null) {
203 // we can access private data!
204 map.remove(sv.key);
205 }
206 }
207
208 public boolean isEmpty() {
209 processQueue();
210 return map.isEmpty();
211 }
212
213 public boolean containsKey(Object key) {
214 processQueue();
215 return map.containsKey(key);
216 }
217
218 public boolean containsValue(Object value) {
219 processQueue();
220 Collection values = values();
221 return values != null && values.contains(value);
222 }
223
224 public void putAll(Map<? extends K, ? extends V> m) {
225 if (m == null || m.isEmpty()) {
226 processQueue();
227 return;
228 }
229 for (Map.Entry<? extends K, ? extends V> entry : m.entrySet()) {
230 put(entry.getKey(), entry.getValue());
231 }
232 }
233
234 public Set<K> keySet() {
235 processQueue();
236 return map.keySet();
237 }
238
239 @SuppressWarnings("unchecked")
240 public Collection<V> values() {
241 processQueue();
242 Collection<K> keys = map.keySet();
243 if (keys.isEmpty()) {
244 return Collections.EMPTY_SET;
245 }
246 Collection<V> values = new ArrayList<V>(keys.size());
247 for (K key : keys) {
248 V v = get(key);
249 if (v != null) {
250 values.add(v);
251 }
252 }
253 return values;
254 }
255
256 /**
257 * Creates a new entry, but wraps the value in a SoftValue instance to enable auto garbage collection.
258 */
259 public V put(K key, V value) {
260 // throw out garbage collected values first
261 processQueue();
262 SoftValue<V, K> sv = new SoftValue<V, K>(value, key, queue);
263 SoftValue<V, K> previous = map.put(key, sv);
264 addToStrongReferences(value);
265 return previous != null ? previous.get() : null;
266 }
267
268 public V remove(Object key) {
269 // throw out garbage collected values first
270 processQueue();
271 SoftValue<V, K> raw = map.remove(key);
272 return raw != null ? raw.get() : null;
273 }
274
275 public void clear() {
276 strongReferencesLock.lock();
277 try {
278 strongReferences.clear();
279 } finally {
280 strongReferencesLock.unlock();
281 }
282 // throw out garbage collected values
283 processQueue();
284 map.clear();
285 }
286
287 public int size() {
288 // throw out garbage collected values first
289 processQueue();
290 return map.size();
291 }
292
293 @SuppressWarnings("unchecked")
294 public Set<Map.Entry<K, V>> entrySet() {
295 // throw out garbage collected values first
296 processQueue();
297 Collection<K> keys = map.keySet();
298 if (keys.isEmpty()) {
299 //noinspection unchecked
300 return Collections.EMPTY_SET;
301 }
302
303 Map<K, V> kvPairs = new HashMap<K, V>(keys.size());
304 for (K key : keys) {
305 V v = get(key);
306 if (v != null) {
307 kvPairs.put(key, v);
308 }
309 }
310 return kvPairs.entrySet();
311 }
312
313 /**
314 * We define our own subclass of SoftReference which contains
315 * not only the value but also the key to make it easier to find
316 * the entry in the HashMap after it's been garbage collected.
317 */
318 private static final class SoftValue<V, K> extends SoftReference<V> {
319
320 private final K key;
321
322 /**
323 * Constructs a new instance, wrapping the value, key, and queue, as
324 * required by the superclass.
325 *
326 * @param value the map value
327 * @param key the map key
328 * @param queue the soft reference queue to poll to determine if the entry had been reaped by the GC.
329 */
330 private SoftValue(V value, K key, ReferenceQueue<? super V> queue) {
331 super(value, queue);
332 this.key = key;
333 }
334
335 }
336 }