View Javadoc
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 }