Java 自定义实现 LRU 缓存算法

Java 自定义实现 LRU(最近最少使用) 缓存算法

方式一基于LinkedHashMap实现LRU缓存算法

LinkedHashMap

  1. LinkedHashMap继承自HashMap,内部提供了一个removeEldestEntry方法,该方法正是实现LRU策略的关键所在, 且HashMap内部专门为LinkedHashMap提供了3个专用回调方法,afterNodeAccess、 afterNodeInsertion、afterNodeRemoval,这3个方法的字面意思非常容易理解,就是节点访问后、节点插入后、节点删除后 分别执行的行为。基于以上行为LinkedHashMap就可以实现一个LRUCache的功能了。
  2. 关于LinkedHashMap的eldest:eldest字面意思为最老的,LinkedHashMap中有个叫做accessOrder的字 段,当accessOrder为true时表示LinkedHashMap内部节点按照访问次数排序,最老的节点也就是访问最少的节点。当 accessOrder为false时表示LinkedHashMap内部节点按照插入顺序排序,最老的节点也就是最早插入的节点,该值默认为 false。

代码实现

class LRUCache extends LinkedHashMap<Integer,Integer>{

    private int capacity;

    public LRUCache(int capacity) {
        //初始化LinkedHashMap
        super(capacity,0.75F,true);
        this.capacity = capacity;
    }

    public int get(int key) {
        return super.getOrDefault(key,-1);
    }

    public void put(int key, int value) {
        super.put(key,value);
    }
    @Override
    protected boolean removeEldestEntry(Map.Entry<Integer, Integer> eldest) {
        return size() > capacity;
    }
}

方式二自定义双向链表 + 哈希表实现

实现思路

  1. 通过哈希表辅以双向链表实现,我们用一个哈希表和一个双向链表维护所有在缓存中的键值对。
  2. 双向链表按照被使用的顺序存储了这些键值对,靠近头部的键值对是最近使用的,而靠近尾部的键值对是最久未使用的
  3. 哈希表即为普通的哈希映射(HashMap),通过缓存数据的键映射到其在双向链表中的位置

代码实现

class LRUCache {
    //自定义双向链表
    class LinkedNode {
        int key;
        int value;
        LinkedNode prev;//前驱结点
        LinkedNode next;//后继结点
        public LinkedNode() {};
        public LinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    //哈希表
    private Map<Integer, LinkedNode> cache = new HashMap<>();
    private int size;
    private int capacity;
    //头结点和尾结点
    private LinkedNode head, tail;

    //构造器,初始化容量
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        //使用伪头结点和伪为节点
        head = new LinkedNode();
        tail = new LinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    public int get(int key){
        //从hash表中根据key获取双向链表结点位置达到O(1)的时间复杂度
        LinkedNode linkedNode = cache.get(key);
        //如果hash表中没有值,那么返回默认值-1
        if (linkedNode == null) return -1;
        //如果存在,把这个值移动到双向链表的头部
        //链表的头部记录了最近使用过的结点,尾部记录了最近不常被使用的结点
        moveToHead(linkedNode);
        return linkedNode.value;
    }

    public void put(int key, int value) {
        LinkedNode node = cache.get(key);
        if (node == null) {
            // 如果 key 不存在,创建一个新的节点
            LinkedNode newNode = new LinkedNode(key, value);
            // 添加进哈希表
            cache.put(key, newNode);
            // 添加至双向链表的头部
            addToHead(newNode);
            size++;
            if (size > capacity) {
                // 如果超出容量,删除双向链表的尾部节点
                LinkedNode tail = removeTail();
                // 删除哈希表中对应的项
                cache.remove(tail.key);
                size--;
            }
        }
        else {
            // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
            node.value = value;
            moveToHead(node);
        }
    }

    //把节点移动到头部
    private void moveToHead(LinkedNode node){
        //先删除,后添加
        removeNode(node);
        addToHead(node);
    }
    //添加结点到链表头部
    private void addToHead(LinkedNode node){
        //伪头结点的后继结点就是当前要插入的结点
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    //删除结点
    private void removeNode(LinkedNode node){
        LinkedNode prev = node.prev;
        prev.next = node.next;
        node.next.prev = prev;
    }
    //删除尾部结点
    private LinkedNode removeTail(){
        //因为使用伪头尾结点的原因,伪尾结点的前驱结点就是实际元素结点的尾部
        LinkedNode prev = tail.prev;
        removeNode(prev);
        return prev;
    }
}

方式三借助ReadWriteLock(读写锁)+ 双向链表 + 哈希表实现线程安全的LRU缓存算法

实现思路

  1. 借助ReadWriteLock(读写锁)+ 双向链表 + 哈希表实现线程安全的LRU缓存算法,当get的时候加上读锁,当put的时候加上写锁

代码实现

class LRUCache {
    //读写锁对象
    private ReadWriteLock readWriteLock = new ReentrantReadWriteLock();

    //自定义双向链表
    class LinkedNode {
        int key;
        int value;
        LinkedNode prev;//前驱结点
        LinkedNode next;//后继结点
        public LinkedNode() {};
        public LinkedNode(int _key, int _value) {key = _key; value = _value;}
    }

    //哈希表
    private Map<Integer, LinkedNode> cache = new HashMap<>();
    private int size;
    private int capacity;
    //头结点和尾结点
    private LinkedNode head, tail;
    //构造器,初始化容量
    public LRUCache(int capacity) {
        this.size = 0;
        this.capacity = capacity;
        //使用伪头结点和伪为节点
        head = new LinkedNode();
        tail = new LinkedNode();
        head.next = tail;
        tail.prev = head;
    }

    //当进行get时加上读锁
    public int get(int key){
        //上读锁
        readWriteLock.readLock().lock();
        try {
            //从hash表中根据key获取双向链表结点位置达到O(1)的时间复杂度
            LinkedNode linkedNode = cache.get(key);
            //如果hash表中没有值,那么返回默认值-1
            if (linkedNode == null) return -1;
            //如果存在,把这个值移动到双向链表的头部
            //链表的头部记录了最近使用过的结点,尾部记录了最近不常被使用的结点
            moveToHead(linkedNode);
            return linkedNode.value;
        }finally {
            //释放读锁
            readWriteLock.readLock().unlock();
        }
    }

    //当进行put操作时加上写锁
    public void put(int key, int value) {
        //上写锁
        readWriteLock.writeLock().lock();
        try {
            LinkedNode node = cache.get(key);
            if (node == null) {
                // 如果 key 不存在,创建一个新的节点
                LinkedNode newNode = new LinkedNode(key, value);
                // 添加进哈希表
                cache.put(key, newNode);
                // 添加至双向链表的头部
                addToHead(newNode);
                ++size;
                if (size > capacity) {
                    // 如果超出容量,删除双向链表的尾部节点
                    LinkedNode tail = removeTail();
                    // 删除哈希表中对应的项
                    cache.remove(tail.key);
                    --size;
                }
            }
            else {
                // 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部
                node.value = value;
                moveToHead(node);
            }
        }finally {
            readWriteLock.writeLock().unlock();
        }
    }

    //把节点移动到头部
    private void moveToHead(LinkedNode node){
        //先删除,后添加
        removeNode(node);
        addToHead(node);
    }
    //添加结点到链表头部
    private void addToHead(LinkedNode node){
        //伪头结点的后继结点就是当前要插入的结点
        node.prev = head;
        node.next = head.next;
        head.next.prev = node;
        head.next = node;
    }
    //删除结点
    private void removeNode(LinkedNode node){
        LinkedNode prev = node.prev;
        prev.next = node.next;
        node.next.prev = prev;
    }
    //删除尾部结点
    private LinkedNode removeTail(){
        //因为使用伪头尾结点的原因,伪尾结点的前驱结点就是实际元素结点的尾部
        LinkedNode prev = tail.prev;
        removeNode(prev);
        return prev;
    }
}

最后由于个人技术水平有限,本文难免有不足的地方,请各位给予指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值