用LindedHashMap实现LRU算法
LRU,最近最少使用。如果我们用一个数据结构来实现LRU的话,那么需要满足两个条件,第一个该数据结构需要存储最近使用或未使用的,第二个,需要限制这个数据结构的大小。
我们用LindedHashMap实现LRU,第一需要设置accessOrder,在默认情况下,accessOrder是为false,表示顺序为插入顺序。为true时,表示会根据访问顺序排序(在get时),最新使用的排在尾巴上。初始化设置accessOrder的源码如下:
/** * Constructs an empty <tt>LinkedHashMap</tt> instance with the * specified initial capacity, load factor and ordering mode. * * @param initialCapacity the initial capacity * @param loadFactor the load factor * @param accessOrder the ordering mode - <tt>true</tt> for * access-order, <tt>false</tt> for insertion-order * @throws IllegalArgumentException if the initial capacity is negative * or the load factor is nonpositive */ public LinkedHashMap(int initialCapacity, float loadFactor, boolean accessOrder) { super(initialCapacity, loadFactor); this.accessOrder = accessOrder; }
在get或者方法里都会调用afterNodeAccess,这个方法就是把当前node放在尾巴上(看不明白没关系,看到这个注释了吗,// move node to last,hhhhhh)
void afterNodeAccess(Node<K,V> e) { // move node to last LinkedHashMap.Entry<K,V> last; if (accessOrder && (last = tail) != e) { LinkedHashMap.Entry<K,V> p = (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after; p.after = null; if (b == null) head = a; else b.after = a; if (a != null) a.before = b; else last = b; if (last == null) head = p; else { p.before = last; last.after = p; } tail = p; ++modCount; } }
这样,设置了accessOrder=true,我们就满足了第一个条件,此时LinkedHashMap的顺序是访问顺序,最新使用的在后面。然后我们需要重写removeEldestEntry这个方法,这个方面默认是返回false,表示不需要移除第一个,需要重写它,表示需要移除第一个,即最近未使用的。在源码里也有说明:
/** *......... * <p>Sample use: this override will allow the map to grow up to 100 * entries and then delete the eldest entry each time a new entry is * added, maintaining a steady state of 100 entries. * <pre> * private static final int MAX_ENTRIES = 100; * * protected boolean removeEldestEntry(Map.Entry eldest) { * return size() > MAX_ENTRIES; * } * </pre> * *........... */ protected boolean removeEldestEntry(Map.Entry<K,V> eldest) { return false; }
所以我们只需要这样重写,当Map的最大值大于设定的最大值时,需要移除第一个:
LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer,Integer>(maxSize, 0.75f, true){ @Override protected boolean removeEldestEntry(Map.Entry<Integer,Integer> eldest) { return this.size() > maxSize; } };
这样重写就好啦,当我们每次使用LinkedHashMap里的元素时,使用的这个元素就会放在最后面,最长未使用的就会在前面。当新加的元素超过Map的大小时,就会移除第一个,然后把新加的放在最后面。
附上测试代码和结果;
/** * main * * @description 测试 * @author zhui * @date 2020-12-23 14:09 * @version v1.0.0 */ public class main { private static ExecutorService executorService; public static void main(String[] args) throws InterruptedException{ createThreadPool(); LRUByLinkedHashMap(3); } /** * @description 创建一个单线程,用来测试 * @return void **/ public static void createThreadPool(){ executorService = Executors.newSingleThreadExecutor(); } /** * @description 测试代码 * @return void **/ public static void LRUByLinkedHashMap(int maxSize) throws InterruptedException{ // LRU的LinkedHashMap LinkedHashMap<Integer, Integer> map = new LinkedHashMap<Integer,Integer>(maxSize, 0.75f, true){ @Override protected boolean removeEldestEntry(Map.Entry<Integer,Integer> eldest) { return this.size() > maxSize; } }; for (int i = 1; i <= maxSize; i++) { map.put(i, 0); } System.out.println("初始map:" +map); // 测试 for (int i = 1; i <= 10; i++){ int temp = randInt(1,100); if (temp % 2 == 0) { // 对map的key随机get randomGetValue(maxSize, map); }else { // 对map进行随机put randomPutValue(maxSize,map); } } // 关闭单线程 executorService.shutdown(); } /** * @description 对map随机取值,观察其顺序变化 * @param maxSize map大小,作为key取值范围 * @param map map **/ public static void randomGetValue(int maxSize, Map<Integer, Integer> map) throws InterruptedException{ final CountDownLatch end = new CountDownLatch(maxSize); for(int i=1; i<= maxSize; i++){ executorService.execute(() -> { int key = randInt(1, maxSize); forGetValueByRandom(key, map); System.out.println("key=" + key + ",随机取值后map:" + map); end.countDown(); }); } end.await(); } /** * @description 给map新put 1个键值对,观察剩下的map变化 * @param maxSize map的size大小,决定新键值对的下限 * @param map map * @return void **/ public static void randomPutValue(int maxSize, Map<Integer, Integer> map){ executorService.execute(() -> { int key = randInt(maxSize+1, 10); forGetValueByRandom(key, map); System.out.println("插入" + key + "后,map:" + map); }); } /** * @description 根据key,随机对这个key进行get1次,没有则添加一个键值对,同时更新其value * @param key key * @param map map * @return void **/ public static void forGetValueByRandom(int key, Map<Integer, Integer> map){ Integer num = map.getOrDefault(key, 0); map.put(key, num + 1); } /** * @description 在一个范围内随机取值 * @param min 随机取值最小值 * @param max 随机取值最大值 * @return int **/ public static int randInt(int min, int max) { Random rand = new Random(); int randomNum = rand.nextInt((max - min) + 1) + min; return randomNum; } }
然后我们看些结果,是否和预期一样呢?可以看到新加的元素或者刚使用的元素,都会在最后面,简直完美!
初始map:{1=0, 2=0, 3=0} 插入9后,map:{2=0, 3=0, 9=1} //1被移除了 key=3,随机取值后map:{2=0, 9=1, 3=1} //3放到最后面 key=1,随机取值后map:{9=1, 3=1, 1=1} //2被移除,1在最后面 key=3,随机取值后map:{9=1, 1=1, 3=2} //3在最后面 插入8后,map:{1=1, 3=2, 8=1} //8在最后面 插入7后,map:{3=2, 8=1, 7=1} //7在最后面 key=3,随机取值后map:{8=1, 7=1, 3=3} //3在最后面 key=3,随机取值后map:{8=1, 7=1, 3=4} //3在最后面 key=2,随机取值后map:{7=1, 3=4, 2=1} //2在最后面 key=1,随机取值后map:{3=4, 2=1, 1=1} //1在最后面 key=1,随机取值后map:{3=4, 2=1, 1=2} //1在最后面 key=2,随机取值后map:{3=4, 1=2, 2=2} //2在最后面 key=2,随机取值后map:{3=4, 1=2, 2=3} //2在最后面 key=1,随机取值后map:{3=4, 2=3, 1=3} //1在最后面 key=1,随机取值后map:{3=4, 2=3, 1=4} //1在最后面 插入8后,map:{2=3, 1=4, 8=1} //8在最后面 插入4后,map:{1=4, 8=1, 4=1} //4在最后面 插入6后,map:{8=1, 4=1, 6=1} //6在最后面