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 * 019 */ 020package org.apache.mina.util; 021 022import java.util.Collection; 023import java.util.Map; 024import java.util.Set; 025import java.util.concurrent.ConcurrentHashMap; 026import java.util.concurrent.CopyOnWriteArrayList; 027import java.util.concurrent.locks.ReadWriteLock; 028import java.util.concurrent.locks.ReentrantReadWriteLock; 029 030/** 031 * A map with expiration. This class contains a worker thread that will 032 * periodically check this class in order to determine if any objects 033 * should be removed based on the provided time-to-live value. 034 * 035 * @author <a href="http://mina.apache.org">Apache MINA Project</a> 036 */ 037public class ExpiringMap<K, V> implements Map<K, V> { 038 /** The default value, 60 seconds */ 039 public static final int DEFAULT_TIME_TO_LIVE = 60; 040 041 /** The default value, 1 second */ 042 public static final int DEFAULT_EXPIRATION_INTERVAL = 1; 043 044 private static volatile int expirerCount = 1; 045 046 private final ConcurrentHashMap<K, ExpiringObject> delegate; 047 048 private final CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners; 049 050 private final Expirer expirer; 051 052 /** 053 * Creates a new instance of ExpiringMap using the default values 054 * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL 055 * 056 */ 057 public ExpiringMap() { 058 this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL); 059 } 060 061 /** 062 * Creates a new instance of ExpiringMap using the supplied 063 * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL 064 * 065 * @param timeToLive The time-to-live value (seconds) 066 */ 067 public ExpiringMap(int timeToLive) { 068 this(timeToLive, DEFAULT_EXPIRATION_INTERVAL); 069 } 070 071 /** 072 * Creates a new instance of ExpiringMap using the supplied values and 073 * a {@link ConcurrentHashMap} for the internal data structure. 074 * 075 * @param timeToLive The time-to-live value (seconds) 076 * @param expirationInterval The time between checks to see if a value should be removed (seconds) 077 */ 078 public ExpiringMap(int timeToLive, int expirationInterval) { 079 this(new ConcurrentHashMap<K, ExpiringObject>(), new CopyOnWriteArrayList<ExpirationListener<V>>(), timeToLive, 080 expirationInterval); 081 } 082 083 private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate, 084 CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners, int timeToLive, int expirationInterval) { 085 this.delegate = delegate; 086 this.expirationListeners = expirationListeners; 087 088 this.expirer = new Expirer(); 089 expirer.setTimeToLive(timeToLive); 090 expirer.setExpirationInterval(expirationInterval); 091 } 092 093 public V put(K key, V value) { 094 ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis())); 095 096 if (answer == null) { 097 return null; 098 } 099 100 return answer.getValue(); 101 } 102 103 public V get(Object key) { 104 ExpiringObject object = delegate.get(key); 105 106 if (object != null) { 107 object.setLastAccessTime(System.currentTimeMillis()); 108 109 return object.getValue(); 110 } 111 112 return null; 113 } 114 115 public V remove(Object key) { 116 ExpiringObject answer = delegate.remove(key); 117 if (answer == null) { 118 return null; 119 } 120 121 return answer.getValue(); 122 } 123 124 public boolean containsKey(Object key) { 125 return delegate.containsKey(key); 126 } 127 128 public boolean containsValue(Object value) { 129 return delegate.containsValue(value); 130 } 131 132 public int size() { 133 return delegate.size(); 134 } 135 136 public boolean isEmpty() { 137 return delegate.isEmpty(); 138 } 139 140 public void clear() { 141 delegate.clear(); 142 } 143 144 @Override 145 public int hashCode() { 146 return delegate.hashCode(); 147 } 148 149 public Set<K> keySet() { 150 return delegate.keySet(); 151 } 152 153 @Override 154 public boolean equals(Object obj) { 155 return delegate.equals(obj); 156 } 157 158 public void putAll(Map<? extends K, ? extends V> inMap) { 159 for (Entry<? extends K, ? extends V> e : inMap.entrySet()) { 160 this.put(e.getKey(), e.getValue()); 161 } 162 } 163 164 public Collection<V> values() { 165 throw new UnsupportedOperationException(); 166 } 167 168 public Set<Map.Entry<K, V>> entrySet() { 169 throw new UnsupportedOperationException(); 170 } 171 172 public void addExpirationListener(ExpirationListener<V> listener) { 173 expirationListeners.add(listener); 174 } 175 176 public void removeExpirationListener(ExpirationListener<V> listener) { 177 expirationListeners.remove(listener); 178 } 179 180 public Expirer getExpirer() { 181 return expirer; 182 } 183 184 public int getExpirationInterval() { 185 return expirer.getExpirationInterval(); 186 } 187 188 public int getTimeToLive() { 189 return expirer.getTimeToLive(); 190 } 191 192 public void setExpirationInterval(int expirationInterval) { 193 expirer.setExpirationInterval(expirationInterval); 194 } 195 196 public void setTimeToLive(int timeToLive) { 197 expirer.setTimeToLive(timeToLive); 198 } 199 200 private class ExpiringObject { 201 private K key; 202 203 private V value; 204 205 private long lastAccessTime; 206 207 private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock(); 208 209 ExpiringObject(K key, V value, long lastAccessTime) { 210 if (value == null) { 211 throw new IllegalArgumentException("An expiring object cannot be null."); 212 } 213 214 this.key = key; 215 this.value = value; 216 this.lastAccessTime = lastAccessTime; 217 } 218 219 public long getLastAccessTime() { 220 lastAccessTimeLock.readLock().lock(); 221 222 try { 223 return lastAccessTime; 224 } finally { 225 lastAccessTimeLock.readLock().unlock(); 226 } 227 } 228 229 public void setLastAccessTime(long lastAccessTime) { 230 lastAccessTimeLock.writeLock().lock(); 231 232 try { 233 this.lastAccessTime = lastAccessTime; 234 } finally { 235 lastAccessTimeLock.writeLock().unlock(); 236 } 237 } 238 239 public K getKey() { 240 return key; 241 } 242 243 public V getValue() { 244 return value; 245 } 246 247 @Override 248 public boolean equals(Object obj) { 249 return value.equals(obj); 250 } 251 252 @Override 253 public int hashCode() { 254 return value.hashCode(); 255 } 256 } 257 258 /** 259 * A Thread that monitors an {@link ExpiringMap} and will remove 260 * elements that have passed the threshold. 261 * 262 */ 263 public class Expirer implements Runnable { 264 private final ReadWriteLock stateLock = new ReentrantReadWriteLock(); 265 266 private long timeToLiveMillis; 267 268 private long expirationIntervalMillis; 269 270 private boolean running = false; 271 272 private final Thread expirerThread; 273 274 /** 275 * Creates a new instance of Expirer. 276 * 277 */ 278 public Expirer() { 279 expirerThread = new Thread(this, "ExpiringMapExpirer-" + expirerCount++); 280 expirerThread.setDaemon(true); 281 } 282 283 public void run() { 284 while (running) { 285 processExpires(); 286 287 try { 288 Thread.sleep(expirationIntervalMillis); 289 } catch (InterruptedException e) { 290 // Do nothing 291 } 292 } 293 } 294 295 private void processExpires() { 296 long timeNow = System.currentTimeMillis(); 297 298 for (ExpiringObject o : delegate.values()) { 299 300 if (timeToLiveMillis <= 0) { 301 continue; 302 } 303 304 long timeIdle = timeNow - o.getLastAccessTime(); 305 306 if (timeIdle >= timeToLiveMillis) { 307 delegate.remove(o.getKey()); 308 309 for (ExpirationListener<V> listener : expirationListeners) { 310 listener.expired(o.getValue()); 311 } 312 } 313 } 314 } 315 316 /** 317 * Kick off this thread which will look for old objects and remove them. 318 * 319 */ 320 public void startExpiring() { 321 stateLock.writeLock().lock(); 322 323 try { 324 if (!running) { 325 running = true; 326 expirerThread.start(); 327 } 328 } finally { 329 stateLock.writeLock().unlock(); 330 } 331 } 332 333 /** 334 * If this thread has not started, then start it. 335 * Otherwise just return; 336 */ 337 public void startExpiringIfNotStarted() { 338 stateLock.readLock().lock(); 339 try { 340 if (running) { 341 return; 342 } 343 } finally { 344 stateLock.readLock().unlock(); 345 } 346 347 stateLock.writeLock().lock(); 348 try { 349 if (!running) { 350 running = true; 351 expirerThread.start(); 352 } 353 } finally { 354 stateLock.writeLock().unlock(); 355 } 356 } 357 358 /** 359 * Stop the thread from monitoring the map. 360 */ 361 public void stopExpiring() { 362 stateLock.writeLock().lock(); 363 364 try { 365 if (running) { 366 running = false; 367 expirerThread.interrupt(); 368 } 369 } finally { 370 stateLock.writeLock().unlock(); 371 } 372 } 373 374 /** 375 * Checks to see if the thread is running 376 * 377 * @return 378 * If the thread is running, true. Otherwise false. 379 */ 380 public boolean isRunning() { 381 stateLock.readLock().lock(); 382 383 try { 384 return running; 385 } finally { 386 stateLock.readLock().unlock(); 387 } 388 } 389 390 /** 391 * @return the Time-to-live value in seconds. 392 */ 393 public int getTimeToLive() { 394 stateLock.readLock().lock(); 395 396 try { 397 return (int) timeToLiveMillis / 1000; 398 } finally { 399 stateLock.readLock().unlock(); 400 } 401 } 402 403 /** 404 * Update the value for the time-to-live 405 * 406 * @param timeToLive 407 * The time-to-live (seconds) 408 */ 409 public void setTimeToLive(long timeToLive) { 410 stateLock.writeLock().lock(); 411 412 try { 413 this.timeToLiveMillis = timeToLive * 1000; 414 } finally { 415 stateLock.writeLock().unlock(); 416 } 417 } 418 419 /** 420 * Get the interval in which an object will live in the map before 421 * it is removed. 422 * 423 * @return 424 * The time in seconds. 425 */ 426 public int getExpirationInterval() { 427 stateLock.readLock().lock(); 428 429 try { 430 return (int) expirationIntervalMillis / 1000; 431 } finally { 432 stateLock.readLock().unlock(); 433 } 434 } 435 436 /** 437 * Set the interval in which an object will live in the map before 438 * it is removed. 439 * 440 * @param expirationInterval 441 * The time in seconds 442 */ 443 public void setExpirationInterval(long expirationInterval) { 444 stateLock.writeLock().lock(); 445 446 try { 447 this.expirationIntervalMillis = expirationInterval * 1000; 448 } finally { 449 stateLock.writeLock().unlock(); 450 } 451 } 452 } 453}