利用LinkedHashMap实现LRU算法缓存

LRU原理

LRU 是 Least Recently Used 的缩写,这种算法认为最近使用的数据是热门数据,下一次很大概率将会再次被使用。而最近很少被使用的数据,很大概率下一次不再用到。当缓存容量的满时候,优先淘汰最近很少使用的数据。

假如现在的用户缓存如下:

  1. 假设我们使用哈希链表来缓存用户信息,目前缓存了四个用户,这四个用户依次按照时间顺序从链表右端插入
    在这里插入图片描述
  2. 此时,用户访问用户005,由于哈希表中没有用户005的数据,所以我们从数据库中读取出来,插入到缓存中。这时候,链表中最右端是最新访问的用户005,最左端是最近最少的用户001。
    在这里插入图片描述
    3.接下来,业务访问用户002,哈希表中存在用户002,那么我们会怎么办呢? 我们吧用户002从节点中移除掉,重新插入到最右端。这时候,链表中最右端变成了最新访问到的用户002,最左端仍然是最近最少访问的用户的001。

在这里插入图片描述

4.接下来,业务修改用户004的信息,那么我们就把用户004从原来的位置上移动到链表最右侧,并把用户信息的值更新掉。这时候,链表中最右端是最新访问到的用户004,最左端仍然是最近最少访问的用户001。

在这里插入图片描述
5.后来业务访问用户006,用户006不在缓存中,需要从数据库中查询出来,然后加入到哈希表中。假设这个时候哈希表的容量已经达到上限,必须先删除最近最少访问的数据,那么位于哈希链表最左端的用户001就会被删除掉,然后再把006插入到最右端。

在这里插入图片描述
在这里插入图片描述
以上就是LRU算法的基本思路。

LRU 实现一

实现代码如下:

public class Node {
    public Node pre;
    public Node next;
    public String key;
    public String value;

    public Node(String key ,String value){
        this.key = key;
        this.value = value;
    }
}
public class LRUCache {
    private Node head;
    private Node  end;
    //缓存存储上限
    private int limit;
    private HashMap<String,Node> hashMap;
    public LRUCache(int limit){
        this.limit = limit;
        hashMap=new HashMap<>();
    }

    public String get(String key){
        Node node = hashMap.get(key);
        if(node == null){
            return null;
        }
        refreshNode(node);
        return node.value;
    }

    public void put(String key,String value){
        Node node = hashMap.get(key);
        if(node == null){
            // key 不存在,则插入 key - value
            if(hashMap.size() >= limit){
                String  oldKey = removeNode(head);
                hashMap.remove(oldKey);
            }
            node = new Node(key,value);
            addNode(node);
            hashMap.put(key,node);
        }else {
            //如果key存在,刷新key- value
            node.value = value;
            refreshNode(node);
        }
    }

    public void remove(String key){
        Node node = hashMap.get(key);
        removeNode(node);
        hashMap.remove(key);
    }

    /**
     * 刷新被访问的节点位置
     * @param node
     */
    private void refreshNode(Node node) {
        // 如果访问的是尾节点,则无须移动节点
        if(node == end){
            return;
        }
        //删除节点
        removeNode(node);
        //重新插入节点
        addNode(node);
    }

    /**
     * 尾部插入节点
     * @param node
     */
    private void addNode(Node node) {
        if(end != null){
            end.next = node;
            node.pre = end;
            node.next=null;
        }
        end = node;
        if(head == null){
            head = node;
        }
    }

    /**
     * 删除节点
     * @param node
     */
    private String removeNode(Node node) {
        if(node == end){
            // 移除尾结点
            end = end.pre;
        }else if(node == head){
            // 移除头节点
            head = head.next;
        }else{
            //移除中间节点
            node.pre.next = node.next;
            node.next.pre = node.pre;
        }
        return node.key;
    }

    public static void main(String[] args) {
        LRUCache lruCache = new LRUCache(5);

        lruCache.put("001","张三");
        lruCache.put("002","李四");
        lruCache.put("003","王五");
        lruCache.put("004","tom");

        lruCache.put("005","Jack");

        lruCache.get("002");

        lruCache.put("004","(修改)李四");

        lruCache.put("006","andy");

    }
}

上面代码不是线程安全的,要想做到线程安全,需要 加上synchronized修饰符。

LRU实现二

由于LinkedHashMap 支持按访问顺序排序双向链表的特性,所以可以基于LinkedHashMap来实现一个LRU缓存,在缓存类中,重写removeEldestEntry方法来定义删除最近最少访问的节点的条件,方法源码如下:

/**
     * Returns <tt>true</tt> if this map should remove its eldest entry.
     * This method is invoked by <tt>put</tt> and <tt>putAll</tt> after
     * inserting a new entry into the map.  It provides the implementor
     * with the opportunity to remove the eldest entry each time a new one
     * is added.  This is useful if the map represents a cache: it allows
     * the map to reduce memory consumption by deleting stale entries.
     *
     * <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>
     *
     * <p>This method typically does not modify the map in any way,
     * instead allowing the map to modify itself as directed by its
     * return value.  It <i>is</i> permitted for this method to modify
     * the map directly, but if it does so, it <i>must</i> return
     * <tt>false</tt> (indicating that the map should not attempt any
     * further modification).  The effects of returning <tt>true</tt>
     * after modifying the map from within this method are unspecified.
     *
     * <p>This implementation merely returns <tt>false</tt> (so that this
     * map acts like a normal map - the eldest element is never removed).
     *
     * @param    eldest The least recently inserted entry in the map, or if
     *           this is an access-ordered map, the least recently accessed
     *           entry.  This is the entry that will be removed it this
     *           method returns <tt>true</tt>.  If the map was empty prior
     *           to the <tt>put</tt> or <tt>putAll</tt> invocation resulting
     *           in this invocation, this will be the entry that was just
     *           inserted; in other words, if the map contains a single
     *           entry, the eldest entry is also the newest.
     * @return   <tt>true</tt> if the eldest entry should be removed
     *           from the map; <tt>false</tt> if it should be retained.
     */
    protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
        return false;
    }

实现一个LRUCache 很容易,几十行代码即可

public class LRUCache2 extends LinkedHashMap {
    private int maxElements;
    public LRUCache2(int maxElements){
         super(maxSize, 0.75F, true);
        maxElements = maxElements;
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry eldest) {
        return size() > maxElements;
    }
}

LinkedHashMap可以实现LRU算法的缓存基于两点:
1、LinkedList首先它是一个Map,Map是基于K-V的,和缓存一致
2、LinkedList提供了一个boolean值可以让用户指定是否实现LRU

我们看一下LinkedHashMap 自带boolean 型参数的构造方法

public LinkedHashMap(int initialCapacity,
         float loadFactor,
                     boolean accessOrder) {
    super(initialCapacity, loadFactor);
    this.accessOrder = accessOrder;
}

accessOrder 的含义

  • false : 所有Entry 按照插入的顺序排列
  • true: 所有的Entry 按照访问的顺序排列

所以要想实现LRU ,accessOrder 必须设置为 true ,如果 有 1 2 3 Entry元素,如果访问1,则1 会移动到末尾去,每次要淘汰的数据的时候,双向队列嘴头的那个数据就是最不常访问的元素,换句话说,双向链表最头的那个数据就是要淘汰的数据。

什么是访问呢? 比如 get 和 put

下面我们从源码上分析get和put 方法

put方法
LinkedHashMap 的 put 方法会直接调用 HashMap 的put 方法,但是方法里会预留方法给LinkedHashMap 访问:


 final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
       ......
	   
        if ((p = tab[i = (n - 1) & hash]) == null)
		    // LinkedHashMap 会重写 newNode方法
            tab[i] = newNode(hash, key, value, null);
        else {
             ......
            }
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
			    // LinkedHashMap 会重写 afterNodeAccess 方法
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        if (++size > threshold)
            resize();
		// LinkedHashMap 会重写 afterNodeInsertion 方法
        afterNodeInsertion(evict);
        return null;
    }

我们先不考虑红黑树

  1. newNode 方法

通过上述两个方法来创建自己的节点,并对before 和 after 进行操作。

    Node<K,V> newNode(int hash, K key, V value, Node<K,V> e) {
        LinkedHashMap.Entry<K,V> p =
            new LinkedHashMap.Entry<K,V>(hash, key, value, e);
        linkNodeLast(p);
        return p;
    }
        private void linkNodeLast(LinkedHashMap.Entry<K,V> p) {
        LinkedHashMap.Entry<K,V> last = tail;
        tail = p;
        if (last == null)
            head = p;
        else {
            p.before = last;
            last.after = p;
        }
    }

无论是插入顺序还是LRU顺序,新插入的节点都被放入到末尾。

而在HashMap的putVal 方法末尾有这两个方法判断

  if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
			    // LinkedHashMap 会重写 afterNodeAccess 方法
                afterNodeAccess(e);
                return oldValue;
            }
        }

上面代码的意思,当key存在时,直接更新值,afterNodeAccess方法被调用,该方法在HashMap 中为空,在LinkedHashMap中实现,作用是对accessOrder 为 true 情况( LRU 顺序) 下 将该节点调到末尾,因为它被改动了。

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;
        }
    }

将e节点的前一个节点b与后一个节点a连在一起,将e调到末尾。LRU顺序下,末尾节点代表着最新的节点,意思是要么是新插入的,被更改的,被get访问到的。

  if (++size > threshold)
            resize();
		// LinkedHashMap 会重写 afterNodeInsertion 方法
        afterNodeInsertion(evict);

上面的代码的意思是,插入新节点后,对于LinkedHashMap 来说要进行afterNodeInsertion 操作,主要作用判断是否要删除head节点,你可以重写removeEldestEntry 方法,执行自己的逻辑,比如数量超过某个值后插入新值会删除最久未使用的值,即头结点。

    void afterNodeInsertion(boolean evict) { // possibly remove eldest
        LinkedHashMap.Entry<K,V> first;
        if (evict && (first = head) != null && removeEldestEntry(first)) {
            K key = first.key;
            removeNode(hash(key), key, null, false, true);
        }
    }

get方法

    public V get(Object key) {
        Node<K,V> e;
        if ((e = getNode(hash(key), key)) == null)
            return null;
        if (accessOrder)
            afterNodeAccess(e);
        return e.value;
    }

具体实现在HashMap的getNode方法里,可以看到若是LRU顺序,则被访问的节点会被放入到末尾。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

半夏_2021

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值