前言


LRU 是 LeastRecentlyUsed 的简写,字面意思则是 最近最少使用

通常用于缓存的淘汰策略实现,由于缓存的内存非常宝贵,所以需要根据某种规则来剔除数据保证内存不被撑满。

如常用的 Redis 就有以下几种策略:

策略 描述
volatile-lru 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru 从所有数据集中挑选最近最少使用的数据淘汰
allkeys-random 从所有数据集中任意选择数据进行淘汰
no-envicition 禁止驱逐数据

摘抄自:https://github.com/CyC2018/Interview-Notebook/blob/master/notes/Redis.md#%E5%8D%81%E4%B8%89%E6%95%B0%E6%8D%AE%E6%B7%98%E6%B1%B0%E7%AD%96%E7%95%A5

实现一


之前也有接触过一道面试题,大概需求是:

  • 实现一个 LRU 缓存,当缓存数据达到 N 之后需要淘汰掉最近最少使用的数据。

  • N 小时之内没有被访问的数据也需要淘汰掉。

以下是我的实现:

public class LRUAbstractMap extends java.util.AbstractMap {private final static Logger LOGGER = LoggerFactory.getLogger(LRUAbstractMap.class);/*** 检查是否超期线程*/private ExecutorService checkTimePool ;/*** map 最大size*/private final static int MAX_SIZE = 1024 ;private final static ArrayBlockingQueue<Node> QUEUE = new ArrayBlockingQueue<>(MAX_SIZE) ;/*** 默认大小*/private final static int DEFAULT_ARRAY_SIZE =1024 ;/*** 数组长度*/private int arraySize ;/*** 数组*/private Object[] arrays ;/*** 判断是否停止 flag*/private volatile boolean flag = true ;/*** 超时时间*/private final static Long EXPIRE_TIME = 60 * 60 * 1000L ;/*** 整个 Map 的大小*/private volatile AtomicInteger size  ;public LRUAbstractMap() {arraySize = DEFAULT_ARRAY_SIZE;arrays = new Object[arraySize] ;//开启一个线程检查最先放入队列的值是否超期executeCheckTime();}/*** 开启一个线程检查最先放入队列的值是否超期 设置为守护线程*/private void executeCheckTime() {ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("check-thread-%d").setDaemon(true).build();checkTimePool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<>(1),namedThreadFactory,new ThreadPoolExecutor.AbortPolicy());checkTimePool.execute(new CheckTimeThread()) ;}@Overridepublic Set<Entry> entrySet() {return super.keySet();}@Overridepublic Object put(Object key, Object value) {int hash = hash(key);int index = hash % arraySize ;Node currentNode = (Node) arrays[index] ;if (currentNode == null){arrays[index] = new Node(null,null, key, value);//写入队列QUEUE.offer((Node) arrays[index]) ;sizeUp();}else {Node cNode = currentNode ;Node nNode = cNode ;//存在就覆盖if (nNode.key == key){cNode.val = value ;}while (nNode.next != null){//key 存在 就覆盖 简单判断if (nNode.key == key){nNode.val = value ;break ;}else {//不存在就新增链表sizeUp();Node node = new Node(nNode,null,key,value) ;//写入队列QUEUE.offer(currentNode) ;cNode.next = node ;}nNode = nNode.next ;}}return null ;}@Overridepublic Object get(Object key) {int hash = hash(key) ;int index = hash % arraySize ;Node currentNode = (Node) arrays[index] ;if (currentNode == null){return null ;}if (currentNode.next == null){//更新时间currentNode.setUpdateTime(System.currentTimeMillis());//没有冲突return currentNode ;}Node nNode = currentNode ;while (nNode.next != null){if (nNode.key == key){//更新时间currentNode.setUpdateTime(System.currentTimeMillis());return nNode ;}nNode = nNode.next ;}return super.get(key);}@Overridepublic Object remove(Object key) {int hash = hash(key) ;int index = hash % arraySize ;Node currentNode = (Node) arrays[index] ;if (currentNode == null){return null ;}if (currentNode.key == key){sizeDown();arrays[index] = null ;//移除队列QUEUE.poll();return currentNode ;}Node nNode = currentNode ;while (nNode.next != null){if (nNode.key == key){sizeDown();//在链表中找到了 把上一个节点的 next 指向当前节点的下一个节点nNode.pre.next = nNode.next ;nNode = null ;//移除队列QUEUE.poll();return nNode;}nNode = nNode.next ;}return super.remove(key);}/*** 增加size*/private void sizeUp(){//在put值时候认为里边已经有数据了flag = true ;if (size == null){size = new AtomicInteger() ;}int size = this.size.incrementAndGet();if (size >= MAX_SIZE) {//找到队列头的数据Node node = QUEUE.poll() ;if (node == null){throw new RuntimeException("data error") ;}//移除该 keyObject key = node.key ;remove(key) ;lruCallback() ;}}/*** 数量减小*/private void sizeDown(){if (QUEUE.size() == 0){flag = false ;}this.size.decrementAndGet() ;}@Overridepublic int size() {return size.get() ;}/*** 链表*/private class Node{private Node next ;private Node pre ;private Object key ;private Object val ;private Long updateTime ;public Node(Node pre,Node next, Object key, Object val) {this.pre = pre ;this.next = next;this.key = key;this.val = val;this.updateTime = System.currentTimeMillis() ;}public void setUpdateTime(Long updateTime) {this.updateTime = updateTime;}public Long getUpdateTime() {return updateTime;}@Overridepublic String toString() {return "Node{" +"key=" + key +", val=" + val +'}';}}/*** copy HashMap 的 hash 实现* @param key* @return*/public int hash(Object key) {int h;return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);}private void lruCallback(){LOGGER.debug("lruCallback");}private class CheckTimeThread implements Runnable{@Overridepublic void run() {while (flag){try {Node node = QUEUE.poll();if (node == null){continue ;}Long updateTime = node.getUpdateTime() ;if ((updateTime - System.currentTimeMillis()) >= EXPIRE_TIME){remove(node.key) ;}} catch (Exception e) {LOGGER.error("InterruptedException");}}}}}

感兴趣的朋友可以直接从:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUAbstractMap.java

下载代码本地运行。

代码看着比较多,其实实现的思路还是比较简单:

  • 采用了与 HashMap 一样的保存数据方式,只是自己手动实现了一个简易版。

  • 内部采用了一个队列来保存每次写入的数据。

  • 写入的时候判断缓存是否大于了阈值 N,如果满足则根据队列的 FIFO 特性将队列头的数据删除。因为队列头的数据肯定是最先放进去的。

  • 再开启了一个守护线程用于判断最先放进去的数据是否超期(因为就算超期也是最先放进去的数据最有可能满足超期条件。)

  • 设置为守护线程可以更好的表明其目的(最坏的情况下,如果是一个用户线程最终有可能导致程序不能正常退出,因为该线程一直在运行,守护线程则不会有这个情况。)

以上代码大体功能满足了,但是有一个致命问题。

就是最近最少使用没有满足,删除的数据都是最先放入的数据。

不过其中的 putget 流程算是一个简易的 HashMap 实现,可以对 HashMap 加深一些理解。

实现二


因此如何来实现一个完整的 LRU 缓存呢,这次不考虑过期时间的问题。

其实从上一个实现也能想到一些思路:

  • 要记录最近最少使用,那至少需要一个有序的集合来保证写入的顺序。

  • 在使用了数据之后能够更新它的顺序。

基于以上两点很容易想到一个常用的数据结构:链表

  1. 每次写入数据时将数据放入链表头结点。

  2. 使用数据时候将数据移动到头结点

  3. 缓存数量超过阈值时移除链表尾部数据。

因此有了以下实现:

public class LRUMap<K, V> {private final Map<K, V> cacheMap = new HashMap<>();/*** 最大缓存大小*/private int cacheSize;/*** 节点大小*/private int nodeCount;/*** 头结点*/private Node<K, V> header;/*** 尾结点*/private Node<K, V> tailer;public LRUMap(int cacheSize) {this.cacheSize = cacheSize;//头结点的下一个结点为空header = new Node<>();header.next = null;//尾结点的上一个结点为空tailer = new Node<>();tailer.tail = null;//双向链表 头结点的上结点指向尾结点header.tail = tailer;//尾结点的下结点指向头结点tailer.next = header;}public void put(K key, V value) {cacheMap.put(key, value);//双向链表中添加结点addNode(key, value);}public V get(K key){Node<K, V> node = getNode(key);//移动到头结点moveToHead(node) ;return cacheMap.get(key);}private void moveToHead(Node<K,V> node){//如果是最后的一个节点if (node.tail == null){node.next.tail = null ;tailer = node.next ;nodeCount -- ;}//如果是本来就是头节点 不作处理if (node.next == null){return ;}//如果处于中间节点if (node.tail != null && node.next != null){//它的上一节点指向它的下一节点 也就删除当前节点node.tail.next = node.next ;nodeCount -- ;}//最后在头部增加当前节点//注意这里需要重新 new 一个对象,不然原本的node 还有着下面的引用,会造成内存溢出。node = new Node<>(node.getKey(),node.getValue()) ;addHead(node) ;}/*** 链表查询 效率较低* @param key* @return*/private Node<K,V> getNode(K key){Node<K,V> node = tailer ;while (node != null){if (node.getKey().equals(key)){return node ;}node = node.next ;}return null ;}/*** 写入头结点* @param key* @param value*/private void addNode(K key, V value) {Node<K, V> node = new Node<>(key, value);//容量满了删除最后一个if (cacheSize == nodeCount) {//删除尾结点delTail();}//写入头结点addHead(node);}/*** 添加头结点** @param node*/private void addHead(Node<K, V> node) {//写入头结点header.next = node;node.tail = header;header = node;nodeCount++;//如果写入的数据大于2个 就将初始化的头尾结点删除if (nodeCount == 2) {tailer.next.next.tail = null;tailer = tailer.next.next;}}    private void delTail() {//把尾结点从缓存中删除cacheMap.remove(tailer.getKey());//删除尾结点tailer.next.tail = null;tailer = tailer.next;nodeCount--;}private class Node<K, V> {private K key;private V value;Node<K, V> tail;Node<K, V> next;public Node(K key, V value) {this.key = key;this.value = value;}public Node() {}public K getKey() {return key;}public void setKey(K key) {this.key = key;}public V getValue() {return value;}public void setValue(V value) {this.value = value;}}@Overridepublic String toString() {StringBuilder sb = new StringBuilder() ;Node<K,V> node = tailer ;while (node != null){sb.append(node.getKey()).append(":").append(node.getValue()).append("-->") ;node = node.next ;}return sb.toString();}}

源码: https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRUMap.java

实际效果,写入时:

   @Testpublic void put() throws Exception {LRUMap<String,Integer> lruMap = new LRUMap(3) ;lruMap.put("1",1) ;lruMap.put("2",2) ;lruMap.put("3",3) ;System.out.println(lruMap.toString());lruMap.put("4",4) ;System.out.println(lruMap.toString());lruMap.put("5",5) ;System.out.println(lruMap.toString());}

//输出:

1:1-->2:2-->3:3-->
2:2-->3:3-->4:4-->
3:3-->4:4-->5:5-->

使用时:

   @Testpublic void get() throws Exception {LRUMap<String,Integer> lruMap = new LRUMap(3) ;lruMap.put("1",1) ;lruMap.put("2",2) ;lruMap.put("3",3) ;System.out.println(lruMap.toString());System.out.println("==============");Integer integer = lruMap.get("1");System.out.println(integer);System.out.println("==============");System.out.println(lruMap.toString());}

//输出

1:1-->2:2-->3:3-->
==============
1
==============
2:2-->3:3-->1:1-->

实现思路和上文提到的一致,说下重点:

  • 数据是直接利用 HashMap 来存放的。

  • 内部使用了一个双向链表来存放数据,所以有一个头结点 header,以及尾结点 tailer。

  • 每次写入头结点,删除尾结点时都是依赖于 header tailer,如果看着比较懵建议自己实现一个链表熟悉下,或结合下文的对象关系图一起理解。

  • 使用数据移动到链表头时,第一步是需要在双向链表中找到该节点。这里就体现出链表的问题了。查找效率很低,最差需要 O(N)。之后依赖于当前节点进行移动。

  • 在写入头结点时有判断链表大小等于 2 时需要删除初始化的头尾结点。这是因为初始化时候生成了两个双向节点,没有数据只是为了形成一个数据结构。当真实数据进来之后需要删除以方便后续的操作(这点可以继续优化)。

  • 以上的所有操作都是线程不安全的,需要使用者自行控制。

下面是对象关系图:

初始化时

写入数据时

LRUMap<String,Integer> lruMap = new LRUMap(3) ;
lruMap.put("1",1) ;

lruMap.put("2",2) ;

lruMap.put("3",3) ;

lruMap.put("4",4) ;

获取数据时

数据和上文一样:

Integer integer = lruMap.get("2");

通过以上几张图应该是很好理解数据是如何存放的了。

实现三


其实如果对 Java 的集合比较熟悉的话,会发现上文的结构和 LinkedHashMap 非常类似。

对此不太熟悉的朋友可以先了解下 LinkedHashMap 底层分析 。

所以我们完全可以借助于它来实现:

public class LRULinkedMap<K,V> {/*** 最大缓存大小*/private int cacheSize;private LinkedHashMap<K,V> cacheMap ;public LRULinkedMap(int cacheSize) {this.cacheSize = cacheSize;cacheMap = new LinkedHashMap(16,0.75F,true){@Overrideprotected boolean removeEldestEntry(Map.Entry eldest) {if (cacheSize + 1 == cacheMap.size()){return true ;}else {return false ;}}};}public void put(K key,V value){cacheMap.put(key,value) ;}public V get(K key){return cacheMap.get(key) ;}public Collection<Map.Entry<K, V>> getAll() {return new ArrayList<Map.Entry<K, V>>(cacheMap.entrySet());}}

源码: https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/actual/LRULinkedMap.java

这次就比较简洁了,也就几行代码(具体的逻辑 LinkedHashMap 已经帮我们实现好了)

实际效果:

   @Testpublic void put() throws Exception {LRULinkedMap<String,Integer> map = new LRULinkedMap(3) ;map.put("1",1);map.put("2",2);map.put("3",3);for (Map.Entry<String, Integer> e : map.getAll()){System.out.print(e.getKey() + " : " + e.getValue() + "\t");}System.out.println("");map.put("4",4);for (Map.Entry<String, Integer> e : map.getAll()){System.out.print(e.getKey() + " : " + e.getValue() + "\t");}}

//输出

1 : 1    2 : 2   3 : 3
2 : 2    3 : 3   4 : 4    

使用时:

   @Testpublic void get() throws Exception {LRULinkedMap<String,Integer> map = new LRULinkedMap(4) ;map.put("1",1);map.put("2",2);map.put("3",3);map.put("4",4);for (Map.Entry<String, Integer> e : map.getAll()){System.out.print(e.getKey() + " : " + e.getValue() + "\t");}System.out.println("");map.get("1") ;for (Map.Entry<String, Integer> e : map.getAll()){System.out.print(e.getKey() + " : " + e.getValue() + "\t");}}}

//输出​​​​​​​

1 : 1    2 : 2   3 : 3   4 : 4
2 : 2    3 : 3   4 : 4   1 : 1

LinkedHashMap 内部也有维护一个双向队列,在初始化时也会给定一个缓存大小的阈值。初始化时自定义是否需要删除最近不常使用的数据,如果是则会按照实现二中的方式管理数据。

其实主要代码就是重写了 LinkedHashMap 的 removeEldestEntry 方法:

   protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {return false;}

它默认是返回 false,也就是不会管有没有超过阈值。

所以我们自定义大于了阈值时返回 true,这样 LinkedHashMap 就会帮我们删除最近最少使用的数据。

总结


以上就是对 LRU 缓存的实现,了解了这些至少在平时使用时可以知其所以然。

当然业界使用较多的还有 guava 的实现,并且它还支持多种过期策略。

动手实现一个 LRU cache相关推荐

  1. 动手实现一个 localcache - 设计篇

    前言 哈喽,大家好,我是asong.最近想动手写一个localcache练练手,工作这么久了,也看过很多同事实现的本地缓存,都各有所长,自己平时也在思考如何实现一个高性能的本地缓存,接下来我将基于自己 ...

  2. 单机 “5千万以上“ 工业级 LRU cache 实现

    文章目录 前言 工业级 LRU Cache 1. 基本架构 2. 基本操作 2.1 insert 操作 2.2 高并发下 insert 的一致性/性能 保证 2.3 Lookup操作 2.4 shar ...

  3. 代码写对了还挂了?程序媛小姐姐从 LRU Cache 带你看面试的本质

    来源 | 码农田小齐 责编 |  Carol 前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码 ...

  4. 从 LRU Cache 带你看面试的本质

    前言 在讲这道题之前,我想先聊聊「技术面试究竟是在考什么」这个问题. 技术面试究竟在考什么 在人人都知道刷题的今天,面试官也都知道大家会刷题准备面试,代码大家都会写,那面试为什么还在考这些题?那为什么 ...

  5. 自己动手写一个印钞机 第四章

    2019独角兽企业重金招聘Python工程师标准>>> 作者:阿布? 未经本人允许禁止转载 ipython notebook git版本 目录章节地址: 自己动手写一个印钞机 第一章 ...

  6. linux cache lru回收,LRU cache 算法

    上周末同学问了一些操作系统的问题,涉及到LRU cache,顺便复习了一下. LRU是least recently used的缩写,意思是最近最少使用,是一种内存页面置换算法.根据程序设计局部性的原则 ...

  7. 如何设计LRU Cache算法

    前言 相信有的伙伴在面试的过程中,或多或少的会被问到redis的内存淘汰策略,可能大部分人都知道都有哪些对应的策略,毕竟对于八股文的套路大家肯定早已铭记于心.但是当面试官问你如何实现或者让你去写一个对 ...

  8. 【LeetCode】LRU Cache 解决报告

    插话:只写了几个连续的博客,博客排名不再是实际"远在千里之外"该.我们已经进入2一万内. 再接再厉.油! Design and implement a data structure ...

  9. 146. LRU Cache

    Title 运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制.它应该支持以下操作: 获取数据 get 和 写入数据 put . 获取数据 get(key) -:如果密钥 (k ...

最新文章

  1. 这个北航妹子也太卷了...
  2. 简单介绍SQLserver中的declare变量用法
  3. C语言中被常用到的宏
  4. matlab simulink 四分之一1/4车辆垂向振动模型 轮毂电机
  5. 【今日互联网大事儿】传百度要投资Uber呢~
  6. java语言实验报告,Java语言 实验报告(二)
  7. 常见CSS选择器分类
  8. 【Java从0到架构师】git 核心原理和分支管理
  9. 《C++编程——数据结构与程序设计方法》程序范例:影碟店(源代码)
  10. clinux 防火墙增加白名单_linux增加iptables防火墙规则的示例
  11. java的四个元注解 @Retention @Target @Document @Inherited
  12. wifi分析仪怎么看哪个信道好_一定得学的切换WiFi信道技巧,让你的网速如飞!...
  13. 高斯启发式Gaussian Heuristic 格理论相关知识
  14. 微信小程序通过url 上传远端图片 到微信小程序临时素材库 java
  15. 『互联网架构』软件架构-软件环境的持续发布管理(上)
  16. python中英文切换_python国际化(i18n)和中英文切换
  17. 微信小程序一个按钮同时获取用户信息和手机号码
  18. 使用VSCode创建一个Vue项目
  19. 老男孩python全栈s21day04作业
  20. 打印机出现黄色感叹号!无法查看属性和设置,开机查看打印机,打印自动变灰色问题无法使用!

热门文章

  1. Hyper-V 2016 系列教程26 Hyper-v平台并口外设解决方案介绍
  2. lt;xliff:ggt;标签
  3. 【翻译】VisualStudio11中的CSS编辑器改进(asp.net 4.5系列)-ScottGu
  4. cisco SMD 配置安装
  5. 在Asp.net页面中实现数据饼图
  6. Tensorflow初学者之搭建神经网络基本流程
  7. 视频、音频打时间戳的方法及其音视频同步(播放)原理
  8. The Windows Subsystem for Linux optional component is not enabled. Please enable it and try again.
  9. 静态路由实验 +http+dns_华为静态路由配置实验
  10. 示波器1m和50欧姆示阻抗匹配_为什么示波器阻抗偏偏是1M和50欧?-测试测量-与非网...