百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程字典 > 正文

浅谈LRU、LFU缓存,或许你可以用得到

toyiye 2024-06-27 00:52 20 浏览 0 评论

遵循要面向接口,面向抽象编程的原则,首先建个缓存抽象类:

public abstract class Cache<K, V> {
    //缓存容量
    protected int capacity;
    protected Cache(int capacity) {
          this.capacity = capacity;
     }
    /**
    *获取缓存
    */
    public abstract V get(K key);
    /**
    *保存缓存
    */
    public abstract void put(K key, V value);
}

下面先实现 LRU(优先淘汰最久未使用缓存) 为了能尽量达到 o(1)的时间复杂度,本例采用 map 作为存储结构 而施行淘汰策略时如何找到那个最久未使用的缓存,为了也能达到 o(1)的时间复杂度,貌似只能用双向链表结构,而这个节点的值存储的就是缓存的键,只要保证每次使用缓存就将该节点压入尾部即可,基本思路很简单,下面上数据结构:

class Node<E> {
    //这里用于存储缓存键
    volatile E val;
    volatile Node<E> next;
    volatile Node<E> prev;
    //以下是一些关于 cas 的操作,可找相关文档或者源码了解如何使用
    //相对锁来说避免的因线程的挂起导致频繁内核状态的切换带来的开销
    static final sun.misc.Unsafe unsafe;
    private static final long valOffset;
    private static final long nextOffset;
    private static final long prevOffset;

    static {
          try {
               Field unsafeField = Unsafe.class.getDeclaredFields()[0];
               unsafeField.setAccessible(true);
               Unsafe temp = null;
               try {
                 temp = (Unsafe) unsafeField.get(null);
               } catch (IllegalArgumentException | IllegalAccessException e) {
                 temp = null;
               }
               unsafe = temp;
               temp = null;
               Class<?> k = Node.class;
               valOffset = unsafe.objectFieldOffset(k.getDeclaredField("val"));
               nextOffset = unsafe.objectFieldOffset(k.getDeclaredField("next"));
               prevOffset = unsafe.objectFieldOffset(k.getDeclaredField("prev"));
         } catch (Exception e) {
               throw new Error(e);
          }
    }

    Node(E val) {
         unsafe.putObject(this, valOffset, val);
     }

    boolean casNext(Node<E> expect, Node<E> next) {
         return unsafe.compareAndSwapObject(this, nextOffset, expect, next);
     }

    boolean casPrev(Node<E> expect, Node<E> prev) {
          return unsafe.compareAndSwapObject(this, prevOffset, expect, prev);
     }

    /**
    *弹出当前节点,当获取一个已经存在的缓存时,需要将该缓存对应的节点弹出双向链表,并压入尾部
    */
    void poll() {
          Node<E> prev;
          Node<E> next;
          do {
               prev = this.prev;
               next = this.next;
          } while (!prev.casNext(this, next) || !next.casPrev(this, prev));
     }

    /**
    *将当前节点压入尾部,一般是被刚弹出的节点或者新加入的缓存节点
    */
    void offer(Node<E> tail) {
          Node<E> prev;
          do {
               prev = tail.prev;
               this.prev = prev;
               this.next = tail;
          } while (!prev.casNext(tail, this) || !tail.casPrev(prev, this));
     }

}

下面贴出 LRUCahce 完整类结构:

public class LRUCache<K, V> extends Cache<K, V> {

    private static class LRUCacheValue<K, V> {
        //该节点的值指向该缓存在 map 中的 key
        private volatile Node<K> node;
        //具体的缓存值
          private volatile V val;
        private LRUCacheValue(K key, V value) {
               node = new Node<>(key);
               val = value;
          }
    }

     //存放缓存的 map
     private final ConcurrentHashMap<K, LRUCacheValue<K, V>> cache;
     //头节点,该节点的 next 节点即最久未使用的缓存
     private volatile Node<K> head;
     //尾节点,该节点的 prev 节点即刚使用过的缓存或刚插入的缓存
     private volatile Node<K> tail;
     //用于并发控制新增缓存的状态值,为 0 表示可以新增缓存
     volatile int state;
     private static final long headOffset;
     private static final long tailOffset;
     private static final long stateOffset;
     static {
          try {
            Class<?> k = LRUCache.class;
            headOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("head"));
            tailOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("tail"));
            stateOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("state"));
          } catch (Exception e) {
            throw new Error(e);
          }
      }

    private boolean casHead(Node<K> expect, Node<K> head) {
          return Node.unsafe.compareAndSwapObject(this, headOffset, expect, head);
    }

    private boolean casTail(Node<K> expect, Node<K> tail) {
          return Node.unsafe.compareAndSwapObject(this, tailOffset, expect, tail);
     }

    /**
    *判断是否可以新增缓存
    */
    private boolean casState(int expect, int state) {
          return Node.unsafe.compareAndSwapInt(this, stateOffset, expect, state);
    }

    public LRUCache(int capacity) {
         super(capacity);
          cache = capacity < 64 ? new ConcurrentHashMap<>(capacity + 1, 1.0f) : new ConcurrentHashMap<>();
          casHead(null, new Node<>(null));
          casTail(null, new Node<>(null));
          head.casNext(null, tail);
         tail.casPrev(null, head);
     }

    @Override
     public V get(K key) {
          LRUCacheValue<K, V> lruCacheValue = cache.get(key);
          if (lruCacheValue != null) {
               // 如果存在则弹出
               poll(lruCacheValue.node);
               // 压入尾节点前,表示最近使用过
               offer(lruCacheValue.node);
          }
          return lruCacheValue == null ? null : lruCacheValue.val;
     }

    private K poll(Node<K> node) {
          if (node == null || node == head || node == tail)
               return null;
          node.poll();
          return node.val;
     }

    private void offer(Node<K> node) {
          if (node == null)
               return;
           node.offer(tail);
     }

    @Override
     public void put(K key, V value) {
          restartFromHead: for (;;) {
               LRUCacheValue<K, V> lruCacheValue = cache.get(key);
               if (lruCacheValue != null) {
                    // 如果存在则弹出
                    poll(lruCacheValue.node);
                     // 更新缓存值
                    lruCacheValue.val = value;
                     cache.put(key, lruCacheValue);
                     // 压入尾节点前,表示最近使用过
                    offer(lruCacheValue.node);
               } else {
                    // 不存在缓存
                    if (!casState(0, 1))
                         // 这里利用 cas 替换锁,分不同情况也可以选择加锁,确保不被 new 多个对象
                         continue restartFromHead;
                    lruCacheValue = new LRUCacheValue<>(key, value);
                    K k = null;
                    // 判断缓存容量并弹出头节点的 next 节点
                    while (cache.size() >= capacity && (k = poll(head.next)) != null)
                         cache.remove(k);
                     // 保存新缓存
                    cache.put(key, lruCacheValue);
                     // 将新缓存节点压入尾节点前
                    offer(lruCacheValue.node);
                     // 恢复可 new 状态
                     state = 0;
               }
              break;
          }
     }

}

下面测试下:

public class LRUCacheTest {
    public static void main(String[] args) throws InterruptedException {
        LRUCache<Integer, Integer> lruCache = new LRUCache<>(2);
        lruCache.put(1, 1);
        lruCache.put(2, 2);
        System.out.println(lruCache.get(2));
        System.out.println("ans:2");
        lruCache.put(3, 3);
        System.out.println(lruCache.get(1));
        System.out.println(lruCache.get(2));
        System.out.println(lruCache.get(3));
        System.out.println("ans:null,2,3");
        lruCache.put(4, 4);
        System.out.println(lruCache.get(1));
        System.out.println(lruCache.get(2));
        System.out.println(lruCache.get(3));
        System.out.println(lruCache.get(4));
        System.out.println("ans:null,null,3,4");
     }

}

LRU 完成,下面上 LFU 代码:

LFU(优先淘汰最不经常使用缓存,若使用频率相同则按 LRU 策略淘汰) 相比 LRU 多了一个条件,使用频率 存储结构还是那个 map,节点依然还是那个节点 我们需要对使用频率经行排序,我想到的是用 TreeMap 来存储头节点和尾节点,使用频率作为 key,map 的 value 中再加一个属性使用次数

下面贴出 LFUCache 完整类结构:

public class LFUCache<K, V> extends Cache<K, V> {

    private static class LFUCacheValue<K, V> {
        private volatile Node<K> node;
          private volatile V val;
          //比 LRU 多加一个使用次数属性
          private volatile int useTimes;
          private static final long useTimesOffset;

        static {
              try {
                    Class<?> k = LFUCacheValue.class;
                    useTimesOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("useTimes"));
               } catch (Exception e) {
                    throw new Error(e);
              }
          }

        private LFUCacheValue(K key, V value) {
               node = new Node<>(key);
               val = value;
          }

        /**
        *用于递增使用次数
        */
        private boolean casAddUseTimes(int expect) {
               return Node.unsafe.compareAndSwapInt(this, useTimesOffset, expect, expect + 1);
          }

    }

    private static class NodeInfo<T> {
        //头节点
        private volatile Node<T> head;
        //尾节点
          private volatile Node<T> tail;
          private static final long headOffset;
          private static final long tailOffset;

        static {
               try {
                    Class<?> k = NodeInfo.class;
                    headOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("head"));
                    tailOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("tail"));
               } catch (Exception e) {
                    throw new Error(e);
               }
          }

        private NodeInfo() {
               casHead(null, new Node<>(null));
              casTail(null, new Node<>(null));
              head.casNext(null, tail);
               tail.casPrev(null, head);
          }

        private boolean casHead(Node<T> expect, Node<T> head) {
               return Node.unsafe.compareAndSwapObject(this, headOffset, expect, head);
          }

        private boolean casTail(Node<T> expect, Node<T> tail) {
               return Node.unsafe.compareAndSwapObject(this, tailOffset, expect, tail);
          }

    }

    private final ConcurrentHashMap<K, LFUCacheValue<K, V>> cache;
    //存放不同使用次数的头尾节点信息
    private final TreeMap<Integer, NodeInfo<K>> times;
     volatile int state;
     private static final long stateOffset;

    static {
          try {
               Class<?> k = LFUCache.class;
               stateOffset = Node.unsafe.objectFieldOffset(k.getDeclaredField("state"));
          } catch (Exception e) {
               throw new Error(e);
         }
     }

    private boolean casState(int expect, int state) {
          return Node.unsafe.compareAndSwapInt(this, stateOffset, expect, state);
     }

    public LFUCache(int capacity) {
          super(capacity);
           cache = capacity < 64 ? new ConcurrentHashMap<>(capacity + 1, 1.0f) : new ConcurrentHashMap<>();
           //使用次数最少的排在最前,其中的节点遵循 LRU 缓存策略
          times = new TreeMap<>((t, u) -> t - u);
     }

    @Override
     public V get(K key) {
          LFUCacheValue<K, V> lfuCacheValue = cache.get(key);
          if (lfuCacheValue != null) {
              // 获取当前缓存使用次数
               int useTimes = lfuCacheValue.useTimes;
              // 使用 cas 递增使用次数
               casAddUseTimes(lfuCacheValue);
              // 如果存在则弹出
               poll(useTimes, lfuCacheValue.node);
              // 压入对应使用次数+1 的尾节点前
               offer(useTimes + 1, lfuCacheValue.node);
          }
         return lfuCacheValue == null ? null : lfuCacheValue.val;
     }

    /**
    *原子操作增加使用次数
    */
    private void casAddUseTimes(LFUCacheValue<K, V> lfuCacheValue) {
          int useTimes;
          do {
               useTimes = lfuCacheValue.useTimes;
          } while (!lfuCacheValue.casAddUseTimes(useTimes));
     }

    /**
    *根据使用次数找到对应的节点信息并弹出
    */
    private K poll(int useTimes, Node<K> node) {
          if (node == null)
               return null;
          NodeInfo<K> nodeInfo = times.get(useTimes);
          if (nodeInfo == null)
               return null;
          node.poll();
          if (nodeInfo.head.next == nodeInfo.tail)
               times.remove(useTimes);
          return node.val;
     }

    /**
    *根据使用次数压入对应的链表尾部,如果没有对应的次数则初始化对应的头节点尾节点
    */
    private void offer(int useTimes, Node<K> node) {
          if (node == null)
               return;
           node.offer(times.computeIfAbsent(useTimes, k -> new NodeInfo<>()).tail);
     }

    @Override
     public void put(K key, V value) {
          restartFromHead: for (;;) {
              if (capacity <= 0)
                    break;
               LFUCacheValue<K, V> lfuCacheValue = cache.get(key);
               if (lfuCacheValue != null) {
                    // 获取当前缓存使用次数
                    int useTimes = lfuCacheValue.useTimes;
                    // 使用 cas 递增使用次数
                    casAddUseTimes(lfuCacheValue);
                   // 如果存在则弹出
                    poll(useTimes, lfuCacheValue.node);
                   // 更新缓存值
                   lfuCacheValue.val = value;
                    cache.put(key, lfuCacheValue);
                   // 压入对应使用次数+1 的尾节点前
                    offer(useTimes + 1, lfuCacheValue.node);
               } else {
                    // 不存在缓存
                    if (!casState(0, 1))
                         // 这里利用 cas 替换锁,分不同情况也可以选择加锁,确保不被 new 多个对象
                         continue restartFromHead;
                    lfuCacheValue = new LFUCacheValue<>(key, value);
                    K k = null;
                   // 判断缓存容量并弹出使用次数最少节点头节点的 next 节点
                    while (cache.size() >= capacity && (k = poll()) != null)
                         cache.remove(k);
                    // 保存新缓存
                    cache.put(key, lfuCacheValue);
                    // 将新缓存节点压入对应使用次数的尾节点前
                    offer(lfuCacheValue.useTimes, lfuCacheValue.node);
                    // 恢复可 new 状态
                    state = 0;
              }
               break;
          }
     }

    /**
    *弹出使用频率最少的缓存
    */
    private K poll() {
          Integer firstKey = null;
          try {
             //第一个即使用频率最少的,在构造 TreeMap 时就是以使用频率来排序依据
               firstKey = times.firstKey();
          } catch (NoSuchElementException e) {
               return null;
          }
          //在相同的使用频率时,头节点的后置节点即最久未使用的节点
          return poll(firstKey, times.get(firstKey).head.next);
     }

}

下面测试 LFU:

public class LFUCacheTest {

    public static void main(String[] args) {
          LFUCache<Integer, Integer> lfuCache = new LFUCache<>(2);
          lfuCache.put(1, 1);
          lfuCache.put(2, 2);
          System.out.println(lfuCache.get(2));
          System.out.println("ans:2");
          lfuCache.put(3, 3);
          System.out.println(lfuCache.get(1));
          System.out.println(lfuCache.get(2));
          System.out.println(lfuCache.get(3));
          System.out.println("ans:null,2,3");
          lfuCache.put(4, 4);
          System.out.println(lfuCache.get(1));
          System.out.println(lfuCache.get(2));
          System.out.println(lfuCache.get(3));
          System.out.println(lfuCache.get(4));
          System.out.println("ans:null,2,null,4");
          lfuCache.put(5, 5);
          System.out.println(lfuCache.get(1));
          System.out.println(lfuCache.get(2));
          System.out.println(lfuCache.get(3));
          System.out.println(lfuCache.get(4));
          System.out.println(lfuCache.get(5));
          System.out.println("ans:null,2,null,null,5");
    }   
}

如发现有不足之处,还望指正,大家互相交流学习

相关推荐

如何用 coco 数据集训练 Detectron2 模型?

随着最新的Pythorc1.3版本的发布,下一代完全重写了它以前的目标检测框架,新的目标检测框架被称为Detectron2。本教程将通过使用自定义coco数据集训练实例分割模型,帮助你开始使...

CICD联动阿里云容器服务Kubernetes实践之Bamboo篇

本文档以构建一个Java软件项目并部署到阿里云容器服务的Kubernetes集群为例说明如何使用Bamboo在阿里云Kubernetes服务上运行RemoteAgents并在agents上...

Open3D-ML点云语义分割实验【RandLA-Net】

作为点云Open3D-ML实验的一部分,我撰写了文章解释如何使用Tensorflow和PyTorch支持安装此库。为了测试安装,我解释了如何运行一个简单的Python脚本来可视化名为...

清理系统不用第三方工具(系统自带清理软件效果好不?)

清理优化系统一定要借助于优化工具吗?其实,手动优化系统也没有那么神秘,掌握了方法和技巧,系统清理也是一件简单和随心的事。一方面要为每一个可能产生累赘的文件找到清理的方法,另一方面要寻找能够提高工作效率...

【信创】联想开先终端开机不显示grub界面的修改方法

原文链接:【信创】联想开先终端开机不显示grub界面的修改方法...

如意玲珑成熟度再提升,三大发行版支持教程来啦!

前期,我们已分别发布如意玲珑在deepinV23与UOSV20、openEuler24.03发行版的操作指南,本文,我们将为大家详细介绍Ubuntu24.04、Debian12、op...

118种常见的多媒体文件格式(英文简写)

MP4[?mpi?f??]-MPEG-4Part14(MPEG-4第14部分)AVI[e?vi??a?]-AudioVideoInterleave(音视频交错)MOV[m...

密码丢了急上火?码住7种console密码紧急恢复方式!

身为攻城狮的你,...

CSGO丨CS2的cfg指令代码分享(csgo自己的cfg在哪里?config文件位置在哪?)

?...

使用open SSL生成局域网IP地址证书

某些特殊情况下,用户内网访问多可文档管理系统时需要启用SSL传输加密功能,但只有IP,没有域名和证书。这种情况下多可提供了一种免费可行的方式,通过openSSL生成免费证书。此方法生成证书浏览器会提示...

Python中加载配置文件(python怎么加载程序包)

我们在做开发的时候经常要使用配置文件,那么配置文件的加载就需要我们提前考虑,再不使用任何框架的情况下,我们通常会有两种解决办法:完整加载将所有配置信息一次性写入单一配置文件.部分加载将常用配置信息写...

python开发项目,不得不了解的.cfg配置文件

安装软件时,经常会见到后缀为.cfg、.ini的文件,一般我们不用管,只要不删就行。因为这些是程序安装、运行时需要用到的配置文件。但对开发者来说,这种文件是怎么回事就必须搞清了。本文从.cfg文件的创...

瑞芯微RK3568鸿蒙开发板OpenHarmony系统修改cfg文件权限方法

本文适用OpenHarmony开源鸿蒙系统,本次使用的是开源鸿蒙主板,搭载瑞芯微RK3568芯片。深圳触觉智能专注研发生产OpenHarmony开源鸿蒙硬件,包括核心板、开发板、嵌入式主板,工控整机等...

Python9:图像风格迁移-使用阿里的接口

先不多说,直接上结果图。#!/usr/bin/envpython#coding=utf-8importosfromaliyunsdkcore.clientimportAcsClient...

Python带你打造个性化的图片文字识别

我们的目标:从CSV文件读取用户的文件信息,并将文件名称修改为姓名格式的中文名称,进行规范资料整理,从而实现快速对多个文件进行重命名。最终效果:将原来无规律的文件名重命名为以姓名为名称的文件。技术点:...

取消回复欢迎 发表评论:

请填写验证码