Advertisement
questzen

TimeLimitedCacheMap

Apr 12th, 2011
197
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Java 5 7.63 KB | None | 0 0
  1. package test;
  2.  
  3. import java.util.Collections;
  4. import java.util.HashMap;
  5. import java.util.HashSet;
  6. import java.util.Map;
  7. import java.util.Set;
  8. import java.util.concurrent.ConcurrentHashMap;
  9. import java.util.concurrent.Executors;
  10. import java.util.concurrent.ScheduledExecutorService;
  11. import java.util.concurrent.ThreadFactory;
  12. import java.util.concurrent.TimeUnit;
  13. import java.util.concurrent.locks.ReentrantReadWriteLock;
  14.  
  15. /**
  16.  * Thread safe object map, which clears objects after a specified time. The objects are stored in the
  17.  * underlying hashMap.  
  18.  * @author questzen
  19.  *
  20.  */
  21. public final class TimeLimitedCacheMap {
  22.        
  23.     private final ConcurrentHashMap<String, Object> objectMap = new ConcurrentHashMap<String, Object>(10);
  24.     private final ConcurrentHashMap<String, Long> timeMap = new ConcurrentHashMap<String, Long>();
  25.     /* I need a shared lock, readwrite lock is an excellent candidate.
  26.      * evcition is run with writeLock, put/remove with readLock
  27.      */
  28.     private final ReentrantReadWriteLock accessLock = new ReentrantReadWriteLock();
  29.     //private final ReentrantLock accessLock = new ReentrantLock();
  30.     private final Runnable evictor = new Runnable() {
  31.        
  32.         /* evictor thread removes data, and changes map state. This is
  33.          * in conflict with put() and remove(). So we need sync for these operations  
  34.          *
  35.          * In case you are wondering why evictor needs sync (it just calls remove() right?)
  36.          * eviction is a compound action that spans more than a single remove(). It enters
  37.          * into a conflict with put()
  38.          *
  39.          * evictor: start looping ------------------------------> keyset is stale, evictor removes the recent put & armagedon comes
  40.          * Thrd1:--------------------->put(same key, new object)
  41.          *
  42.          */
  43.         @Override
  44.         public void run() {
  45.             // avoid runs on empty maps
  46.             if(timeMap.isEmpty()){
  47.                 Thread.yield();
  48.             }
  49.             long currentTime = System.nanoTime();
  50.             accessLock.writeLock().lock();
  51.             Set<String> keys = new HashSet<String>(timeMap.keySet());
  52.             accessLock.writeLock().unlock();
  53.             /* First attempt to detect & mark stale entries, but don't delete
  54.              * The hash map may contain 1000s of objects dont' block it. The returned
  55.              * Set returned may be stale, implying:
  56.              * 1. contains keys for objects which are removed by user, using remove() (not a problem)
  57.              * 2. contains keys for objects which are updated by user, using put() [a big problem]
  58.              */
  59.             Set<String> markedForRemoval = new HashSet<String>(10);
  60.             for (String key : keys) {
  61.                 long lastTime = timeMap.get(key);
  62.                 if(lastTime == 0){
  63.                     continue;
  64.                 }
  65.                 long interval = currentTime - lastTime;
  66.                 long elapsedTime = TimeUnit.NANOSECONDS.convert(interval, expiryTimeUnit);
  67.                 if(elapsedTime > expiryTime){
  68.                     markedForRemoval.add(key);
  69.                 }
  70.             }
  71.            
  72.             /* Actual removal call, which runs on the objects marked earlier.
  73.              * Assumption: marked objects.size() < hashmap.size()
  74.              * Do not delete blindly, check for staleness before calling remove
  75.              */
  76.             accessLock.writeLock().lock();
  77.             for (String key : markedForRemoval) {
  78.                 long lastTime = timeMap.get(key);
  79.                 if(lastTime == 0){
  80.                     continue;
  81.                 }
  82.                 long interval = currentTime - lastTime;
  83.                 long elapsedTime = TimeUnit.NANOSECONDS.convert(interval, expiryTimeUnit);
  84.                 if(elapsedTime > expiryTime){
  85.                     remove(key);
  86.                 }
  87.             }
  88.             accessLock.writeLock().unlock();
  89.         }
  90.     };
  91.    
  92.     private final ScheduledExecutorService timer = Executors.newSingleThreadScheduledExecutor(new MyThreadFactory(true));
  93.     private final class MyThreadFactory implements ThreadFactory {
  94.        
  95.         private boolean isDaemon = false;
  96.        
  97.         public MyThreadFactory(boolean daemon){
  98.             isDaemon = daemon;
  99.         }
  100.  
  101.         @Override
  102.         public Thread newThread(Runnable r) {
  103.             Thread t = new Thread(r);
  104.             t.setDaemon(isDaemon);
  105.             return t;
  106.         }      
  107.        
  108.     };
  109.     private final long expiryTime;
  110.     private final TimeUnit expiryTimeUnit;
  111.    
  112.     /**
  113.      * Users can play around with evictionDelay and expiryTime.
  114.      * 1. Large evictionDelay => less frequent checks, hence chances of finding expired Objects are more
  115.      * 2. Lean evictionDelay => aggressive checks, and hence more sync overhead with put() and remove()
  116.      * 3. Large expiryTime => increases the time object stays in object map and less chance of cache miss (cache miss is bad)
  117.      * 4. Lean expiryTime => by itself does not force object to be removed aggressively, needs lean eviction to be configured
  118.      *
  119.      * In case you are wondering, above picture is not complete.
  120.      * Another key aspect is "Arrival Periodicty", or the rate at which put() is called.
  121.      *
  122.      * Ideally: expiryTime == arrival periodicity + 1 [read '+1' as slightly greater]
  123.      *          evictionDelay == expiryTime + 1
  124.      *
  125.      * For random arrival times, which is a more common scenario, use the following pointers
  126.      * 1. eviction Delay > expiry Time
  127.      *      Here user needs to think of the impact of stale hit (define how stale is stale!)
  128.      * 2. eviction Delay < arrival time
  129.      *      This has higher chances of cache miss and accidental treatment as failure    *
  130.      * 3. eviction Delay < expiry Time
  131.      *      Unwanted eviction run(s) resulting in sync overhead on map
  132.      * 4. eviction Delay > arrival Time
  133.      *      Unwanted eviction run(s) resulting in sync overhead on map
  134.      *  
  135.      * @param initialDelay, time after which scheduler starts
  136.      * @param evictionDelay, periodicity with which eviction is carried out  
  137.      * @param expiryTime, age of the object, exceeding which the object is  to be removed
  138.      * @param unit
  139.      */
  140.     public TimeLimitedCacheMap(long initialDelay, long evictionDelay, long expiryTime, TimeUnit unit){
  141.         timer.scheduleWithFixedDelay(evictor, initialDelay, evictionDelay, unit);
  142.         this.expiryTime = expiryTime;
  143.         this.expiryTimeUnit = unit;
  144.     }
  145.  
  146.     /* The intention is to prevent user from modifying the object Map,
  147.      * I want all adds/removals to be suspended. synchronizing on objectMap
  148.      * would also work, but locks are easier to read without scope issues of {}
  149.      *
  150.      * Concurrent hashMap would have allowed put and remove to happen concurrently.
  151.      * I did not use conc-map, because
  152.      * 1. I want to update another map (timeMap)
  153.      * 2. I want to sync the operation with evictor thread
  154.      *
  155.      * The unfortunate invariants:
  156.      *  1. sync between evictor and put()
  157.      *  2. sync between evictor and remove()
  158.      *  imply
  159.      *  3. sync lock between put() and remove()
  160.      *  
  161.      *  3. is unfortunate side effect, as you need to sync all exposed invariants on the same lock.
  162.      *  Lock duplication won't help. If I create putLock() and removeLock(), they will allow put() and remove()
  163.      *  to happen concurrently, but will not help two put()/remove() calls to happen in parallel.  
  164.      */
  165.     public void put(String key, Object value) {    
  166.         accessLock.readLock().lock();
  167.         Long nanoTime = System.nanoTime();
  168.         timeMap.put(key, nanoTime);
  169.         objectMap.put(key, value);
  170.         accessLock.readLock().unlock();    
  171.     }
  172.  
  173.     /* Read comments for put() they apply here as well.
  174.      * If had not allowed remove(), life would have been zillion times simpler.
  175.      * However, an undoable action is quite bad.
  176.      */
  177.     public Object remove(Object key) {     
  178.         accessLock.readLock().lock();
  179.         //accessLock.lock();
  180.         Object value = objectMap.remove(key);
  181.         timeMap.remove(key);
  182.         //accessLock.unlock();
  183.         accessLock.readLock().unlock();
  184.         return value;
  185.        
  186.     }
  187.    
  188.     /* Clone requires locking, to prevent the edge case where
  189.      * the map is updated with clone is in progress
  190.      */
  191.     public Map<String, Object> getClonedMap(){
  192.         accessLock.writeLock().lock();
  193.         HashMap<String, Object> mapClone = new HashMap<String, Object>(objectMap);
  194.         accessLock.writeLock().unlock();
  195.         return Collections.unmodifiableMap(mapClone);
  196.     }
  197.  
  198. }
Advertisement
Add Comment
Please, Sign In to add comment
Advertisement