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   */
    20  package org.apache.mina.util;
    21
    22  import java.util.Collection;
    23  import java.util.Map;
    24  import java.util.Set;
    25  import java.util.concurrent.ConcurrentHashMap;
    26  import java.util.concurrent.CopyOnWriteArrayList;
    27  import java.util.concurrent.locks.ReadWriteLock;
    28  import java.util.concurrent.locks.ReentrantReadWriteLock;
    29
    30  /**
    31   * A map with expiration.  This class contains a worker thread that will
    32   * periodically check this class in order to determine if any objects
    33   * should be removed based on the provided time-to-live value.
    34   *
    35   * @author <a href="http://mina.apache.org">Apache MINA Project</a>
    36   */
    37  public class ExpiringMap<K, V> implements Map<K, V> {
    38
    39      /**
    40       * The default value, 60
    41       */
    42      public static final int DEFAULT_TIME_TO_LIVE = 60;
    43
    44      /**
    45       * The default value, 1
    46       */
    47      public static final int DEFAULT_EXPIRATION_INTERVAL = 1;
    48
    49      private static volatile int expirerCount = 1;
    50
    51      private final ConcurrentHashMap<K, ExpiringObject> delegate;
    52
    53      private final CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners;
    54
    55      private final Expirer expirer;
    56
    57      /**
    58       * Creates a new instance of ExpiringMap using the default values
    59       * DEFAULT_TIME_TO_LIVE and DEFAULT_EXPIRATION_INTERVAL
    60       *
    61       */
    62      public ExpiringMap() {
    63          this(DEFAULT_TIME_TO_LIVE, DEFAULT_EXPIRATION_INTERVAL);
    64      }
    65
    66      /**
    67       * Creates a new instance of ExpiringMap using the supplied
    68       * time-to-live value and the default value for DEFAULT_EXPIRATION_INTERVAL
    69       *
    70       * @param timeToLive
    71       *  The time-to-live value (seconds)
    72       */
    73      public ExpiringMap(int timeToLive) {
    74          this(timeToLive, DEFAULT_EXPIRATION_INTERVAL);
    75      }
    76
    77      /**
    78       * Creates a new instance of ExpiringMap using the supplied values and
    79       * a {@link ConcurrentHashMap} for the internal data structure.
    80       *
    81       * @param timeToLive
    82       *  The time-to-live value (seconds)
    83       * @param expirationInterval
    84       *  The time between checks to see if a value should be removed (seconds)
    85       */
    86      public ExpiringMap(int timeToLive, int expirationInterval) {
    87          this(new ConcurrentHashMap<K, ExpiringObject>(), new CopyOnWriteArrayList<ExpirationListener<V>>(), timeToLive,
    88                  expirationInterval);
    89      }
    90
    91      private ExpiringMap(ConcurrentHashMap<K, ExpiringObject> delegate,
    92              CopyOnWriteArrayList<ExpirationListener<V>> expirationListeners, int timeToLive, int expirationInterval) {
    93          this.delegate = delegate;
    94          this.expirationListeners = expirationListeners;
    95
    96          this.expirer = new Expirer();
    97          expirer.setTimeToLive(timeToLive);
    98          expirer.setExpirationInterval(expirationInterval);
    99      }
    100
    101     public V put(K key, V value) {
    102         ExpiringObject answer = delegate.put(key, new ExpiringObject(key, value, System.currentTimeMillis()));
    103         if (answer == null) {
    104             return null;
    105         }
    106
    107         return answer.getValue();
    108     }
    109
    110     public V get(Object key) {
    111         ExpiringObject object = delegate.get(key);
    112
    113         if (object != null) {
    114             object.setLastAccessTime(System.currentTimeMillis());
    115
    116             return object.getValue();
    117         }
    118
    119         return null;
    120     }
    121
    122     public V remove(Object key) {
    123         ExpiringObject answer = delegate.remove(key);
    124         if (answer == null) {
    125             return null;
    126         }
    127
    128         return answer.getValue();
    129     }
    130
    131     public boolean containsKey(Object key) {
    132         return delegate.containsKey(key);
    133     }
    134
    135     public boolean containsValue(Object value) {
    136         return delegate.containsValue(value);
    137     }
    138
    139     public int size() {
    140         return delegate.size();
    141     }
    142
    143     public boolean isEmpty() {
    144         return delegate.isEmpty();
    145     }
    146
    147     public void clear() {
    148         delegate.clear();
    149     }
    150
    151     @Override
    152     public int hashCode() {
    153         return delegate.hashCode();
    154     }
    155
    156     public Set<K> keySet() {
    157         return delegate.keySet();
    158     }
    159
    160     @Override
    161     public boolean equals(Object obj) {
    162         return delegate.equals(obj);
    163     }
    164
    165     public void putAll(Map<? extends K, ? extends V> inMap) {
    166         for (Entry<? extends K, ? extends V> e : inMap.entrySet()) {
    167             this.put(e.getKey(), e.getValue());
    168         }
    169     }
    170
    171     public Collection<V> values() {
    172         throw new UnsupportedOperationException();
    173     }
    174
    175     public Set<Map.Entry<K, V>> entrySet() {
    176         throw new UnsupportedOperationException();
    177     }
    178
    179     public void addExpirationListener(ExpirationListener<V> listener) {
    180         expirationListeners.add(listener);
    181     }
    182
    183     public void removeExpirationListener(ExpirationListener<V> listener) {
    184         expirationListeners.remove(listener);
    185     }
    186
    187     public Expirer getExpirer() {
    188         return expirer;
    189     }
    190
    191     public int getExpirationInterval() {
    192         return expirer.getExpirationInterval();
    193     }
    194
    195     public int getTimeToLive() {
    196         return expirer.getTimeToLive();
    197     }
    198
    199     public void setExpirationInterval(int expirationInterval) {
    200         expirer.setExpirationInterval(expirationInterval);
    201     }
    202
    203     public void setTimeToLive(int timeToLive) {
    204         expirer.setTimeToLive(timeToLive);
    205     }
    206
    207     private class ExpiringObject {
    208         private K key;
    209
    210         private V value;
    211
    212         private long lastAccessTime;
    213
    214         private final ReadWriteLock lastAccessTimeLock = new ReentrantReadWriteLock();
    215
    216         ExpiringObject(K key, V value, long lastAccessTime) {
    217             if (value == null) {
    218                 throw new IllegalArgumentException("An expiring object cannot be null.");
    219             }
    220
    221             this.key = key;
    222             this.value = value;
    223             this.lastAccessTime = lastAccessTime;
    224         }
    225
    226         public long getLastAccessTime() {
    227             lastAccessTimeLock.readLock().lock();
    228
    229             try {
    230                 return lastAccessTime;
    231             } finally {
    232                 lastAccessTimeLock.readLock().unlock();
    233             }
    234         }
    235
    236         public void setLastAccessTime(long lastAccessTime) {
    237             lastAccessTimeLock.writeLock().lock();
    238
    239             try {
    240                 this.lastAccessTime = lastAccessTime;
    241             } finally {
    242                 lastAccessTimeLock.writeLock().unlock();
    243             }
    244         }
    245
    246         public K getKey() {
    247             return key;
    248         }
    249
    250         public V getValue() {
    251             return value;
    252         }
    253
    254         @Override
    255         public boolean equals(Object obj) {
    256             return value.equals(obj);
    257         }
    258
    259         @Override
    260         public int hashCode() {
    261             return value.hashCode();
    262         }
    263     }
    264
    265     /**
    266      * A Thread that monitors an {@link ExpiringMap} and will remove
    267      * elements that have passed the threshold.
    268      *
    269      */
    270     public class Expirer implements Runnable {
    271         private final ReadWriteLock stateLock = new ReentrantReadWriteLock();
    272
    273         private long timeToLiveMillis;
    274
    275         private long expirationIntervalMillis;
    276
    277         private boolean running = false;
    278
    279         private final Thread expirerThread;
    280
    281         /**
    282          * Creates a new instance of Expirer.
    283          *
    284          */
    285         public Expirer() {
    286             expirerThread = new Thread(this, "ExpiringMapExpirer-" + expirerCount++);
    287             expirerThread.setDaemon(true);
    288         }
    289
    290         public void run() {
    291             while (running) {
    292                 processExpires();
    293
    294                 try {
    295                     Thread.sleep(expirationIntervalMillis);
    296                 } catch (InterruptedException e) {
    297                     // Do nothing
    298                 }
    299             }
    300         }
    301
    302         private void processExpires() {
    303             long timeNow = System.currentTimeMillis();
    304
    305             for (ExpiringObject o : delegate.values()) {
    306
    307                 if (timeToLiveMillis <= 0) {
    308                     continue;
    309                 }
    310
    311                 long timeIdle = timeNow - o.getLastAccessTime();
    312
    313                 if (timeIdle >= timeToLiveMillis) {
    314                     delegate.remove(o.getKey());
    315
    316                     for (ExpirationListener<V> listener : expirationListeners) {
    317                         listener.expired(o.getValue());
    318                     }
    319                 }
    320             }
    321         }
    322
    323         /**
    324          * Kick off this thread which will look for old objects and remove them.
    325          *
    326          */
    327         public void startExpiring() {
    328             stateLock.writeLock().lock();
    329
    330             try {
    331                 if (!running) {
    332                     running = true;
    333                     expirerThread.start();
    334                 }
    335             } finally {
    336                 stateLock.writeLock().unlock();
    337             }
    338         }
    339
    340         /**
    341          * If this thread has not started, then start it.
    342          * Otherwise just return;
    343          */
    344         public void startExpiringIfNotStarted() {
    345             stateLock.readLock().lock();
    346             try {
    347                 if (running) {
    348                     return;
    349                 }
    350             } finally {
    351                 stateLock.readLock().unlock();
    352             }
    353
    354             stateLock.writeLock().lock();
    355             try {
    356                 if (!running) {
    357                     running = true;
    358                     expirerThread.start();
    359                 }
    360             } finally {
    361                 stateLock.writeLock().unlock();
    362             }
    363         }
    364
    365         /**
    366          * Stop the thread from monitoring the map.
    367          */
    368         public void stopExpiring() {
    369             stateLock.writeLock().lock();
    370
    371             try {
    372                 if (running) {
    373                     running = false;
    374                     expirerThread.interrupt();
    375                 }
    376             } finally {
    377                 stateLock.writeLock().unlock();
    378             }
    379         }
    380
    381         /**
    382          * Checks to see if the thread is running
    383          *
    384          * @return
    385          *  If the thread is running, true.  Otherwise false.
    386          */
    387         public boolean isRunning() {
    388             stateLock.readLock().lock();
    389
    390             try {
    391                 return running;
    392             } finally {
    393                 stateLock.readLock().unlock();
    394             }
    395         }
    396
    397         /**
    398          * Returns the Time-to-live value.
    399          *
    400          * @return
    401          *  The time-to-live (seconds)
    402          */
    403         public int getTimeToLive() {
    404             stateLock.readLock().lock();
    405
    406             try {
    407                 return (int) timeToLiveMillis / 1000;
    408             } finally {
    409                 stateLock.readLock().unlock();
    410             }
    411         }
    412
    413         /**
    414          * Update the value for the time-to-live
    415          *
    416          * @param timeToLive
    417          *  The time-to-live (seconds)
    418          */
    419         public void setTimeToLive(long timeToLive) {
    420             stateLock.writeLock().lock();
    421
    422             try {
    423                 this.timeToLiveMillis = timeToLive * 1000;
    424             } finally {
    425                 stateLock.writeLock().unlock();
    426             }
    427         }
    428
    429         /**
    430          * Get the interval in which an object will live in the map before
    431          * it is removed.
    432          *
    433          * @return
    434          *  The time in seconds.
    435          */
    436         public int getExpirationInterval() {
    437             stateLock.readLock().lock();
    438
    439             try {
    440                 return (int) expirationIntervalMillis / 1000;
    441             } finally {
    442                 stateLock.readLock().unlock();
    443             }
    444         }
    445
    446         /**
    447          * Set the interval in which an object will live in the map before
    448          * it is removed.
    449          *
    450          * @param expirationInterval
    451          *  The time in seconds
    452          */
    453         public void setExpirationInterval(long expirationInterval) {
    454             stateLock.writeLock().lock();
    455
    456             try {
    457                 this.expirationIntervalMillis = expirationInterval * 1000;
    458             } finally {
    459                 stateLock.writeLock().unlock();
    460             }
    461         }
    462     }
    463 }