zoukankan      html  css  js  c++  java
  • LRU算法与LRUCache

      关于LRU


      LRU(Least recently used,最近最少使用)算法是操作系统中一种经典的页面置换算法,当发生缺页中断时,需要将内存的一个或几个页面置换出,LRU指出应该将内存最近最少使用的那些页面换出,依据的是程序的局部性原理,最近经常使用的页面再不久的将来也很有可能被使用,反之最近很少使用的页面未来也不太可能在使用。

      其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。但此算法不能保证过去不常用,将来也不常用。

      设计目标


      1、实现LRU算法。

      2、学以致用,了解算法实际应用场景。

      3、封装LRUCache数据结构。

      4、实现线程安全与线程不安全两种版本LRUCache。


     实际应用LRU


       LRU算法非常实用,不仅在操作系统中发挥着很大作用,而且他还是一款缓存淘汰算法。

      在做大型软件或网站服务时,如果想要让系统稳定并且能够承受得住千万级用户的高并发访问,就要尽量缩短因日常维护操作(计划)和突发的系统崩溃(非计划)所导致的停机时间,以提高系统和应用的可用性。那么我们必然要采取一些高可用的措施。

      有人说互联网用户是用脚投票的,这句话其实也从侧面说明了,用户体验是多么的重要。这就要求在软件架构设计时,不但要注重可靠性、安全性、可扩展性以及可维护性等等的一些指标,更要注重用户的体验,用户体验分很多方面,但是有一点非常重要就是对用户操作的响应一定要快。怎样提高用户访问的响应速度,这就是摆在架构设计中必须要解决的问题。说道提高服务的响应速度就不得不说缓存了。

      缓存有三种:数据库缓存、静态缓存和动态缓存。

      从系统的层面说,CPU的速度远远高于磁盘IO的速度。所以要想提高响应速度,必须减少磁盘IO的操作,但是有很多信息又是存在数据库当中的,每次查询数据库就是一次IO操作。

      在目前主流的memcache和redis中都有LRU算法的身影。在两大中间件中,LRU算法都在他们之中起到缓存回收的作用。关于他们的源码以后打算分析。

      静态缓存:一般指 web 类应用中,将图片、js、css、视频、html等静态文件/资源通过磁盘/内存等缓存方式,提高资源响应方式,减少服务器压力/资源开销的一门缓存技术。静态缓存技术:CDN是经典代表之作。静态缓存技术面非常广,涉及的开源技术包含apache、Lighttpd、nginx、varnish、squid等。

      动态缓存:用于临时文件交换,缓存是指临时文件交换区,电脑把最常用的文件从存储器里提出来临时放在缓存里,就像把工具和材料搬上工作台一样,这样会比用时现去仓库取更方便。

      LRU算法过程


         链表+容器实现LRU缓存

       传统意义的LRU算法是为每一个Cache对象设置一个计数器,每次Cache命中则给计数器+1,而Cache用完,需要淘汰旧内容,放置新内容时,就查看所有的计数器,并将最少使用的内容替换掉。

      它的弊端很明显,如果Cache的数量少,问题不会很大, 但是如果Cache的空间过大,达到10W或者100W以上,一旦需要淘汰,则需要遍历所有计算器,其性能与资源消耗是巨大的。

      效率也就非常的慢了。

      所以采用双向链表+hash表的数据结构实现,双向链表作为队列存储当前缓存节点,其中从表头到表尾的元素按照最近使用的时间进行排列,放在表头的是最近刚刚被使用过的元素,表尾的最近最少使用的元素;如果仅仅采用双向链表,那么查询某个元素需要 O(n) 的时间,为了加快双向链表中元素的查询速度,采用hash表讲key进行映射,可以在O(1)的时间内找到需要节点。

      

      1. 新数据插入到链表头部;

      2. 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;

      3. 当链表满的时候,将链表尾部的数据丢弃。

      【命中率】 

      命中率=命中数/(命中数+没有命中数), 缓存命中率是判断加速效果好坏的重要因素之一。

      当存在热点数据的时候,LRU效率很好,但偶发性、周期性的批量操作会导致LRU命中率急剧下滑,缓存污染的情况比较严重。  

       

        

      

      原理: 将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。 
    这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。 
    当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。

      

      1 package com.zuo.lru;
      2 
      3 import java.util.HashMap;
      4 
      5 /**
      6  * 
      7  * @author zuo
      8  *    线程不安全
      9  * @param <K>
     10  * @param <V>
     11  */
     12 public class LRUCache<K, V> {
     13 
     14     private int currentCacheSize;    //当前缓存大小
     15     private int CacheCapcity;        //缓存上限
     16     private HashMap<K, CacheNode> caches; //缓存表
     17     private CacheNode first;    
     18     private CacheNode last;
     19     
     20     public LRUCache(int size) {
     21         currentCacheSize=0;
     22         this.CacheCapcity=size;
     23         caches=new HashMap<K,CacheNode>(size);
     24     }
     25     
     26     /**
     27      * 添加
     28      * @param k
     29      * @param v
     30      */
     31     public void put(K k,V v){
     32         CacheNode node=caches.get(k);
     33         if(node==null){
     34             if(caches.size()>=CacheCapcity){
     35                 caches.remove(last.key);
     36                 removeLast();
     37             }
     38             node=new CacheNode();
     39             node.key=k;
     40         }
     41         node.value=v;
     42         moveToFirst(node);
     43         caches.put(k, node);
     44     }
     45     
     46     public Object get(K k){
     47         CacheNode node=caches.get(k);
     48         if(node==null){
     49             return null;
     50         }
     51         moveToFirst(node);
     52         return node.value;
     53     }
     54     
     55     /**
     56      * 删除
     57      * @param k
     58      * @return
     59      */
     60     public Object remove(K k){
     61         CacheNode node=caches.get(k);
     62         if(node!=null){
     63             if(node.pre!=null){
     64                 node.pre.next=node.next;//前结点的后指针指向当前节点的下一个
     65             }
     66             if(node.next!=null){
     67                 node.next.pre=node.pre;//后节点的前指针指向当前结点的上一个
     68             }
     69             if(node==first){
     70                 first=node.next;
     71             }
     72             if(node==last){
     73                 last=node.pre;
     74             }
     75         }
     76         return caches.remove(k);
     77     }
     78     
     79     /**
     80      * 删除last
     81      */
     82     private void removeLast(){
     83         if(last!=null){
     84             last=last.pre;
     85             if(last==null){
     86                 first=null;
     87             }else{
     88                 last.next=null;
     89             }
     90         }
     91     }
     92     
     93     /**
     94      * 将node移动到头说明使用频率高
     95      * @param node
     96      */
     97     private void moveToFirst(CacheNode node){
     98         if(first==node){
     99             return;
    100         }
    101         if(node.pre!=null){
    102             node.pre.next=node.next;//前结点的后指针指向当前节点的下一个
    103         }
    104         if(node.next!=null){
    105             node.next.pre=node.pre;//后节点的前指针指向当前结点的上一个
    106         }
    107         if(node==last){
    108             last=last.pre;
    109         }
    110         if(first==null || last==null){
    111             first=last=node;
    112             return;
    113         }
    114         node.next=first;
    115         first.pre=node;
    116         first=node;
    117         first.pre=null;
    118     }
    119     
    120     
    121     
    122     /**
    123      * 清空
    124      */
    125     public void clear(){
    126         first=null;
    127         last=null;
    128         caches.clear();
    129     }
    130     
    131     @Override
    132     public String toString() {
    133         StringBuilder stringBuilder=new StringBuilder();
    134         CacheNode node=first;
    135         while(node!=null){
    136             stringBuilder.append(String.format("%s:%s ", node.key,node.value));
    137             node=node.next;
    138         }
    139         return stringBuilder.toString();
    140     }
    141     
    142     /**
    143      * @author zuo
    144      * 双向链表
    145      */
    146     class CacheNode{
    147         CacheNode pre; //前指针
    148         CacheNode next;//后指针
    149         Object key;    //
    150         Object value;  //
    151         public CacheNode() {
    152         }
    153     }
    154     
    155     public int getCurrentCacheSize() {
    156         return currentCacheSize;
    157     }
    158     
    159 
    160     public static void main(String[] args) {
    161 
    162         LRUCache<Integer,String> lru = new LRUCache<Integer,String>(3);
    163 
    164         lru.put(1, "a");    // 1:a
    165         System.out.println(lru.toString());
    166         lru.put(2, "b");    // 2:b 1:a 
    167         System.out.println(lru.toString());
    168         lru.put(3, "c");    // 3:c 2:b 1:a 
    169         System.out.println(lru.toString());
    170         lru.put(4, "d");    // 4:d 3:c 2:b  
    171         System.out.println(lru.toString());
    172         lru.put(1, "aa");   // 1:aa 4:d 3:c  
    173         System.out.println(lru.toString());
    174         lru.put(2, "bb");   // 2:bb 1:aa 4:d
    175         System.out.println(lru.toString());
    176         lru.put(5, "e");    // 5:e 2:bb 1:aa
    177         System.out.println(lru.toString());
    178         lru.get(1);         // 1:aa 5:e 2:bb
    179         System.out.println(lru.toString());
    180         lru.remove(11);     // 1:aa 5:e 2:bb
    181         System.out.println(lru.toString());
    182         lru.remove(1);      //5:e 2:bb
    183         System.out.println(lru.toString());
    184         lru.put(1, "aaa");  //1:aaa 5:e 2:bb
    185         System.out.println(lru.toString());
    186     }
    187     
    188     
    189     
    190 }

      线程安全与线程不安全


      

      线程安全就是多线程访问时,采用了加锁机制,当一个线程访问该类的某个数据时,进行保护,其他线程不能进行访问直到该线程读取完,其他线程才可使用。不会出现数据不一致或者数据污染。
      线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据。  

      1 package com.zuo.lru;
      2 
      3 import java.util.Iterator;
      4 import java.util.LinkedHashMap;
      5 import java.util.Map;
      6 import java.util.Map.Entry;
      7 
      8 /**
      9  * 线程安全
     10  * @author zuo
     11  *
     12  */
     13 public class LRUCacheSafe <K,V>{
     14     
     15     private final LinkedHashMap<K,V> map;
     16     
     17     private int currentCacheSize;    //当前cache的大小
     18     private int CacheCapcity; //cache最大大小
     19     private int putCount;       //put的次数
     20     private int createCount;    //create的次数
     21     private int evictionCount;  //回收的次数
     22     private int hitCount;       //命中的次数
     23     private int missCount;      //未命中次数
     24     
     25     public LRUCacheSafe(int CacheCapcity){
     26         if(CacheCapcity<=0){
     27             throw new IllegalArgumentException("CacheCapcity <= 0");
     28         }
     29         this.CacheCapcity=CacheCapcity;
     30         //将LinkedHashMap的accessOrder设置为true来实现LRU
     31         this.map=new LinkedHashMap<K,V>(0,0.75f,true);//true 就是基于访问的顺序,get一个元素后,这个元素被加到最后(使用了LRU 最近最少被使用的调度算法)
     32     }
     33     
     34     public final V get(K key){
     35         if(key==null){
     36             throw new NullPointerException("key == null");
     37         }
     38         V mapValue;
     39         synchronized (this) {
     40             mapValue=map.get(key);
     41             if(mapValue!=null){
     42                 //mapValue 不为空表示命中,hitCount+1 并返回mapValue对象
     43                 hitCount++;
     44                 return mapValue;
     45             }
     46             missCount++;
     47         }
     48         //如果未命中,则试图创建一个对象,这里create方法放回null,并没有实现创建对象的方法
     49         //如果需要事项创建对象的方法可以重写create方法。因为图片缓存时内存缓存没有命中会去文件缓存或者从网络下载,所以不需要创建。
     50         V createValue=create(key);
     51         if(createValue==null){
     52             return null;
     53         }
     54         //假如创建了新的对象,则继续往下运行
     55         synchronized (this) {
     56             createCount++;
     57             //将createValue加入到map中,并且将原来的key的对象保存到mapValue
     58             mapValue=map.put(key, createValue);
     59             if(mapValue!=null){
     60                 //如果mapValue不为空,则撤销上一步的put操作
     61                 map.put(key, mapValue);
     62             }else{
     63                 //加入新创建的对象之后需要重新计算currentCacheSize大小
     64                 currentCacheSize+=safecurrentCacheSizeOf(key, createValue);
     65             }
     66         }
     67         if(mapValue!=null){
     68             entryRemoved(false, key, createValue, mapValue);
     69             return mapValue;
     70         }else{
     71             //每次新加入对象都需要调用trimTocurrentCacheSize方法看是否回收
     72             trimTocurrentCacheSize(CacheCapcity);
     73             return createValue;
     74         }
     75     }
     76     
     77     /**
     78      * 此方法根据CacheCapcity来调整cache的大小,如果CacheCapcity传入-1,则清空缓存中的的大小
     79      * @param CacheCapcity
     80      */
     81     private void trimTocurrentCacheSize(int CacheCapcity){
     82         while(true){
     83             K key;
     84             V value;
     85             synchronized (this) {
     86                 if(currentCacheSize<0||(map.isEmpty() && currentCacheSize!=0)){
     87                     throw new IllegalStateException(getClass().getName()
     88                          + ".currentCacheSizeOf() is reporting inconsistent results!");
     89                 }
     90                 //如果当前currentCacheSize小于CacheCapcity或者map没有任何对象,则循环结束
     91                 if(currentCacheSize<=CacheCapcity || map.isEmpty()){
     92                     break;
     93                 }
     94                 //移除链表头部的元素,并进入下一次循环
     95                 Map.Entry<K, V> toEvict =map.entrySet().iterator().next();
     96                 key=toEvict.getKey();
     97                 value=toEvict.getValue();
     98                 map.remove(key);
     99                 currentCacheSize-=safecurrentCacheSizeOf(key, value);
    100                 evictionCount++;//回收次数++
    101             }
    102             entryRemoved(true, key, value, null);
    103         }
    104     }
    105     
    106     public final V put(K key,V value){
    107         if(key==null||value==null){
    108             throw new NullPointerException("key == null || value == null");
    109         }
    110         V previous;
    111         synchronized (this) {
    112             putCount++;
    113             currentCacheSize+=safecurrentCacheSizeOf(key, value);//currentCacheSize加上预put对象大小
    114             previous=map.put(key, value);
    115             if(previous!=null){
    116                 //如果之前存在键为key的对象,则currentCacheSize应该减去原来对象的大小
    117                 currentCacheSize-=safecurrentCacheSizeOf(key, previous);
    118             }
    119         }
    120         if(previous!=null){
    121             entryRemoved(false, key, previous, value);
    122         }
    123         //每次新加入的对象都需要调用trimtocurrentCacheSize方法看是否要回收
    124         trimTocurrentCacheSize(CacheCapcity);
    125         return previous;
    126     }
    127     
    128     /**
    129      * 从内存缓存中根据key值移除某个对象并返回该对象
    130      * @param key
    131      * @return
    132      */
    133     public final V remove(K key){
    134         if(key==null){
    135             throw new NullPointerException("key == null");
    136         }
    137         V previous;
    138         synchronized (this) {
    139             previous=map.remove(key);
    140             if(previous!=null){
    141                 currentCacheSize-=safecurrentCacheSizeOf(key, previous);
    142             }
    143         }
    144         if(previous!=null){
    145             entryRemoved(false, key, previous, null);
    146         }
    147         return previous;
    148     }
    149     
    150     /**
    151      * 在高速缓存未命中之后调用以计算对应键的值
    152      * @param key
    153      * @return 如果没有计算值,则返回计算值或NULL
    154      */
    155     protected V create(K key) {
    156         return null;
    157     }
    158     
    159     private int safecurrentCacheSizeOf(K key,V value){
    160         int result=currentCacheSizeOf(key, value);
    161         if(result<0){
    162             throw new IllegalStateException("Negative currentCacheSize: " + key + "=" + value);
    163         }
    164         return result;
    165     }
    166     
    167     /**
    168      * 用来计算单个对象的大小,这里默认返回1
    169      * @param key
    170      * @param value
    171      * @return
    172      */
    173     protected int currentCacheSizeOf(K key,V value) {
    174         return 1;
    175     }
    176     
    177     protected void entryRemoved(boolean evicted,K key,V oldValue,V newValue) {}
    178     
    179     /**
    180      * 清空内存缓存
    181      */
    182     public final void evictAll(){
    183         trimTocurrentCacheSize(-1);
    184     }
    185     
    186     /**
    187      * 当前cache大小
    188      * @return
    189      */
    190     public synchronized final int currentCacheSize(){
    191         return currentCacheSize;
    192     }
    193     /**
    194      * 命中次数
    195      * @return
    196      */
    197     public synchronized final int hitCount(){
    198         return hitCount;
    199     }
    200     /**
    201      * 未命中次数
    202      * @return
    203      */
    204     public synchronized final int missCount(){
    205         return missCount;
    206     }
    207     /**
    208      * create次数
    209      * @return
    210      */
    211     public synchronized final int createCount(){
    212         return createCount;
    213     }
    214     /**
    215      * put次数
    216      * @return
    217      */
    218     public synchronized final int putCount(){
    219         return putCount;
    220     }
    221     /**
    222      * 回收次数
    223      * @return
    224      */
    225     public synchronized final int evictionCount(){
    226         return evictionCount;
    227     }
    228     /**
    229      * 返回一个当前缓存内容的副本
    230      * @return
    231      */
    232     public synchronized final Map<K, V> snapshot(){
    233         return new LinkedHashMap<K,V>(map);
    234     }
    235     
    236     @Override
    237     public synchronized final String toString() {
    238         int accesses =hitCount+missCount;
    239         int hitPercent=accesses!=0?(100 * hitCount/accesses):0;//缓存命中率是判断加速效果好坏的重要因素
    240         Iterator<Entry<K, V>> iterator= map.entrySet().iterator();  
    241         while(iterator.hasNext())  
    242         {  
    243             Entry<K, V> entry = iterator.next();  
    244             System.out.println(entry.getKey()+":"+entry.getValue());  
    245         } 
    246         return String.format("LruCache[缓存最大大小=%d,命中次数=%d,未命中次数=%d,命中率=%d%%]",
    247                         CacheCapcity, hitCount, missCount, hitPercent);
    248     }
    249     
    250     public static void main(String[] args) {
    251 
    252         LRUCacheSafe<Integer,String> lru = new LRUCacheSafe<Integer,String>(3);
    253         System.out.println("--------------------开始使用LRU缓存---------------");
    254        
    255         lru.put(1, "7");    
    256         System.out.println(lru.toString());
    257         lru.put(2, "0");    
    258         System.out.println(lru.toString());
    259         lru.put(3, "1");    
    260         System.out.println(lru.toString());
    261         lru.put(4, "2");     
    262         System.out.println(lru.toString());
    263         lru.put(1, "0");   
    264         System.out.println(lru.toString());
    265         lru.put(2, "3");   
    266         System.out.println(lru.toString());
    267         lru.put(5, "0");   
    268         System.out.println(lru.toString());
    269         lru.put(6, "4");   
    270         System.out.println(lru.toString());
    271         lru.put(7, "2");   
    272         System.out.println(lru.toString());
    273         lru.put(8, "3");   
    274         System.out.println(lru.toString());
    275         lru.put(9, "0");   
    276         System.out.println(lru.toString());
    277         lru.put(10, "3");   
    278         System.out.println(lru.toString());
    279         lru.put(11, "2");   
    280         System.out.println(lru.toString());
    281         lru.put(12, "1");   
    282         System.out.println(lru.toString());
    283         lru.put(13, "2");   
    284         System.out.println(lru.toString());
    285         lru.put(14, "0");   
    286         System.out.println(lru.toString());
    287         lru.put(15, "1");   
    288         System.out.println(lru.toString());
    289         lru.put(16, "7");   
    290         System.out.println(lru.toString());
    291         lru.put(17, "0");   
    292         System.out.println(lru.toString());
    293         lru.put(18, "1");   
    294         System.out.println(lru.toString());
    295         lru.get(1);         
    296         lru.get(18);         
    297         lru.get(2);         
    298         System.out.println(lru.toString());
    299         lru.remove(16);     
    300         System.out.println(lru.toString());
    301     }
    302 
    303 }

     

  • 相关阅读:
    NodeJS3-1基础API----Path(路径)
    NodeJS2-6环境&调试----debug
    NodeJS2-5环境&调试----process(进程)
    NodeJS2-4环境&调试----global变量
    NodeJS2-3环境&调试----module.exports与exports的区别
    短视频秒播优化实践(二)
    短视频秒播优化实践(一)
    仿抖音上下滑动播放视频
    带着问题,再读ijkplayer源码
    上班一个月,后悔当初着急入职的选择了
  • 原文地址:https://www.cnblogs.com/zzuuoo666/p/9098454.html
Copyright © 2011-2022 走看看