解开LRU的神秘面纱

LRU

LRU是Least Recently Used的缩写,即最近最少使用。
----摘自百度百科

LRU最近最少使用,说的直白一点就是最久未使用。我们可以利用这一特点来做缓存的淘汰策略再合适不过了,当缓存满了的时候使用这种算法进行数据淘汰。
在Java里面LinkedHashMap自己实现了LRU,使用的数据结构是HashMap+双向链表,由于LinkedHashMap继承自HashMap,只是增加了数据有序与LRU这一部分,其余方法都是继承自HashMap,所以本篇文章只针对LinkedHashMap中关于LRU的部分来进行解读,如要看HashMap,请看我的另一篇文章:《HashMap2》。我们先来看一看LinkedHashMap是如何实现LRU的:
LinkedHashMap的源码如下:

LinkedHashMap中的一个内部类(实现双向链表):

 static class Entry<K,V> extends HashMap.Node<K,V> {
         //双向链表的头节点,尾节点
        Entry<K,V> before, after;
        Entry(int hash, K key, V value, Node<K,V> next) {
            super(hash, key, value, next);
        }
    }

以下是LinkedHashMap重要的三个属性:

    /**
     * 双向链表的头节点
     */
    transient LinkedHashMap.Entry<K,V> head;

    /**
     * 双向链表的尾节点
     */
    transient LinkedHashMap.Entry<K,V> tail;

    /**
    这个属性是控制LinkedHashMap中元素的顺序,是根据插入顺序排序,还是根据访问顺序排序  false代表按插入顺序  ,true代表按照访问顺序,此属性是能够实现LRU的关键
     */
    final boolean accessOrder;

LinkedHashMap的默认元素排序顺序:

 public LinkedHashMap() {
        super();
        //默认按照插入顺序排序
        accessOrder = false;
    }

我们先来看get流程:

public V get(Object key) {
        Node<K,V> e;
        //如果再LinkedHashMap中获取不到key对应的value,则返回空
        if ((e = getNode(hash(key), key)) == null)
            return null;
        //这个判断很重要,前面说过了,LinkedHashMap是默认按照插入顺序有序的,这里则表示按照访问顺序有序,那么我们如果访问到了这个元素,则要将他放到链表的尾部,这里为什么是头部而不是尾部,因为链表插入都是插入尾部呀,相当于是把原来位置的元素删掉,然后在尾部插入该元素
        if (accessOrder)
            afterNodeAccess(e);
          // 返回获取到的value
        return e.value;
    }

具体来看实现:

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;

            //如果b为null 那么表明p为头节点
            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;
        }
    }

LinkedHashMap的链表remove流程:
注:这里只是列出了维护链表的代码,具体的remove流程请看HashMap的removeNode()方法:

 void afterNodeRemoval(Node<K,V> e) { // unlink
        LinkedHashMap.Entry<K,V> p =
            (LinkedHashMap.Entry<K,V>)e, b = p.before, a = p.after;
        p.before = p.after = null;
        if (b == null)
            head = a;
        else
            b.after = a;
        if (a == null)
            tail = b;
        else
            a.before = b;
    }

LinkedHashMap正儿八经实现LRU的方法:


    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        //判断是否会触发LRU淘汰机制
        if (evict && (first = head) != null && removeEldestEntry(first)) {
           //移除头节点
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }



      //此方法默认返回false,就是说,LRU不会执行,这个方法其实就是LRU淘汰数据触发的条件
      //我们自己通过继承LinkedHashMap的时候就是通过重写此方法来实现LRU的。
      protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

总结一下上述的实现方式:
在我个人看来,要通过双向链表+HashMap实现LRU只有以下几个关键点:

  • 构造HashMap+双向链表的数据结构。
  • 在put的时候,不光要put进HashMap里面,而且还要将元素接到双向链表的末尾。
  • get的时候要将该元素放到双向链表的末尾
  • remove的时候要移除双向链表中的节点,即维护双向链表的关系。

有了以上思路,我们就可以自己实现简单的LRU算法:
下面我就以2种方式实现LRU:

  • 继承LinkedHashMap:

public class LRUCache2<K,V> extends LinkedHashMap<K,V> {
    //定义缓存的容量
    private int capacity;
    
    //默认的缓存大小值为100
    private static  final  int initialCapacity=100;

   

    //指定默认的缓存大小
    public LRUCache2(){
        this(initialCapacity);
    }



    public LRUCache2(int capacity) {
        //这个值一定要设置为true  才是根据访问顺序排序
        super(capacity,0.75f,true);
        this.capacity=capacity;
    }



    public V putVal(K k,V v){

        return put(k,v);
    }


    public V getVal(K k){
        return get(k);
    }

    public V removeVal(K k){
        return  remove(k);
    }


    //LRU淘汰机制触发的条件     关键在于重写这个方法
    @Override
    protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
        return size()>capacity;
    }


}
  • 自己构造双向链表+Hashmap

public class LRUCache {
	//双向链表节点
	class Node {
		Integer key;
		Integer value;
		Node next;
		Node pre;

		public Node() {
		}

		public Node(Integer key, Integer value) {
			this.key = key;
			this.value = value;
		}
	}

	//用于存储映射
	private HashMap<Integer, Node> map = new HashMap<>();

	//元素的个数
	private Integer size;

	//缓存的大小 容量
	//当size>capacity时  HashMap就需要淘汰数据了
	private Integer capacity;

	//双向链表的头尾节点
	private Node head;
	private Node tail;


	public LRUCache(Integer capacity) {
		this.capacity = capacity;
		this.size = 0;
		head = new Node();
		tail = new Node();
		//初始化链表  head<--->tail
		//这2个节点只是辅助节点  没有数据
		head.next = tail;
		tail.pre = head;
	}

    //LinkedHashMap里面是将元素插入到双向链表的尾部,此处我是将元素插入到头部的
	public int get(int key) {
		Node node = map.get(key);
		if (node == null) return -1;
		//如果key存在   先通过Hash表定位  然后移到链表头部
		removeToHead(node);
		return node.value;
	}

 
	public void put(int key, int value) {
		Node node = map.get(key);
		//说明 key不存在  那么则创建一个新的节点
		if (node == null) {
			Node newNode = new Node(key, value);
			map.put(key, newNode);
			addToHead(newNode);
			size++;
			//如果缓存已满   则删除链表的尾节点
			if (size > capacity) {
				//链表中移除尾节点
				Node node1 = removeTail();
				//删除对应的Hash映射
				map.remove(node1.key);
				size--;
			}
		} else {   //这个key存在  那么新的值覆盖旧的值   并放在链表的头部
			node.value = value;
			removeToHead(node);
		}
	}



   //移除元素   先删除HashMap中的映射,再维护链表关系
   public void remove(Object key){
         Node node=map.remove(key);
         deleteNode(node);
         

   }
	//将最近访问过的链表节点移动到头部(删除该访问的节点,然后再新建一个节点添加到头部)
	public void removeToHead(Node node) {
		deleteNode(node);
		addToHead(node);
	}


	//添加新的节点到头部
	public void addToHead(Node node) {
		node.pre = head;
		node.next = head.next;
		head.next.pre = node;
		head.next = node;
	}


	//删除末尾的节点(实际上是末尾的上一个节点,这个末尾节点没有实际数据)  缓存淘汰
	public Node removeTail() {
		//获取尾节点的前一个节点  并删除
		Node node = tail.pre;
		deleteNode(node);
		return node;
	}

	//删除节点
	public void deleteNode(Node node) {
		node.pre.next = node.next;
		node.next.pre = node.pre;
	}
}

附上LinkedHashMap的图解:作者实在懒得画,网上找的。
在这里插入图片描述

作者感想:
写这一篇LRU原因有以下几点,首先java中的LRU在LinkedHashMap中实现了的,我们通过LRU可以更好的理解LinkedHashMap的数据结构与工作原理;再者LRU本身是很重要的一个算法,通过这一篇文章,我们可以理解LRU的实现,还可以自己手写LRU;还有一个很重要的原因,上一次面试某家互联网企业,面试官让我手撕LRU,我说了大概思路,但是在细节上没有回答好,非非我只能卒!!!哎,菜是原罪!!!希望各位小伙伴们看了我的文章,能有一点收获吧。

码字不易,如果觉得好,请不要吝啬你们的大拇指,点个赞,嘿嘿嘿。!!!

你们的给力,就是我的动力,我是爱生活的袁非非!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值