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 }