關於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 }
