精华内容
下载资源
问答
  • 缓存淘汰算法

    2018-05-03 21:24:25
    缓存淘汰算法——LRU算法1. LRU1.1. 原理LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。1.2. 实现...

    缓存淘汰算法——LRU算法

    1. LRU
    1.1. 原理

    LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

    1.2. 实现

    最常见的实现是使用一个链表保存缓存数据,详细算法实现如下:

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

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

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

    1.3. 分析

    【命中率】

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

    【复杂度】

    实现简单。

    【代价】

    命中时需要遍历链表,找到命中的数据块索引,然后需要将数据移到头部。

     

    2. LRU-K

    2.1. 原理

    LRU-K中的K代表最近使用的次数,因此LRU可以认为是LRU-1。LRU-K的主要目的是为了解决LRU算法“缓存污染”的问题,其核心思想是将“最近使用过1次”的判断标准扩展为“最近使用过K次”。

    2.2. 实现

    相比LRU,LRU-K需要多维护一个队列,用于记录所有缓存数据被访问的历史。只有当数据的访问次数达到K次的时候,才将数据放入缓存。当需要淘汰数据时,LRU-K会淘汰第K次访问时间距当前时间最大的数据。详细实现如下:

    1. 数据第一次被访问,加入到访问历史列表;

    2. 如果数据在访问历史列表里后没有达到K次访问,则按照一定规则(FIFO,LRU)淘汰;

    3. 当访问历史队列中的数据访问次数达到K次后,将数据索引从历史队列删除,将数据移到缓存队列中,并缓存此数据,缓存队列重新按照时间排序;

    4. 缓存数据队列中被再次访问后,重新排序;

    5. 需要淘汰数据时,淘汰缓存队列中排在末尾的数据,即:淘汰“倒数第K次访问离现在最久”的数据。

    LRU-K具有LRU的优点,同时能够避免LRU的缺点,实际应用中LRU-2是综合各种因素后最优的选择,LRU-3或者更大的K值命中率会高,但适应性差,需要大量的数据访问才能将历史访问记录清除掉。

    2.3. 分析

    【命中率】

    LRU-K降低了“缓存污染”带来的问题,命中率比LRU要高。

    【复杂度】

    LRU-K队列是一个优先级队列,算法复杂度和代价比较高。

    【代价】

    由于LRU-K还需要记录那些被访问过、但还没有放入缓存的对象,因此内存消耗会比LRU要多;当数据量很大的时候,内存消耗会比较可观。

    LRU-K需要基于时间进行排序(可以需要淘汰时再排序,也可以即时排序),CPU消耗比LRU要高。

    3. Two queues(2Q)

    3.1. 原理

    Two queues(以下使用2Q代替)算法类似于LRU-2,不同点在于2Q将LRU-2算法中的访问历史队列(注意这不是缓存数据的)改为一个FIFO缓存队列,即:2Q算法有两个缓存队列,一个是FIFO队列,一个是LRU队列。

    3.2. 实现

    当数据第一次访问时,2Q算法将数据缓存在FIFO队列里面,当数据第二次被访问时,则将数据从FIFO队列移到LRU队列里面,两个队列各自按照自己的方法淘汰数据。详细实现如下:

    1. 新访问的数据插入到FIFO队列;

    2. 如果数据在FIFO队列中一直没有被再次访问,则最终按照FIFO规则淘汰;

    3. 如果数据在FIFO队列中被再次访问,则将数据移到LRU队列头部;

    4. 如果数据在LRU队列再次被访问,则将数据移到LRU队列头部;

    5. LRU队列淘汰末尾的数据。

     

    注:上图中FIFO队列比LRU队列短,但并不代表这是算法要求,实际应用中两者比例没有硬性规定。

    3.3. 分析

    【命中率】

    2Q算法的命中率要高于LRU。

    【复杂度】

    需要两个队列,但两个队列本身都比较简单。

    【代价】

    FIFO和LRU的代价之和。

    2Q算法和LRU-2算法命中率类似,内存消耗也比较接近,但对于最后缓存的数据来说,2Q会减少一次从原始存储读取数据或者计算数据的操作。

    4. Multi Queue(MQ)

    4.1. 原理

    MQ算法根据访问频率将数据划分为多个队列,不同的队列具有不同的访问优先级,其核心思想是:优先缓存访问次数多的数据。

    4.2. 实现

    MQ算法将缓存划分为多个LRU队列,每个队列对应不同的访问优先级。访问优先级是根据访问次数计算出来的,例如

    详细的算法结构图如下,Q0,Q1....Qk代表不同的优先级队列,Q-history代表从缓存中淘汰数据,但记录了数据的索引和引用次数的队列:

     

    如上图,算法详细描述如下:

    1. 新插入的数据放入Q0;

    2. 每个队列按照LRU管理数据;

    3. 当数据的访问次数达到一定次数,需要提升优先级时,将数据从当前队列删除,加入到高一级队列的头部;

    4. 为了防止高优先级数据永远不被淘汰,当数据在指定的时间里访问没有被访问时,需要降低优先级,将数据从当前队列删除,加入到低一级的队列头部;

    5. 需要淘汰数据时,从最低一级队列开始按照LRU淘汰;每个队列淘汰数据时,将数据从缓存中删除,将数据索引加入Q-history头部;

    6. 如果数据在Q-history中被重新访问,则重新计算其优先级,移到目标队列的头部;

    7. Q-history按照LRU淘汰数据的索引。

    4.3. 分析

    【命中率】

    MQ降低了“缓存污染”带来的问题,命中率比LRU要高。

    【复杂度】

    MQ需要维护多个队列,且需要维护每个数据的访问时间,复杂度比LRU高。

    【代价】

    MQ需要记录每个数据的访问时间,需要定时扫描所有队列,代价比LRU要高。

    注:虽然MQ的队列看起来数量比较多,但由于所有队列之和受限于缓存容量的大小,因此这里多个队列长度之和和一个LRU队列是一样的,因此队列扫描性能也相近。

     

    5. LRU类算法对比

    由于不同的访问模型导致命中率变化较大,此处对比仅基于理论定性分析,不做定量分析。

    对比点

    对比

    命中率

    LRU-2 > MQ(2) > 2Q > LRU

    复杂度

    LRU-2 > MQ(2) > 2Q > LRU

    代价

    LRU-2  > MQ(2) > 2Q > LRU

    实际应用中需要根据业务的需求和对数据的访问情况进行选择,并不是命中率越高越好。例如:虽然LRU看起来命中率会低一些,且存在”缓存污染“的问题,但由于其简单和代价小,实际应用中反而应用更多。

     

    java中最简单的LRU算法实现,就是利用jdk的LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可

    如果你去看LinkedHashMap的源码可知,LRU算法是通过双向链表来实现,当某个位置被命中,通过调整链表的指向将该位置调整到头位置,新加入的内容直接放在链表头,如此一来,最近被命中的内容就向链表头移动,需要替换时,链表最后的位置就是最近最少使用的位置

    import java.util.ArrayList;  
    import java.util.Collection;  
    import java.util.LinkedHashMap;  
    import java.util.concurrent.locks.Lock;  
    import java.util.concurrent.locks.ReentrantLock;  
    import java.util.Map;  
       
       
    /** 
     * 类说明:利用LinkedHashMap实现简单的缓存, 必须实现removeEldestEntry方法,具体参见JDK文档 
     *  
     * @author dennis 
     *  
     * @param <K> 
     * @param <V> 
     */ 
    public class LRULinkedHashMap<K, V> extends LinkedHashMap<K, V> {  
        private final int maxCapacity;  
       
        private static final float DEFAULT_LOAD_FACTOR = 0.75f;  
       
        private final Lock lock = new ReentrantLock();  
       
        public LRULinkedHashMap(int maxCapacity) {  
            super(maxCapacity, DEFAULT_LOAD_FACTOR, true);  
            this.maxCapacity = maxCapacity;  
        }  
       
        @Override 
        protected boolean removeEldestEntry(java.util.Map.Entry<K, V> eldest) {  
            return size() > maxCapacity;  
        }  
        @Override 
        public boolean containsKey(Object key) {  
            try {  
                lock.lock();  
                return super.containsKey(key);  
            } finally {  
                lock.unlock();  
            }  
        }  
       
           
        @Override 
        public V get(Object key) {  
            try {  
                lock.lock();  
                return super.get(key);  
            } finally {  
                lock.unlock();  
            }  
        }  
       
        @Override 
        public V put(K key, V value) {  
            try {  
                lock.lock();  
                return super.put(key, value);  
            } finally {  
                lock.unlock();  
            }  
        }  
       
        public int size() {  
            try {  
                lock.lock();  
                return super.size();  
            } finally {  
                lock.unlock();  
            }  
        }  
       
        public void clear() {  
            try {  
                lock.lock();  
                super.clear();  
            } finally {  
                lock.unlock();  
            }  
        }  
       
        public Collection<Map.Entry<K, V>> getAll() {  
            try {  
                lock.lock();  
                return new ArrayList<Map.Entry<K, V>>(super.entrySet());  
            } finally {  
                lock.unlock();  
            }  
        }  
    }  

    基于双链表 的LRU实现:

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

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

        它的原理: 将Cache的所有位置都用双连表连接起来,当一个位置被命中之后,就将通过调整链表的指向,将该位置调整到链表头的位置,新加入的Cache直接加到链表头中。

         这样,在多次进行Cache操作后,最近被命中的,就会被向链表头方向移动,而没有命中的,而想链表后面移动,链表尾则表示最近最少使用的Cache。

         当需要替换内容时候,链表的最后位置就是最少被命中的位置,我们只需要淘汰链表最后的部分即可。

      上面说了这么多的理论, 下面用代码来实现一个LRU策略的缓存。

        我们用一个对象来表示Cache,并实现双链表,

    Java代码  
    public class LRUCache {  
        /** 
         * 链表节点 
         * @author Administrator 
         * 
         */  
        class CacheNode {  
            ……  
        }  
        private int cacheSize;//缓存大小  
        private Hashtable nodes;//缓存容器  
        private int currentSize;//当前缓存对象数量  
        private CacheNode first;//(实现双链表)链表头  
        private CacheNode last;//(实现双链表)链表尾  
    }  

          下面给出完整的实现,这个类也被Tomcat所使用( org.apache.tomcat.util.collections.LRUCache),但是在tomcat6.x版本中,已经被弃用,使用另外其他的缓存类来替代它。

    public class LRUCache {  
        /** 
         * 链表节点 
         * @author Administrator 
         * 
         */  
        class CacheNode {  
            CacheNode prev;//前一节点  
            CacheNode next;//后一节点  
            Object value;//值  
            Object key;//键  
            CacheNode() {  
            }  
        }  
        public LRUCache(int i) {  
            currentSize = 0;  
            cacheSize = i;  
            nodes = new Hashtable(i);//缓存容器  
        }  
          
        /** 
         * 获取缓存中对象 
         * @param key 
         * @return 
         */  
        public Object get(Object key) {  
            CacheNode node = (CacheNode) nodes.get(key);  
            if (node != null) {  
                moveToHead(node);  
                return node.value;  
            } else {  
                return null;  
            }  
        }  
          
        /** 
         * 添加缓存 
         * @param key 
         * @param value 
         */  
        public void put(Object key, Object value) {  
            CacheNode node = (CacheNode) nodes.get(key);  
              
            if (node == null) {  
                //缓存容器是否已经超过大小.  
                if (currentSize >= cacheSize) {  
                    if (last != null)//将最少使用的删除  
                        nodes.remove(last.key);  
                    removeLast();  
                } else {  
                    currentSize++;  
                }  
                  
                node = new CacheNode();  
            }  
            node.value = value;  
            node.key = key;  
            //将最新使用的节点放到链表头,表示最新使用的.  
            moveToHead(node);  
            nodes.put(key, node);  
        }  
        /** 
         * 将缓存删除 
         * @param key 
         * @return 
         */  
        public Object remove(Object key) {  
            CacheNode node = (CacheNode) nodes.get(key);  
            if (node != null) {  
                if (node.prev != null) {  
                    node.prev.next = node.next;  
                }  
                if (node.next != null) {  
                    node.next.prev = node.prev;  
                }  
                if (last == node)  
                    last = node.prev;  
                if (first == node)  
                    first = node.next;  
            }  
            return node;  
        }  
        public void clear() {  
            first = null;  
            last = null;  
        }  
        /** 
         * 删除链表尾部节点 
         *  表示 删除最少使用的缓存对象 
         */  
        private void removeLast() {  
            //链表尾不为空,则将链表尾指向null. 删除连表尾(删除最少使用的缓存对象)  
            if (last != null) {  
                if (last.prev != null)  
                    last.prev.next = null;  
                else  
                    first = null;  
                last = last.prev;  
            }  
        }  
          
        /** 
         * 移动到链表头,表示这个节点是最新使用过的 
         * @param node 
         */  
        private void moveToHead(CacheNode node) {  
            if (node == first)  
                return;  
            if (node.prev != null)  
                node.prev.next = node.next;  
            if (node.next != null)  
                node.next.prev = node.prev;  
            if (last == node)  
                last = node.prev;  
            if (first != null) {  
                node.next = first;  
                first.prev = node;  
            }  
            first = node;  
            node.prev = null;  
            if (last == null)  
                last = first;  
        }  
        private int cacheSize;  
        private Hashtable nodes;//缓存容器  
        private int currentSize;  
        private CacheNode first;//链表头  
        private CacheNode last;//链表尾  
    }<br style="margin: 0px; padding: 0px;"><br style="margin: 0px; padding: 0px;">  

    展开全文
  • Java缓存淘汰算法

    千次阅读 2020-12-14 09:50:35
    1、Redis缓存淘汰算法 2、Memcache缓存淘汰算法 3、Ehcache缓存淘汰算法 4、Guava缓存淘汰算法 5、Caffein缓存淘汰算法 1、Redis缓存淘汰算法 2、Memcache缓存淘汰算法 3、Ehcache缓存淘汰算法 4、...

    目录

    1、Redis缓存淘汰算法

    2、Memcache缓存淘汰算法

    3、Ehcache缓存淘汰算法

    4、Guava缓存淘汰算法

     4.1 Eviction by Size

    4.1  Eviction by Weight

    4.3   Eviction by Time

    4.4  Weak Keys

    4.5  Soft Values

    5、Caffein缓存淘汰算法

    5.1 Size-based

    5.1 Time-based

    5.3 Reference-based

    6、LFU淘汰算法的Java实现

    7、LRU淘汰算法的Java实现

    8、FIFO淘汰算法的Java实现


    1、Redis缓存淘汰算法

       摘自Redis原文:https://redis.io/topics/config

           If you plan to use Redis just as a cache where every key will have an expire set, you may consider using the following configuration instead (assuming a max memory limit of 2 megabytes as an example):

    maxmemory 2mb
    maxmemory-policy allkeys-lru
    #面试题 maxmemory-samples是干什么的?
    maxmemory-samples 5

    The following policies are available:  原文点击

    • noeviction: return errors when the memory limit was reached and the client is trying to execute commands that could result in more memory to be used (most write commands, but DEL and a few more exceptions).
    • allkeys-lru: evict keys by trying to remove the less recently used (LRU) keys first, in order to make space for the new data added.
    • volatile-lru: evict keys by trying to remove the less recently used (LRU) keys first, but only among keys that have an expire set, in order to make space for the new data added.
    • allkeys-random: evict keys randomly in order to make space for the new data added.
    • volatile-random: evict keys randomly in order to make space for the new data added, but only evict keys with an expire set.
    • volatile-ttl: evict keys with an expire set, and try to evict keys with a shorter time to live (TTL) first, in order to make space for the new data added.

    Redis4之后新增了两种 allkey-lfuvolatile-lfu 两种策略

    问题1: Redis为什么使用近似LFU算法?
    问题2:如何提高近似LFU算法精度?
    问题3:如何选择适合自己的maxmemory-policy?

    2、Memcache缓存淘汰算法

    https://memcached.org/blog/modern-lru/

    3、Ehcache缓存淘汰算法

            参考文档:参考原文

    <cache name="myCache"
          maxEntriesLocalDisk="10000" eternal="false" timeToIdleSeconds="3600"
          timeToLiveSeconds="0" memoryStoreEvictionPolicy="LFU">
    </cache>

     

    Note the following about the myCache configuration:

    • Accessing an entry in myCache that has been idle for more than an hour (timeToIdleSeconds) causes that element to be evicted.
    • If an entry expires but is not accessed, and no resource constraints force eviction, then the expired entry remains in place.
    • Entries in myCache can remain in the cache forever if accessed at least once per 60 minutes (timeToLiveSeconds). However, unexpired entries may still be flushed based on other limitations (see How to Size Caches).
    • In all, myCache can store a maximum of 10000 entries (maxEntriesLocalDisk). This is the effective maximum number of entries myCache is allowed. Note, however, that this value may be exceeded as it is overridden by settings such as pinning.

    memoryStoreEvictionPolicy支持以下淘汰策略:

    1. LRU - least recently used
    2. LFU - least frequently used
    3. FIFO - first in first out, the oldest element by creation time

    4、Guava缓存淘汰算法

            参考文档:参考原文

     4.1 Eviction by Size

          We can limit the size of our cache using maximumSize(). If the cache reaches the limit, the oldest items will be evicted.

    @Test
    public void whenCacheReachMaxSize_thenEviction() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().maximumSize(3).build(loader);
    
        cache.getUnchecked("first");
        cache.getUnchecked("second");
        cache.getUnchecked("third");
        cache.getUnchecked("forth");
        assertEquals(3, cache.size());
        assertNull(cache.getIfPresent("first"));
        assertEquals("FORTH", cache.getIfPresent("forth"));
    }

     

    4.1  Eviction by Weight

    We can also limit the cache size using a custom weight function. In the following code, we use the length as our custom weight function:

    @Test
    public void whenCacheReachMaxWeight_thenEviction() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
    
        Weigher<String, String> weighByLength;
        weighByLength = new Weigher<String, String>() {
            @Override
            public int weigh(String key, String value) {
                return value.length();
            }
        };
    
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
          .maximumWeight(16)
          .weigher(weighByLength)
          .build(loader);
    
        cache.getUnchecked("first");
        cache.getUnchecked("second");
        cache.getUnchecked("third");
        cache.getUnchecked("last");
        assertEquals(3, cache.size());
        assertNull(cache.getIfPresent("first"));
        assertEquals("LAST", cache.getIfPresent("last"));
    }

    Note: The cache may remove more than one record to leave room for a new large one.

    4.3   Eviction by Time

    Beside using size to evict old records, we can use time. In the following example, we customize our cache to remove records that have been idle for 2ms:

    @Test
    public void whenEntryIdle_thenEviction()
      throws InterruptedException {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
    
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
          .expireAfterAccess(2,TimeUnit.MILLISECONDS)
          .build(loader);
    
        cache.getUnchecked("hello");
        assertEquals(1, cache.size());
    
        cache.getUnchecked("hello");
        Thread.sleep(300);
    
        cache.getUnchecked("test");
        assertEquals(1, cache.size());
        assertNull(cache.getIfPresent("hello"));
    }

    We can also evict records based on their total live time. In the following example, the cache will remove the records after 2ms of being stored:

    @Test
    public void whenEntryLiveTimeExpire_thenEviction()
      throws InterruptedException {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
    
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder()
          .expireAfterWrite(2,TimeUnit.MILLISECONDS)
          .build(loader);
    
        cache.getUnchecked("hello");
        assertEquals(1, cache.size());
        Thread.sleep(300);
        cache.getUnchecked("test");
        assertEquals(1, cache.size());
        assertNull(cache.getIfPresent("hello"));
    }

     

    4.4  Weak Keys

             Next, let's see how to make our cache keys have weak references – allowing the garbage collector to collect cache keys that are not referenced elsewhere.
    By default, both cache keys and values have strong references but we can make our cache store the keys using weak references using weakKeys() as in the following example:

    @Test
    public void whenWeakKeyHasNoRef_thenRemoveFromCache() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
    
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().weakKeys().build(loader);
    }

    4.5  Soft Values

             We can allow the garbage collector to collect our cached values by using softValues() as in the following example:

    @Test
    public void whenSoftValue_thenRemoveFromCache() {
        CacheLoader<String, String> loader;
        loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                return key.toUpperCase();
            }
        };
    
        LoadingCache<String, String> cache;
        cache = CacheBuilder.newBuilder().softValues().build(loader);
    }
    Note: Many soft references may affect the system performance – it's preferred to use maximumSize().

     

    5、Caffein缓存淘汰算法

          参考文档:参考原文

    Caffeine provides three types of eviction: size-based eviction, time-based eviction, and reference-based eviction.

    5.1 Size-based

    // Evict based on the number of entries in the cache
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumSize(10_000)
        .build(key -> createExpensiveGraph(key));
    
    // Evict based on the number of vertices in the cache
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .maximumWeight(10_000)
        .weigher((Key key, Graph graph) -> graph.vertices().size())
        .build(key -> createExpensiveGraph(key));

    5.1 Time-based

    // Evict based on a fixed expiration policy
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfterAccess(5, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfterWrite(10, TimeUnit.MINUTES)
        .build(key -> createExpensiveGraph(key));
    
    // Evict based on a varying expiration policy
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .expireAfter(new Expiry<Key, Graph>() {
          public long expireAfterCreate(Key key, Graph graph, long currentTime) {
            // Use wall clock time, rather than nanotime, if from an external resource
            long seconds = graph.creationDate().plusHours(5)
                .minus(System.currentTimeMillis(), MILLIS)
                .toEpochSecond();
            return TimeUnit.SECONDS.toNanos(seconds);
          }
          public long expireAfterUpdate(Key key, Graph graph, 
              long currentTime, long currentDuration) {
            return currentDuration;
          }
          public long expireAfterRead(Key key, Graph graph,
              long currentTime, long currentDuration) {
            return currentDuration;
          }
        })
        .build(key -> createExpensiveGraph(key));

    5.3 Reference-based

    // Evict when neither the key nor value are strongly reachable
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .weakKeys()
        .weakValues()
        .build(key -> createExpensiveGraph(key));
    
    // Evict when the garbage collector needs to free memory
    LoadingCache<Key, Graph> graphs = Caffeine.newBuilder()
        .softValues()
        .build(key -> createExpensiveGraph(key));

     

    6、LFU淘汰算法的Java实现

                             LFU(least frequently used) 最少使用率缓存 ,根据使用次数来判定对象是否被持续缓存  , 使用率是通过访问次数计算的。 
              ,当缓存满时清理过期对象。 , 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。

    参考:HuTool工具包中的 cn.hutool.cache.impl.LFUCache 

    /**
    	 * 清理过期对象。<br>
    	 * 清理后依旧满的情况下清除最少访问(访问计数最小)的对象并将其他对象的访问数减去这个最小访问数,以便新对象进入后可以公平计数。
    	 * 
    	 * @return 清理个数
    	 */
    	@Override
    	protected int pruneCache() {
    		int count = 0;
    		CacheObj<K, V> comin = null;
    
    		// 清理过期对象并找出访问最少的对象
    		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
    		CacheObj<K, V> co;
    		while (values.hasNext()) {
    			co = values.next();
    			if (co.isExpired() == true) {
    				values.remove();
    				onRemove(co.key, co.obj);
    				count++;
    				continue;
    			}
    
    			//找出访问最少的对象
    			if (comin == null || co.accessCount.get() < comin.accessCount.get()) {
    				comin = co;
    			}
    		}
    
    		// 减少所有对象访问量,并清除减少后为0的访问对象
    		if (isFull() && comin != null) {
    			long minAccessCount = comin.accessCount.get();
    
    			values = cacheMap.values().iterator();
    			CacheObj<K, V> co1;
    			while (values.hasNext()) {
    				co1 = values.next();
    				if (co1.accessCount.addAndGet(-minAccessCount) <= 0) {
    					values.remove();
    					onRemove(co1.key, co1.obj);
    					count++;
    				}
    			}
    		}
    		
    		return count;
    	}

    7、LRU淘汰算法的Java实现

               LRU (least recently used)最近最久未使用缓存,根据使用时间来判定对象是否被持续缓存 ,当对象被访问时放入缓存,当缓存满了,最久未被使用的对象将被移除。 
    ,此缓存基于LinkedHashMap,因此当被缓存的对象每被访问一次,这个对象的key就到链表头部。 这个算法简单并且非常快,他比FIFO有一个显著优势是经常使用的对象不太可能被移除缓存。
     缺点是当缓存满时,不能被很快的访问。

    参考:HuTool工具包中的 cn.hutool.cache.impl.LRUCache

    	/**
    	 * 只清理超时对象,LRU的实现会交给<code>LinkedHashMap</code>
    	 */
    	@Override
    	protected int pruneCache() {
    		if (isPruneExpiredActive() == false) {
    			return 0;
    		}
    		int count = 0;
    		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
    		CacheObj<K, V> co;
    		while (values.hasNext()) {
    			co = values.next();
    			if (co.isExpired()) {
    				values.remove();
    				onRemove(co.key, co.obj);
    				count++;
    			}
    		}
    		return count;
    	}

     

    8、FIFO淘汰算法的Java实现

    元素不停的加入缓存直到缓存满为止,当缓存满时,清理过期缓存对象,清理后依旧满则删除先入的缓存(链表首部对象)。
     优点:简单快速 <br>
     缺点:不灵活,不能保证最常用的对象总是被保留

    参考:HuTool工具包中的 cn.hutool.cache.impl.FIFOCache

    /**
    	 * 先进先出的清理策略<br>
    	 * 先遍历缓存清理过期的缓存对象,如果清理后还是满的,则删除第一个缓存对象
    	 */
    	@Override
    	protected int pruneCache() {
    		int count = 0;
    		CacheObj<K, V> first = null;
    
    		// 清理过期对象并找出链表头部元素(先入元素)
    		Iterator<CacheObj<K, V>> values = cacheMap.values().iterator();
    		while (values.hasNext()) {
    			CacheObj<K, V> co = values.next();
    			if (co.isExpired()) {
    				values.remove();
    				count++;
    			}
    			if (first == null) {
    				first = co;
    			}
    		}
    
    		// 清理结束后依旧是满的,则删除第一个被缓存的对象
    		if (isFull() && null != first) {
    			cacheMap.remove(first.key);
    			onRemove(first.key, first.obj);
    			count++;
    		}
    		return count;
    	}

     

    展开全文
  • 常用的缓存淘汰算法
  • 缓存淘汰算法之LRU

    2017-05-05 11:50:53
    缓存淘汰算法之LRU
  • 主要介绍了golang实现LRU缓存淘汰算法的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
  • 手写LRU缓存淘汰算法

    2021-03-01 21:58:29
    手写LRU缓存淘汰算法 背景 在我们这个日益追求高效的世界,我们对任何事情的等待都显得十分的浮躁,网页页面刷新不出来,好烦,电脑打开运行程序慢,又是好烦!那怎么办,技术的产生不就是我们所服务么,今天我们就...

    手写LRU缓存淘汰算法

    背景

    在我们这个日益追求高效的世界,我们对任何事情的等待都显得十分的浮躁,网页页面刷新不出来,好烦,电脑打开运行程序慢,又是好烦!那怎么办,技术的产生不就是我们所服务么,今天我们就聊一聊缓存这个技术,并使用我们熟知的数据结构--用链表实现LRU缓存淘汰算法

    在学习如何使用链表实现LRU缓存淘汰算法前,我们先提出几个问题,大家好好思考下,问题如下:

    • 什么是缓存,缓存的作用?
    • 缓存的淘汰策略有哪些?
    • 如何使用链表实现LRU缓存淘汰算法,有什么特点,如何优化?

    好了,我们带着上面的问题来学进行下面的学习。

    1、什么是缓存,缓存的作用是什么?

    缓存可以简单的理解为保存数据的一个副本,以便于后续能够快速的进行访问。以计算机的使用场景为例,当cpu要访问内存中的一条数据时,它会先在缓存里查找,如果能够找到则直接使用,如果没找到,则需要去内存里查找;

    同样的,在数据库的访问场景中,当项目系统需要查询数据库中的某条数据时,可以先让请求查询缓存,如果命中,就直接返回缓存的结果,如果没有命中,就查询数据库, 并将查询结果放入缓存,下次请求时查询缓存命中,直接返回结果,就不用再次查询数据库。

    通过以上两个例子,我们发现无论在哪种场景下,都存在这样一个顺序:先缓存,后内存先缓存,后数据库。但是缓存的存在也占用了一部分内存空间,所以缓存是典型的以空间换时间,牺牲数据的实时性,却满足计算机运行的高效性

    仔细想一下,我们日常开发中遇到缓存的例子还挺多的。

    • 操作系统的缓存

    减少与磁盘的交互

    • 数据库缓存

    减少对数据库的查询

    • Web服务器缓存

    减少对应用服务器的请求

    • 客户浏览器的缓存

    减少对网站的访问

    2、缓存有哪些淘汰策略?

    缓存的本质是以空间换时间,那么缓存的容量大小肯定是有限的,当缓存被占满时,缓存中的那些数据应该被清理出去,那些数据应该被保留呢?这就需要缓存的淘汰策略来决定。

    事实上,常用的缓存的淘汰策略有三种:先进先出算法(First in First out FIFO);淘汰一定时期内被访问次数最少的页面(Least Frequently Used LFU);淘汰最长时间未被使用的页面(Least Recently Used LRU)

    这些算法在不同层次的缓存上执行时具有不同的效率,需要结合具体的场景来选择。

    2.1 FIFO算法

    FIFO算法即先进先出算法,常采用队列实现。在缓存中,它的设计原则是:如果一个数据最先进入缓存中,则应该最早淘汰掉

    image-20210227115124480

    • 新访问的数据插入FIFO队列的尾部,队列中数据由队到队头按顺序顺序移动
    • 队列满时,删除队头的数据

    2.2 LRU算法

    LRU算法是根据对数据的历史访问次数来进行淘汰数据的,通常使用链表来实现。在缓存中,它的设计原则是:如果数据最近被访问过,那么将来它被访问的几率也很高。

    image-20210227120433527

    • 新加入的数据插入到链表的头部
    • 每当缓存命中时(即缓存数据被访问),则将数据移到链表头部
    • 当来链表已满的时候,将链表尾部的数据丢弃

    2.3 LFU算法

    LFU算法是根据数据的历史访问频率来淘汰数据,因此,LFU算法中的每个数据块都有一个引用计数,所有数据块按照引用计数排序,具有相同引用计数的数据块则按照时间排序。在缓存中,它的设计原则是:如果数据被访问多次,那么将来它的访问频率也会更高

    image-20210227121952816

    • 新加入数据插入到队列尾部(引用计数为1;
    • 队列中的数据被访问后,引用计数增加,队列重新排序;
    • 当需要淘汰数据时,将已经排序的列表最后的数据块删除。

    3、如何使用链表实现缓存淘汰,有什么特点,如何优化?

    在上面的文章中我们理解了缓存的概念淘汰策略,其中LRU算法是笔试/面试中考察比较频繁的,我秋招的时候,很多公司都让我手写了这个算法,为了避免大家采坑,下面,我们就手写一个LRU缓存淘汰算法。

    我们都知道链表的形式不止一种,我们应该选择哪一种呢?

    思考三分钟........

    好了,公布答案!

    事实上,链表按照不同的连接结构可以划分为单链表循环链表双向链表

    • 单链表
    • 每个节点只包含一个指针,即后继指针。
    • 单链表有两个特殊的节点,即首节点和尾节点,用首节点地址表示整条链表,尾节点的后继指针指向空地址null。
    • 性能特点:插入和删除节点的时间复杂度为O(1),查找的时间复杂度为O(n)。
    • 循环链表
    • 除了尾节点的后继指针指向首节点的地址外均与单链表一致。
    • 适用于存储有循环特点的数据,比如约瑟夫问题。
    • 双向链表
    • 节点除了存储数据外,还有两个指针分别指向前一个节点地址(前驱指针prev)和下一个节点地址(后继指针next)

    • 首节点的前驱指针prev和尾节点的后继指针均指向空地址。

    双向链表相较于单链表的一大优势在于:找到前驱节点的时间复杂度为O(1),而单链表只能从头节点慢慢往下找,所以仍然是O(n).而且,对于插入和删除也是有优化的。

    我们可能会有问题:单链表的插入删除不是O(1)吗?

    是的,但是一般情况下,我们想要进行插入删除操作,很多时候还是得先进行查找,再插入或者删除,可见其实是先O(n),再O(1)。

    不熟悉链表解题的同学可以先看看我的上一篇算法解析文章刷了LeetCode链表专题,我发现了一个秘密

    因为我们需要删除操作,删除一个节点不仅要得到该节点本身的指针,也需要操作其它前驱节点的指针,而双向链表能够直接找到前驱,保证了操作时间复杂度为O(1),因此使用双向链表作为实现LRU缓存淘汰算法的结构会更高效。

    算法思路

    维护一个双向链表,保存所有缓存的值,并且最老的值放在链表最后面。

    • 当访问的值在链表中时: 将找到链表中值将其删除,并重新在链表头添加该值(保证链表中 数值的顺序是从新到旧)
    • 当访问的值不在链表中时: 当链表已满:删除链表最后一个值,将要添加的值放在链表头 当链表未满:直接在链表头添加

    3.1 LRU缓存淘汰算法

    极客时间王争的《数据结构与算法之美》给出了一个使用有序单链表实现LRU缓存淘汰算法,代码如下:

    public class LRUBaseLinkedList<T> {
    
        /**
         * 默认链表容量
         */
        private final static Integer DEFAULT_CAPACITY = 10;
    
        /**
         * 头结点
         */
        private SNode<T> headNode;
    
        /**
         * 链表长度
         */
        private Integer length;
    
        /**
         * 链表容量
         */
        private Integer capacity;
    
        public LRUBaseLinkedList() {
            this.headNode = new SNode<>();
            this.capacity = DEFAULT_CAPACITY;
            this.length = 0;
        }
    
        public LRUBaseLinkedList(Integer capacity) {
            this.headNode = new SNode<>();
            this.capacity = capacity;
            this.length = 0;
        }
    
        public void add(T data) {
            SNode preNode = findPreNode(data);
    
            // 链表中存在,删除原数据,再插入到链表的头部
            if (preNode != null) {
                deleteElemOptim(preNode);
                intsertElemAtBegin(data);
            } else {
                if (length >= this.capacity) {
                    //删除尾结点
                    deleteElemAtEnd();
                }
                intsertElemAtBegin(data);
            }
        }
    
        /**
         * 删除preNode结点下一个元素
         *
         * @param preNode
         */
        private void deleteElemOptim(SNode preNode) {
            SNode temp = preNode.getNext();
            preNode.setNext(temp.getNext());
            temp = null;
            length--;
        }
    
        /**
         * 链表头部插入节点
         *
         * @param data
         */
        private void intsertElemAtBegin(T data) {
            SNode next = headNode.getNext();
            headNode.setNext(new SNode(data, next));
            length++;
        }
    
        /**
         * 获取查找到元素的前一个结点
         *
         * @param data
         * @return
         */
        private SNode findPreNode(T data) {
            SNode node = headNode;
            while (node.getNext() != null) {
                if (data.equals(node.getNext().getElement())) {
                    return node;
                }
                node = node.getNext();
            }
            return null;
        }
    
        /**
         * 删除尾结点
         */
        private void deleteElemAtEnd() {
            SNode ptr = headNode;
            // 空链表直接返回
            if (ptr.getNext() == null) {
                return;
            }
    
            // 倒数第二个结点
            while (ptr.getNext().getNext() != null) {
                ptr = ptr.getNext();
            }
    
            SNode tmp = ptr.getNext();
            ptr.setNext(null);
            tmp = null;
            length--;
        }
    
        private void printAll() {
            SNode node = headNode.getNext();
            while (node != null) {
                System.out.print(node.getElement() + ",");
                node = node.getNext();
            }
            System.out.println();
        }
    
        public class SNode<T> {
    
            private T element;
    
            private SNode next;
    
            public SNode(T element) {
                this.element = element;
            }
    
            public SNode(T element, SNode next) {
                this.element = element;
                this.next = next;
            }
    
            public SNode() {
                this.next = null;
            }
    
            public T getElement() {
                return element;
            }
    
            public void setElement(T element) {
                this.element = element;
            }
    
            public SNode getNext() {
                return next;
            }
    
            public void setNext(SNode next) {
                this.next = next;
            }
        }
    
        public static void main(String[] args) {
            LRUBaseLinkedList list = new LRUBaseLinkedList();
            Scanner sc = new Scanner(System.in);
            while (true) {
                list.add(sc.nextInt());
                list.printAll();
            }
        }
    }

    这段代码不管缓存有没有满,都需要遍历一遍链表,所以这种基于链表的实现思路,缓存访问的时间复杂度为 O(n)。

    3.2使用哈希表优化LRU

    事实上,这个思路还可以继续优化,我们可以把单链表换成双向链表,并引入散列表

    • 双向链表支持查找前驱,保证操作的时间复杂度为O(1)
    • 引入散列表记录每个数据的位置,将缓存访问的时间复杂度降到O(1)

    哈希表查找较快,但是数据无固定的顺序;链表倒是有顺序之分。插入、删除较快,但是查找较慢。将它们结合,就可以形成一种新的数据结构--哈希链表(LinkedHashMap)

    image-20210227203448255

    力扣上146题-LRU缓存机制刚好可以拿来练手,题图如下:

    题目:

    运用你所掌握的数据结构,设计和实现一个 LRU (最近最少使用) 缓存机制 。

    • 实现 LRUCache 类:

    LRUCache(int capacity) 以正整数作为容量 capacity 初始化 LRU 缓存 int get(int key) 如果关键字 key 存在于缓存中,则返回关键字的值,否则返回 -1 。 void put(int key, int value) 如果关键字已经存在,则变更其数据值;如果关键字不存在,则插入该组「关键字-值」。当缓存容量达到上限时,它应该在写入新数据之前删除最久未使用的数据值,从而为新的数据值留出空间。

    思路:

    我们的思路就是哈希表+双向链表

    • 哈希表用于满足题目时间复杂度O(1)的要求,双向链表用于存储顺序
    • 哈希表键值类型:<Integer, ListNode>,哈希表的键用于存储输入的key,哈希表的值用于存储双向链表的节点
    • 双向链表的节点中除了value外还需要包含key,因为在删除最久未使用的数据时,需要通过链表来定位hashmap中应当删除的键值对
    • 一些操作:双向链表中,在后面的节点表示被最近访问
    • 新加入的节点放在链表末尾,addNodeToLast(node)
    • 若容量达到上限,去除最久未使用的数据,removeNode(head.next)
    • 若数据新被访问过,比如被get了或被put了新值,把该节点挪到链表末尾,moveNodeToLast(node)
    • 为了操作的方便,在双向链表头和尾分别定义一个head和tail节点。

    代码

    class LRUCache {
        private int capacity;
        private HashMap<Integer, ListNode> hashmap; 
        private ListNode head;
        private ListNode tail;
    
        private class ListNode{
            int key;
            int val;
            ListNode prev;
            ListNode next;
            public ListNode(){  
            }
            public ListNode(int key, int val){
                this.key = key;
                this.val = val;
            }
        }
    
        public LRUCache(int capacity) {
            this.capacity = capacity;
            hashmap = new HashMap<>();
            head = new ListNode();
            tail = new ListNode();
            head.next = tail;
            tail.prev = head;
        }
    
        private void removeNode(ListNode node){
            node.prev.next = node.next;
            node.next.prev = node.prev;
        }
    
        private void addNodeToLast(ListNode node){
            node.prev = tail.prev;
            node.prev.next = node;
            node.next = tail;
            tail.prev= node;
        }
    
        private void moveNodeToLast(ListNode node){
            removeNode(node);
            addNodeToLast(node);
        }
    
        public int get(int key) {   
            if(hashmap.containsKey(key)){
                ListNode node = hashmap.get(key);
                moveNodeToLast(node);
                return node.val;
            }else{
                return -1;
            }
        }
    
        public void put(int key, int value) {
            if(hashmap.containsKey(key)){
                ListNode node = hashmap.get(key);
                node.val = value;
                moveNodeToLast(node);
                return;
            }
            if(hashmap.size() == capacity){
                hashmap.remove(head.next.key);
                removeNode(head.next);
            }
    
            ListNode node = new ListNode(key, value);
            hashmap.put(key, node);
            addNodeToLast(node);
        }
    }
    

    巨人的肩膀

    [1]数据结构与算法之美-王争

    [2]力扣-LRU缓存机制(146题)

    [3]https://blog.csdn.net/yangpl_tale/article/details/44998423

    [4]https://leetcode-cn.com/problems/lru-cache/solution/146-lru-huan-cun-ji-zhi-ha-xi-biao-shuan-l3um/

    本文由博客群发一文多发等运营工具平台 OpenWrite 发布

    展开全文
  • 缓存淘汰算法LRU

    2021-07-27 22:28:38
    LRU(Least Recently Used)算法是一种缓存淘汰算法,当系统的缓存过大,达到删除缓存的阀值时,最先删除最久没有被使用过的数据,被用于操作系统和Redis数据库中。 LRU的底层是由哈希算法+链表组成的。哈希算法具有...

    1 概述

    LRU(Least Recently Used)算法是一种缓存淘汰算法,被用于操作系统和Redis数据库中。LRU核心思想是如果数据最近被访问过,那么将来被访问的几率也更高。即当系统的缓存过大,达到删除缓存的阀值时,最先删除最久没有被使用过的数据

    LRU的底层是由哈希算法+链表组成的。哈希算法具有查找高效的优点,却没法保存添加顺序,因此采用链表记录数据的使用顺序。说白了就是数据被存为两份,哈希和链表各一份。在没有达到缓存上限时候,它两相安无事,当达到缓存上限时,根据链表中保存的使用顺序进行删除。

    2 图解数据移动顺序

    假设系统缓存的上限为3个节点

    2.1 向缓存中添加数据

    根据添加的顺序,越后添加的数据在链表中越靠后
    请添加图片描述

    2.2 使用数据

    因为数据2最近被使用,因此会被移动到链表的末尾
    请添加图片描述

    2.3 继续添加数据(未超上限)

    添加数据1,因为1存在缓存中,因此只需将1移动到链表的末尾
    请添加图片描述

    2.4 继续添加数据(超过上限)

    添加数据4,因为4不存在缓存中,因此会触发缓存的淘汰,根据LRU规则,淘汰最久未使用的数据3.
    请添加图片描述

    3 代码实现

    3.1 源码

    /**
     * @ClassName LRUCache
     * @Description 线程不安全版LRU(哈希+双向链表)
     * @Author laiqj
     * @Date 2021-07-25
     */
    public class LRUCache<K, v>  {
        /**
         * 链表头节点
         */
         private Node first;
    
        /**
         * 链表尾节点
         */
         private Node last;
    
        /**
         * 缓存存储上限
         */
        private int limit;
    
        /**
         * 存储数据集合
         */
        private Map<K, Node> map;
    
        /**
         * 构造函数
         * @param limit
         */
        public LRUCache(int limit) {
            this.limit = limit;
            this.map = new HashMap<>(limit);
        }
    
    
        /**
         * 添加
         * @param key
         * @param value
         * @return
         */
        public K put(K key, v value){
            //不支持插入空key或者空的value
            if(key == null || value == null){
                //抛出异常
                new NullPointerException("key或value为空!");
            }
    
            Node<K, v> node = map.get(key);
            //节点不存在
            if(node == null){
                //实际容量不小于设置的阀值
                //删除首节点
                if(map.size() >= limit){
                    node = removeFirst();
                    map.remove(node.key);
                }
            } else{//节点存在
                node = removeNode(node);
                map.remove(node.key);
            }
    
            //插入末节点
            Node<K, v> newNode = addLast(key, value);
            map.put(key, newNode);
    
            return newNode.key;
        }
    
    
    
        /**
         * 查询
         * @param key
         * @return
         */
        public v get(K key){
            Node<K, v> node = map.get(key);
    
            if(node == null){
                return null;
            }
    
            if(first == node) {
                removeFirst();
                addLast(node.key, node.value);
            }else if(last != node){
                //从链表中删除当前节点
                removeNode(node);
                addLast(node.key, node.value);
            }
    
            return node.value;
        }
    
        /**
         * 删除
         * @param key
         * @return
         */
        public v del(K key){
            Node<K, v> node = map.remove(key);
            if(node == null){
                return null;
            }
    
            //从链表中删除当前节点
            if(first == node) {
                removeFirst();
            }else if(last != node){
                removeNode(node);
            }else{
                Node<K, v> prevNode = node.prev;
                prevNode.next = null;
                node.prev = null;
                last = prevNode;
            }
    
            return node.value;
        }
    
        /**
         * 插入末尾节点
         */
        private Node<K, v> addLast(K key, v value){
            //插入末节点
            Node<K, v> newNode = new Node(key, value);
    
            if(first == null){
                first = newNode;
            }else{
                last.next = newNode;
                newNode.prev = last;
            }
    
            last = newNode;
            return newNode;
        }
    
        /**
         * 删除首节点
         */
        private Node<K, v> removeFirst() {
            Node<K, v> tmpNode = first;
            if(tmpNode.next == null){
                last = null;
                first = null;
            }else{
                first.next.prev = null;
                first = first.next;
                tmpNode.next = null;
            }
    
            return tmpNode;
        }
    
        /**
         * 删除尾节点
         */
        private Node<K, v> removeLast() {
            Node<K, v> tmpNode = last;
            if(tmpNode.prev == null){
                first = null;
                last = null;
            }else{
                last.prev.next = null;
                last = last.prev;
                tmpNode.prev = null;
            }
            return tmpNode;
        }
    
        /**
         * 从链表中删除节点
         * @param node
         */
        private Node<K, v> removeNode(Node<K, v> node){
            if(node == first){
                removeFirst();
            }else if(node == last){
                removeLast();
            }else{
                //从链表中删除当前节点
                node.prev.next = node.next;
                node.next.prev = node.prev;
                node.next = null;
                node.prev = null;
            }
            return  node;
        }
    
    
        /**
         * 从头到位获取全部键值对数据
         * @return
         */
        public String displayForward(){
            Node<K, v> node = first;
            StringBuilder sb = new StringBuilder("[");
            while (node != null){
                sb.append("{key:" + node.key + ",value:" + node.value + "},");
                node = node.next;
            }
    
            if(sb.length() == 1){
                return "";
            }
    
            sb.delete(sb.length() - 1, sb.length());
            sb.append("]");
            return sb.toString();
        }
    
    
        /**
         * 从尾到头获取全部键值对数据
         * @return
         */
        public String displayBaclward(){
            Node<K, v> node = last;
            StringBuilder sb = new StringBuilder("[");
            while (node != null){
                sb.append("{key:" + node.key + ",value:" + node.value + "},");
                node = node.prev;
            }
    
            if(sb.length() == 1){
                return "";
            }
    
            sb.delete(sb.length() - 1, sb.length());
            sb.append("]");
            return sb.toString();
        }
    
    
    
        /**
         * 节点类
         * @param <K> 
         * @param <v>
         */
         private class Node<K, v> {
            private Node prev;
            private Node next;
            private K key;
            private v value;
    
            Node(K key, v value){
              this.key = key;
              this.value = value;
            }
        }
    
    
        /**
         * 测试
         * @param args
         */
        public static void main(String[] args) {
            LRUCache lruCache = new LRUCache(10);
            for (int i = 0; i < 5; i++){
                lruCache.put(Integer.toString(i), Integer.toString(i));
            }
    
            System.out.println("打印全部元素....");
            System.out.println(lruCache.displayForward());
            System.out.println(lruCache.displayBaclward());
    
            System.out.println("添加(0, 0)....");
            lruCache.put("0", "0");
            System.out.println(lruCache.displayForward());
            System.out.println(lruCache.displayBaclward());
    
            System.out.println("删除key为3的数据....");
            lruCache.del("3");
            System.out.println(lruCache.displayForward());
            System.out.println(lruCache.displayBaclward());
    
            System.out.println("添加(11, 11)....");
            lruCache.put("11", "11");
            System.out.println(lruCache.displayForward());
            System.out.println(lruCache.displayBaclward());
    
            System.out.println("删除key为11的数据....");
            lruCache.del("11");
            System.out.println(lruCache.displayForward());
            System.out.println(lruCache.displayBaclward());
    
            System.out.println("查询key为11的数据....");
            lruCache.get("11");
            System.out.println(lruCache.displayForward());
            System.out.println(lruCache.displayBaclward());
    
        }
    
    }
    

    3.2 测试结果

    打印全部元素....
    [{key:0,value:0},{key:1,value:1},{key:2,value:2},{key:3,value:3},{key:4,value:4}]
    [{key:4,value:4},{key:3,value:3},{key:2,value:2},{key:1,value:1},{key:0,value:0}]
    添加(0, 0)....
    [{key:1,value:1},{key:2,value:2},{key:3,value:3},{key:4,value:4},{key:0,value:0}]
    [{key:0,value:0},{key:4,value:4},{key:3,value:3},{key:2,value:2},{key:1,value:1}]
    删除key为3的数据....
    [{key:1,value:1},{key:2,value:2},{key:4,value:4},{key:0,value:0}]
    [{key:0,value:0},{key:4,value:4},{key:2,value:2},{key:1,value:1}]
    添加(11, 11)....
    [{key:1,value:1},{key:2,value:2},{key:4,value:4},{key:0,value:0},{key:11,value:11}]
    [{key:11,value:11},{key:0,value:0},{key:4,value:4},{key:2,value:2},{key:1,value:1}]
    删除key为11的数据....
    [{key:1,value:1},{key:2,value:2},{key:4,value:4},{key:0,value:0}]
    [{key:0,value:0},{key:4,value:4},{key:2,value:2},{key:1,value:1}]
    查询key为11的数据....
    [{key:1,value:1},{key:2,value:2},{key:4,value:4},{key:0,value:0}]
    [{key:0,value:0},{key:4,value:4},{key:2,value:2},{key:1,value:1}]
    

    4 参考文献

    [1]程杰. 大话数据结构[M]. 清华大学出版社,2011
    [2] Robert, Lafore., Java数据结构和算法.第2版 版. 2004: 中国电力出版社.[2] Sedgewick Robert与Wayne Kevin, 算法. 2012: 人民邮电出版社.

    展开全文
  • LRU缓存淘汰算法,也就是最近最少使用缓存淘汰策略实现的算法。这里我们使用散列表和链表实现提高操作的时间复杂中度,达到O(1)
  • LRU缓存淘汰算法

    2021-03-31 21:51:10
    LRU缓存淘汰算法 通过链表实现 因为缓存大小有限,当缓存空间不够,需要淘汰一个数据的时候,我们就直接将链表头部的结点删除。当要缓存某个数据的时候,先在链表中查找这个数据。如果没有找到,则直接将数据放到...
  • LRU算法–缓存淘汰算法

    千次阅读 2017-06-03 10:43:50
    LRU算法–缓存淘汰算法目录LRU算法缓存淘汰算法目录 什么是 LRU LRU 算法思想 为什么需要页面置换算法 分页原理图 假设使用 FIFO 先进先出 实现页面置换算法 LRU 算法原理图 使用双向链表实现LRU算法 Clock算法是...
  • 缓存是一个计算机思维,...一、缓存淘汰算法定义 缓存算法是指令的一个明细表,用于决定缓存系统中哪些数据应该被删去。 常见类型包括LFU、LRU、ARC、FIFO、MRU。 二、最不经常使用算法(LFU-Least Frequently Us...
  • 缓存淘汰算法——LRU算法LRU原理LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。实现最常见的实现是使用...
  • 注:以下的缓存淘汰算法实现,不考虑 并发 和 GC(垃圾回收) 问题 本文讨论的是 进程内缓存,是存放在内存中的,因此容量有限。当缓存容量达到某个阈值时,应该删除一条或多条数据。至于移出哪些数据?答案是移出那些...
  • 注:以下的缓存淘汰算法实现,不考虑 并发 和 GC(垃圾回收) 问题 本文讨论的是 进程内缓存,是存放在内存中的,因此容量有限。当缓存容量达到某个阈值时,应该删除一条或多条数据。至于移出哪些数据?答案是移出那些...
  • 缓存淘汰算法-LRU算法

    2018-04-27 09:32:17
    转载自:缓存淘汰算法-LRU算法 &nbsp; 1.&nbsp;LRU1.1.&nbsp;原理 LRU(Least&nbsp;recently&nbsp;used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据...
  • 基于多级队列缓存淘汰算法的处理器全数字仿真优化.pdf
  • 06|链表上如何实现LRU缓存淘汰算法? 06|链表上如何实现LRU缓存淘汰算法? Linked list LRU 今天我们来聊聊 链表 这个数据结构学习链表有什么用呢为了回答这个问题我们先来讨论一个经典的链表应用场景那就是 缓存淘汰...
  • 页面置换算法,或者称缓存淘汰算法,共有三种,即 FIFO,LRU, LFU。 1. FIFO(First In First Out) 这是最简单的,即先进先出算法,其实现方式已经很明显了,就是队列。在操作系统中, 作业调度就需要利用这一种...
  • 一、基本概念 命中:访问缓存是通过key get到对应value 回源: miss了,未命中导致回读源数据 淘汰:缓存满了,那么就会按照某一种策略,把缓存中的旧对象踢出,而把新的对象加入...二、缓存淘汰算法LRU LRU(Le...
  • 链表(上):如何实现LRU缓存淘汰算法
  • golang--算法--LRU 缓存淘汰算法

    千次阅读 2020-02-29 09:54:03
    说到链表一个经典的应用场景,那就是 LRU 缓存淘汰算法。 缓存是一种提高数据读取性能的技术,在硬件设计、软件开发中都有着非常广泛的应用,比如常见的 CPU 缓存、数据库缓存、浏览器缓存等等。缓存的大小有限,当...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 31,346
精华内容 12,538
关键字:

缓存淘汰算法