001/* 002 * Licensed to the Apache Software Foundation (ASF) under one 003 * or more contributor license agreements. See the NOTICE file 004 * distributed with this work for additional information 005 * regarding copyright ownership. The ASF licenses this file 006 * to you under the Apache License, Version 2.0 (the 007 * "License"); you may not use this file except in compliance 008 * with the License. You may obtain a copy of the License at 009 * 010 * http://www.apache.org/licenses/LICENSE-2.0 011 * 012 * Unless required by applicable law or agreed to in writing, 013 * software distributed under the License is distributed on an 014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 015 * KIND, either express or implied. See the License for the 016 * specific language governing permissions and limitations 017 * under the License. 018 */ 019package org.apache.shiro.lang.util; 020 021import java.lang.ref.ReferenceQueue; 022import java.lang.ref.SoftReference; 023 024import java.util.ArrayList; 025import java.util.Collection; 026import java.util.Collections; 027import java.util.HashMap; 028import java.util.Map; 029import java.util.Queue; 030import java.util.Set; 031import java.util.concurrent.ConcurrentHashMap; 032import java.util.concurrent.ConcurrentLinkedQueue; 033import java.util.concurrent.locks.ReentrantLock; 034 035 036/** 037 * A <code><em>Soft</em>HashMap</code> is a memory-constrained map that stores its <em>values</em> in 038 * {@link SoftReference SoftReference}s. (Contrast this with the JDK's 039 * {@link java.util.WeakHashMap WeakHashMap}, which uses weak references for its <em>keys</em>, which is of little value if you 040 * want the cache to auto-resize itself based on memory constraints). 041 * <p/> 042 * Having the values wrapped by soft references allows the cache to automatically reduce its size based on memory 043 * limitations and garbage collection. This ensures that the cache will not cause memory leaks by holding strong 044 * references to all of its values. 045 * <p/> 046 * This class is a generics-enabled Map based on initial ideas from Heinz Kabutz's and Sydney Redelinghuys's 047 * <a href="http://www.javaspecialists.eu/archive/Issue015.html">publicly posted version (with their approval)</a>, with 048 * continued modifications. 049 * <p/> 050 * This implementation is thread-safe and usable in concurrent environments. 051 * 052 * @param <K> K 053 * @param <V> V 054 * @since 1.0 055 */ 056public class SoftHashMap<K, V> implements Map<K, V> { 057 058 /** 059 * The default value of the RETENTION_SIZE attribute, equal to 100. 060 */ 061 private static final int DEFAULT_RETENTION_SIZE = 100; 062 063 /** 064 * The internal HashMap that will hold the SoftReference. 065 */ 066 private final Map<K, SoftValue<V, K>> map; 067 068 /** 069 * The number of strong references to hold internally, that is, the number of instances to prevent 070 * from being garbage collected automatically (unlike other soft references). 071 */ 072 private final int retentionSize; 073 074 /** 075 * The FIFO list of strong references (not to be garbage collected), order of last access. 076 */ 077 //guarded by 'strongReferencesLock' 078 private final Queue<V> strongReferences; 079 private final ReentrantLock strongReferencesLock; 080 081 /** 082 * Reference queue for cleared SoftReference objects. 083 */ 084 private final ReferenceQueue<? super V> queue; 085 086 /** 087 * Creates a new SoftHashMap with a default retention size size of 088 * {@link #DEFAULT_RETENTION_SIZE DEFAULT_RETENTION_SIZE} (100 entries). 089 * 090 * @see #SoftHashMap(int) 091 */ 092 public SoftHashMap() { 093 this(DEFAULT_RETENTION_SIZE); 094 } 095 096 /** 097 * Creates a new SoftHashMap with the specified retention size. 098 * <p/> 099 * 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}