简单LRU算法实现缓存

来源:互联网 发布:食品安全专业出国 知乎 编辑:程序博客网 时间:2024/05/18 03:49
最简单的LRU算法实现,就是利用jdk的LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可,如下所示:
java 代码
 
  1. import java.util.ArrayList;  
  2. import java.util.Collection;  
  3. import java.util.LinkedHashMap;  
  4. import java.util.concurrent.locks.Lock;  
  5. import java.util.concurrent.locks.ReentrantLock;  
  6. import java.util.Map;  
  7.   
  8.   
  9. /** 
  10.  * 类说明:利用LinkedHashMap实现简单的缓存, 必须实现removeEldestEntry方法,具体参见JDK文档 
  11.  *  
  12.  * @author dennis 
  13.  *  
  14.  * @param <K> 
  15.  * @param <V> 
  16.  */  
  17. public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {  
  18.     private final int maxCapacity;  
  19.   
  20.     private static final float DEFAULT_LOAD_FACTOR = 0.75f;  
  21.   
  22.     private final Lock lock = new ReentrantLock();  
  23.   
  24.     public LRULinkedHashMap(int maxCapacity) {  
  25.         super(maxCapacity, DEFAULT_LOAD_FACTOR, true);  
  26.         this.maxCapacity = maxCapacity;  
  27.     }  
  28.   
  29.     @Override  
  30.     protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {  
  31.         return size() > maxCapacity;  
  32.     }  
  33.     @Override  
  34.     public boolean containsKey(Object key) {  
  35.         try {  
  36.             lock.lock();  
  37.             return super.containsKey(key);  
  38.         } finally {  
  39.             lock.unlock();  
  40.         }  
  41.     }  
  42.   
  43.       
  44.     @Override  
  45.     public V get(Object key) {  
  46.         try {  
  47.             lock.lock();  
  48.             return super.get(key);  
  49.         } finally {  
  50.             lock.unlock();  
  51.         }  
  52.     }  
  53.   
  54.     @Override  
  55.     public V put(K key, V value) {  
  56.         try {  
  57.             lock.lock();  
  58.             return super.put(key, value);  
  59.         } finally {  
  60.             lock.unlock();  
  61.         }  
  62.     }  
  63.   
  64.     public int size() {  
  65.         try {  
  66.             lock.lock();  
  67.             return super.size();  
  68.         } finally {  
  69.             lock.unlock();  
  70.         }  
  71.     }  
  72.   
  73.     public void clear() {  
  74.         try {  
  75.             lock.lock();  
  76.             super.clear();  
  77.         } finally {  
  78.             lock.unlock();  
  79.         }  
  80.     }  
  81.   
  82.     public Collection<Map.Entry<K, V>> getAll() {  
  83.         try {  
  84.             lock.lock();  
  85.             return new ArrayList<Map.Entry<K, V>>(super.entrySet());  
  86.         } finally {  
  87.             lock.unlock();  
  88.         }  
  89.     }  
  90. }  
  91.     

  如果你去看LinkedHashMap的源码可知,LRU算法是通过双向链表来实现,当某个位置被命中,通过调整链表的指向将该位置调整到头位置,新加入 的内容直接放在链表头,如此一来,最近被命中的内容就向链表头移动,需要替换时,链表最后的位置就是最近最少使用的位置。

深入Java集合学习系列:LinkedHashMap的实现原理


1. LinkedHashMap概述:

   LinkedHashMap是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。
   LinkedHashMap实现与HashMap的不同之处在于,后者维护着一个运行于所有条目的双重链接列表。此链接列表定义了迭代顺序,该迭代顺序可以是插入顺序或者是访问顺序。
   注意,此实现不是同步的。如果多个线程同时访问链接的哈希映射,而其中至少一个线程从结构上修改了该映射,则它必须保持外部同步。

 

2. LinkedHashMap的实现:

   对于LinkedHashMap而言,它继承与HashMap、底层使用哈希表与双向链表来保存所有元素。其基本操作与父类HashMap相似,它通过重写父类相关的方法,来实现自己的链接列表特性。下面我们来分析LinkedHashMap的源代码:

   1) Entry元素:

   LinkedHashMap采用的hash算法和HashMap相同,但是它重新定义了数组中保存的元素Entry,该Entry除了保存当前对象的引用外,还保存了其上一个元素before和下一个元素after的引用,从而在哈希表的基础上又构成了双向链接列表。看源代码:

Java代码  收藏代码
  1. /** 
  2.  * 双向链表的表头元素。 
  3.  */  
  4. private transient Entry<K,V> header;  
  5.   
  6. /** 
  7.  * LinkedHashMap的Entry元素。 
  8.  * 继承HashMap的Entry元素,又保存了其上一个元素before和下一个元素after的引用。 
  9.  */  
  10. private static class Entry<K,V> extends HashMap.Entry<K,V> {  
  11.     Entry<K,V> before, after;  
  12.     ……  
  13. }  

    2) 初始化:

   通过源代码可以看出,在LinkedHashMap的构造方法中,实际调用了父类HashMap的相关构造方法来构造一个底层存放的table数组。如:

Java代码  收藏代码
  1. public LinkedHashMap(int initialCapacity, float loadFactor) {  
  2.     super(initialCapacity, loadFactor);  
  3.     accessOrder = false;  
  4. }  

    HashMap中的相关构造方法:

Java代码  收藏代码
  1. public HashMap(int initialCapacity, float loadFactor) {  
  2.     if (initialCapacity < 0)  
  3.         throw new IllegalArgumentException("Illegal initial capacity: " +  
  4.                                            initialCapacity);  
  5.     if (initialCapacity > MAXIMUM_CAPACITY)  
  6.         initialCapacity = MAXIMUM_CAPACITY;  
  7.     if (loadFactor <= 0 || Float.isNaN(loadFactor))  
  8.         throw new IllegalArgumentException("Illegal load factor: " +  
  9.                                            loadFactor);  
  10.   
  11.     // Find a power of 2 >= initialCapacity  
  12.     int capacity = 1;  
  13.     while (capacity < initialCapacity)  
  14.         capacity <<= 1;  
  15.   
  16.     this.loadFactor = loadFactor;  
  17.     threshold = (int)(capacity * loadFactor);  
  18.     table = new Entry[capacity];  
  19.     init();  
  20. }  

    我们已经知道LinkedHashMap的Entry元素继承HashMap的Entry,提供了双向链表的功能。在上述HashMap的构造器
中,最后会调用init()方法,进行相关的初始化,这个方法在HashMap的实现中并无意义,只是提供给子类实现相关的初始化调用。
   LinkedHashMap重写了init()方法,在调用父类的构造方法完成构造后,进一步实现了对其元素Entry的初始化操作。

Java代码  收藏代码
  1. void init() {  
  2.     header = new Entry<K,V>(-1nullnullnull);  
  3.     header.before = header.after = header;  
  4. }  

    3) 存储:

   LinkedHashMap并未重写父类HashMap的put方法,而是重写了父类HashMap的put方法调用的子方法void addEntry(int hash, K key, V value, int bucketIndex) 和void createEntry(int hash, K key, V value, int bucketIndex),提供了自己特有的双向链接列表的实现。

Java代码  收藏代码
  1. void addEntry(int hash, K key, V value, int bucketIndex) {  
  2.     // 调用create方法,将新元素以双向链表的的形式加入到映射中。  
  3.     createEntry(hash, key, value, bucketIndex);  
  4.   
  5.     // 删除最近最少使用元素的策略定义  
  6.     Entry<K,V> eldest = header.after;  
  7.     if (removeEldestEntry(eldest)) {  
  8.         removeEntryForKey(eldest.key);  
  9.     } else {  
  10.         if (size >= threshold)  
  11.             resize(2 * table.length);  
  12.     }  
  13. }  
Java代码  收藏代码
  1. void createEntry(int hash, K key, V value, int bucketIndex) {  
  2.     HashMap.Entry<K,V> old = table[bucketIndex];  
  3.     Entry<K,V> e = new Entry<K,V>(hash, key, value, old);  
  4.     table[bucketIndex] = e;  
  5.     // 调用元素的addBrefore方法,将元素加入到哈希、双向链接列表。  
  6.     e.addBefore(header);  
  7.     size++;  
  8. }  
Java代码  收藏代码
  1. private void addBefore(Entry<K,V> existingEntry) {  
  2.     after  = existingEntry;  
  3.     before = existingEntry.before;  
  4.     before.after = this;  
  5.     after.before = this;  
  6. }  

    4) 读取:

   LinkedHashMap重写了父类HashMap的get方法,实际在调用父类getEntry()方法取得查找的元素后,再判断当排序模式accessOrder为true时,记录访问顺序,将最新访问的元素添加到双向链表的表头,并从原来的位置删除。由于的链表的增加、删除操作是常量级的,故并不会带来性能的损失。

Java代码  收藏代码
  1. public V get(Object key) {  
  2.     // 调用父类HashMap的getEntry()方法,取得要查找的元素。  
  3.     Entry<K,V> e = (Entry<K,V>)getEntry(key);  
  4.     if (e == null)  
  5.         return null;  
  6.     // 记录访问顺序。  
  7.     e.recordAccess(this);  
  8.     return e.value;  
  9. }  
Java代码  收藏代码
  1. void recordAccess(HashMap<K,V> m) {  
  2.     LinkedHashMap<K,V> lm = (LinkedHashMap<K,V>)m;  
  3.     // 如果定义了LinkedHashMap的迭代顺序为访问顺序,  
  4.     // 则删除以前位置上的元素,并将最新访问的元素添加到链表表头。  
  5.     if (lm.accessOrder) {  
  6.         lm.modCount++;  
  7.         remove();  
  8.         addBefore(lm.header);  
  9.     }  
  10. }  

    5) 排序模式:

   LinkedHashMap定义了排序模式accessOrder,该属性为boolean型变量,对于访问顺序,为true;对于插入顺序,则为false。

Java代码  收藏代码
  1. private final boolean accessOrder;  

 一般情况下,不必指定排序模式,其迭代顺序即为默认为插入顺序。看LinkedHashMap的构造方法,如:

Java代码  收藏代码
  1. public LinkedHashMap(int initialCapacity, float loadFactor) {  
  2.     super(initialCapacity, loadFactor);  
  3.     accessOrder = false;  
  4. }  

    这些构造方法都会默认指定排序模式为插入顺序。如果你想构造一个LinkedHashMap,并打算按从近期访问最少到近期访问最多的顺序(即访问顺序)来保存元素,那么请使用下面的构造方法构造LinkedHashMap:

Java代码  收藏代码
  1. public LinkedHashMap(int initialCapacity,  
  2.          float loadFactor,  
  3.                      boolean accessOrder) {  
  4.     super(initialCapacity, loadFactor);  
  5.     this.accessOrder = accessOrder;  
  6. }  

    该哈希映射的迭代顺序就是最后访问其条目的顺序,这种映射很适合构建LRU缓存。LinkedHashMap提供了removeEldestEntry(Map.Entry<K,V> eldest)方法,在将新条目插入到映射后,put和 putAll将调用此方法。该方法可以提供在每次添加新条目时移除最旧条目的实现程序,默认返回false,这样,此映射的行为将类似于正常映射,即永远不能移除最旧的元素。

Java代码  收藏代码
  1. protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {  
  2.     return false;  
  3. }  

    此方法通常不以任何方式修改映射,相反允许映射在其返回值的指引下进行自我修改。如果用此映射构建LRU缓存,则非常方便,它允许映射通过删除旧条目来减少内存损耗。
   例如:重写此方法,维持此映射只保存100个条目的稳定状态,在每次添加新条目时删除最旧的条目。

Java代码  收藏代码
  1. private static final int MAX_ENTRIES = 100;  
  2. protected boolean removeEldestEntry(Map.Entry eldest) {  
  3.     return size() > MAX_ENTRIES;  
  4. }  

 

3. 相关说明:

   1) 在阅读本文前,请先了解:深入Java集合学习系列:HashMap的实现原理 

   2) 相关HashSet的实现原理,请参考:深入Java集合学习系列:HashSet的实现原理 

   3) 相关LinkedHashSet的实现原理,请参考:深入Java集合学习系列:LinkedHashSet的实现原理 


    LRU算法还可以通过计数来实现,缓存存储的位置附带一个计数器,当命中时将计数器加1,替换时就查找计数最小的位置并替换,结合访问时间戳来实现。这种 算法比较适合缓存数据量较小的场景,显然,遍历查找计数最小位置的时间复杂度为O(n)。我实现了一个,结合了访问时间戳,当最小计数大于 MINI_ACESS时,就移除最久没有被访问的项:
java 代码
 
  1. import java.io.Serializable;  
  2. import java.util.ArrayList;  
  3. import java.util.Collection;  
  4. import java.util.HashMap;  
  5. import java.util.Iterator;  
  6. import java.util.Map;  
  7. import java.util.Set;  
  8. import java.util.concurrent.atomic.AtomicInteger;  
  9. import java.util.concurrent.atomic.AtomicLong;  
  10. import java.util.concurrent.locks.Lock;  
  11. import java.util.concurrent.locks.ReentrantLock;  
  12.   
  13. /** 
  14.  *  
  15.  * @author dennis  
  16.  * 类说明:当缓存数目不多时,才用缓存计数的传统LRU算法 
  17.  * @param <K> 
  18.  * @param <V> 
  19.  */  
  20. public class LRUCache<K, V> implements Serializable {  
  21.   
  22.     private static final int DEFAULT_CAPACITY = 100;  
  23.   
  24.     protected Map<K, ValueEntry> map;  
  25.   
  26.     private final Lock lock = new ReentrantLock();  
  27.   
  28.     private final transient int maxCapacity;  
  29.   
  30.     private static int MINI_ACCESS = 10;  
  31.   
  32.     public LRUCache() {  
  33.         this(DEFAULT_CAPACITY);  
  34.     }  
  35.   
  36.     public LRUCache(int capacity) {  
  37.         if (capacity <= 0)  
  38.             throw new RuntimeException("缓存容量不得小于0");  
  39.         this.maxCapacity = capacity;  
  40.         this.map = new HashMap<K, ValueEntry>(maxCapacity);  
  41.     }  
  42.   
  43.     public boolean ContainsKey(K key) {  
  44.         try {  
  45.             lock.lock();  
  46.             return this.map.containsKey(key);  
  47.         } finally {  
  48.             lock.unlock();  
  49.         }  
  50.     }  
  51.   
  52.     public V put(K key, V value) {  
  53.         try {  
  54.             lock.lock();  
  55.             if ((map.size() > maxCapacity - 1) && !map.containsKey(key)) {  
  56.                 // System.out.println("开始");  
  57.                 Set<Map.Entry<K, ValueEntry>> entries = this.map.entrySet();  
  58.                 removeRencentlyLeastAccess(entries);  
  59.             }  
  60.             ValueEntry valueEntry = map.put(key, new ValueEntry(value));  
  61.             if (valueEntry != null)  
  62.                 return valueEntry.value;  
  63.             else  
  64.                 return null;  
  65.         } finally {  
  66.             lock.unlock();  
  67.         }  
  68.     }  
  69.   
  70.     /** 
  71.      * 移除最近最少访问 
  72.      */  
  73.     protected void removeRencentlyLeastAccess(  
  74.             Set<Map.Entry<K, ValueEntry>> entries) {  
  75.         // 最小使用次数  
  76.         int least = 0;  
  77.         // 最久没有被访问  
  78.         long earliest = 0;  
  79.         K toBeRemovedByCount = null;  
  80.         K toBeRemovedByTime = null;  
  81.         Iterator<Map.Entry<K, ValueEntry>> it = entries.iterator();  
  82.         if (it.hasNext()) {  
  83.             Map.Entry<K, ValueEntry> valueEntry = it.next();  
  84.             least = valueEntry.getValue().count.get();  
  85.             toBeRemovedByCount = valueEntry.getKey();  
  86.             earliest = valueEntry.getValue().lastAccess.get();  
  87.             toBeRemovedByTime = valueEntry.getKey();  
  88.         }  
  89.         while (it.hasNext()) {  
  90.             Map.Entry<K, ValueEntry> valueEntry = it.next();  
  91.             if (valueEntry.getValue().count.get() < least) {  
  92.                 least = valueEntry.getValue().count.get();  
  93.                 toBeRemovedByCount = valueEntry.getKey();  
  94.             }  
  95.             if (valueEntry.getValue().lastAccess.get() < earliest) {  
  96.                 earliest = valueEntry.getValue().count.get();  
  97.                 toBeRemovedByTime = valueEntry.getKey();  
  98.             }  
  99.         }  
  100.         // System.out.println("remove:" + toBeRemoved);  
  101.         // 如果最少使用次数大于MINI_ACCESS,那么移除访问时间最早的项(也就是最久没有被访问的项)  
  102.         if (least > MINI_ACCESS) {  
  103.             map.remove(toBeRemovedByTime);  
  104.         } else {  
  105.             map.remove(toBeRemovedByCount);  
  106.         }  
  107.     }  
  108.   
  109.     public V get(K key) {  
  110.         try {  
  111.             lock.lock();  
  112.             V value = null;  
  113.             ValueEntry valueEntry = map.get(key);  
  114.             if (valueEntry != null) {  
  115.                 // 更新访问时间戳  
  116.                 valueEntry.updateLastAccess();  
  117.                 // 更新访问次数  
  118.                 valueEntry.count.incrementAndGet();  
  119.                 value = valueEntry.value;  
  120.             }  
  121.             return value;  
  122.         } finally {  
  123.             lock.unlock();  
  124.         }  
  125.     }  
  126.   
  127.     public void clear() {  
  128.         try {  
  129.             lock.lock();  
  130.             map.clear();  
  131.         } finally {  
  132.             lock.unlock();  
  133.         }  
  134.     }  
  135.   
  136.     public int size() {  
  137.         try {  
  138.             lock.lock();  
  139.             return map.size();  
  140.         } finally {  
  141.             lock.unlock();  
  142.         }  
  143.     }  
  144.   
  145.     public Collection<Map.Entry<K, V>> getAll() {  
  146.         try {  
  147.             lock.lock();  
  148.             Set<K> keys = map.keySet();  
  149.             Map<K, V> tmp = new HashMap<K, V>();  
  150.             for (K key : keys) {  
  151.                 tmp.put(key, map.get(key).value);  
  152.             }  
  153.             return new ArrayList<Map.Entry<K, V>>(tmp.entrySet());  
  154.         } finally {  
  155.             lock.unlock();  
  156.         }  
  157.     }  
  158.   
  159.     class ValueEntry implements Serializable {  
  160.         private V value;  
  161.   
  162.         private AtomicInteger count;  
  163.   
  164.         private AtomicLong lastAccess;  
  165.   
  166.         public ValueEntry(V value) {  
  167.             this.value = value;  
  168.             this.count = new AtomicInteger(0);  
  169.             lastAccess = new AtomicLong(System.nanoTime());  
  170.         }  
  171.           
  172.         public void updateLastAccess() {  
  173.             this.lastAccess.set(System.nanoTime());  
  174.         }  
  175.   
  176.     }  
  177. }