操作系统:基于页面置换算法的缓存原理详解(下)
概述:
? 在上一篇《操作系統:基于頁面置換算法的緩存原理詳解(上)》中,我們主要闡述了FIFO、LRU和Clock頁面置換算法。接著上一篇說到的,本文也有三個核心算法要講解。分別是LFU(Least Frequently Used)、LRU-K、MQ(Multi Queue)算法。
本文鏈接:http://blog.csdn.net/lemon_tree12138/article/details/50475240 --Coding-Naga
? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?--轉載請注明出處
1.LFU
? 我們從LFU的英文全稱Least Frequently Used中就可以看到,此算法是基于資源被訪問的次數來實現的。由于算法很簡單,我們就直接給出思路,而邏輯代碼就不再展示了(因為下面的LRU-K和MQ才是本文的關鍵)。LFU的原理圖可參見圖-1.
圖-1 LFU置換算法原理圖
算法步驟:
(1)當有新資源被訪問時,就把這個資源添加到緩存隊列的尾部;
(2)當訪問一個已經存在的資源時,就把這個資源被訪問的次數+1,再上移至合適的位置;
(3)在被訪問次數相同的資源集合中,是按照訪問時間來排序的;
(4)當新資源加入時,檢測到此時隊列已滿,那么就把隊列尾部的資源換出,將新資源添加到隊列的尾部。
算法評價:
? 個人認為此算法并不是一個很好緩存算法,因為它不能很好地反映“用戶”在一個比較短的時間里訪問資源的走向。
2.LRU-K
LRU算法中存在的問題:
? LRU-K算法從名字就可以知道這一定是基于LRU算法的,LRU-K算法是在LRU算法上的一次改進。現在我們分兩種情況分別來看看LRU算法的效果。
? 第一種,假設存在一定量連續的訪問,比如說我先訪問A資源10次,再訪問B資源10次,再訪問A資源10次,再訪問C資源10次,等等等等。這樣我可以大致補上一個畫面就是緩存隊列是在一個比較小的范圍里來回替換,這樣就減少換入換出的次數,提高系統的性能,這是第一種情況。
? 第二種情況就是我們依次訪問資源A、B、C、...、Y、Z,再重復n次。并且我們的緩存列隊長度比這個循環周期要小。這樣,我們的資源就需要不停地換入換出,增加IO操作,效率自然就下來了。這種情況導致的效果也被稱著是緩存污染。
LRU-K的優勢:
? 針對上面第二種情況產生的緩存污染,我們做了一個相應地調整——加入了一個新的歷史記錄的隊列。前面LRU算法中,我們是只要訪問了某一個資源,那么就把這個資源加入緩存,這樣的結果是資源浪費,畢竟緩存隊列的資源有限。而在LRU-K算法中,我們不再把只訪問一次的資源放入緩存,而是當資源被訪問了K次之后,才把這個資源加入到緩存隊列中去,并且從歷史隊列中刪除。當資源放入緩存中之后,我們就不用再考慮它的訪問次數了。在緩存隊列中,我們是以LRU算法來進行更新和淘汰的(對于歷史隊列可以使用FIFO也可以使用LRU)。
? 你是不是要問,既然這里加了一個新的歷史隊列還是要使用LRU算法,那么優勢在哪里?而且加入了一個新的隊列,也是開銷呀,怎么能說還解決了LRU的問題呢?事實上一開始我也有這樣的念頭,后來一想,我們的歷史記錄隊列只是一個記錄數組,我們可以讓它的開銷很小。這里要怎么做呢?能想到么?
? 我們知道我們這里要說的資源,可能是一個進程,可能是一個什么其他比較大的對象。那么,如果直接在歷史隊列中保存這些對象或是進程,并不是一件很劃算的事情,不是嗎?所以,我們的突破點就是這個歷史記錄隊列能不能盡可能地小?是可以的。我們可以對對象進行Hash成一個整數,這樣就可以不用保存原來的對象了,而后面的次數可以使用byte來保存(因為我們可以默認某一個資源在一定時間內,訪問的次數不會大得離譜,當然可以使用int或是long,沒問題)。如此一來,歷史列隊就可以做得很小了。如果你對于Hash這一點還不清楚,那么你需要做的是補補你的基礎知識了。
? LRU-K算法原理圖解請下面的圖-1.
圖-2 LRU-K置換算法原理圖
代碼實現:
? 在代碼實現的過程,我們有一個理想地假設,那就是我們將添加新資源與訪問老資源完全獨立開來。添加(offer)資源時默認在兩個隊列中都不包含,訪問的資源總是存在于某一個隊列當中。
添加新元素(offer):
public void offer(Object object) {if (histories == null) {throw new NullPointerException();}if (histories.size() == maxHistoryLength) {cleanHistory();}LRUHistory history = new LRUHistory();history.setHash(object.hashCode());history.setTimes(1);histories.add(history);}
訪問一存在的資源(visitting):
public void visitting(Object object) {if (histories == null) {throw new NullPointerException();}if (caches == null) {throw new NullPointerException();}int hashCode = object.hashCode();if (inHistory(hashCode)) {boolean offerCache = modifyHistory(hashCode);if (!offerCache) {return;}offerToCache(object);} else if (inCache(object)) {displace(object);} else {throw new NullPointerException("對象不存在");}}修改歷史記錄:
boolean offerCache = modifyHistory(hashCode); if (!offerCache) {return; } offerToCache(object); ? 上面代碼的邏輯描述是當我去修改歷史記錄隊列時,發現某一資源可以加入到緩存中去了,就把這個資源添加到緩存中去。訪問緩存隊列某元素:
? 因為LRU-K中的緩存隊列就是一個完完全全的LRU,所以LRU-K中緩存隊列的訪問與LRU中訪問方式一致,如下:
private void displace(Object object) {for (Object item : caches) {if (item.equals(object)) {caches.remove(item);break;}}caches.add(object);}
3.Multi Queue(MQ)
解決的問題:
? 在上面的LRU-K算法中,只要被訪問資源的訪問次數達到一定數量時,就將這個資源添加到緩存中去。當然,這種做法是合理的,不過,這里我們對這種思路進行了一個擴展。我們按資源被訪問的次數對資源進行分級緩存。針對上面的LRU-K算法,我們假設當資源訪問次數超過3次時,就加入緩存隊列。此時有兩個資源A和B,A已經被訪問了20次,B已經被訪問了4次。這時如果再訪問一次B(假設不考慮后續的訪問操作),那么B的被淘汰的可能性比A被淘汰的可能要小。可是從整體上來看這種情況,我們知道理論上是A應該比B具備更大的優先級。
? 上面說到了LRU-K的“緩存污染”(這里并不否認URL-K算法對URL算法的“緩存污染”改善貢獻),所以我們就想了一個辦法來解決。正是這里要講解的MQ(Multi Queue)算法。下面請看MQ置換算法的原理圖:
圖-3 MQ置換算法原理圖
算法步驟:
(1)我們需要一個歷史記錄隊列和一個緩存隊列的數組。這個數組中保存的就是真正的緩存隊列,每個緩存隊列和歷史隊列均按LRU算法進行“淘汰”。而緩存隊列與緩存隊列之間是按訪問次數進行分級;
(2)當我們需要訪問一個全新的資源時,就把這個資源加入到最低等級的Q0中,如果有需要淘汰的資源,就把這個淘汰的資源加入到歷史隊列中;
(3)在某一個緩存隊列中資源再次被訪問時,就把這個資源加入到隊列的頭部。如果當前資源被訪問的次數達到一定的次數,就把當前資源從當前隊列中刪除,并加入到更高級的緩存隊列的頭部;
(4)為了從一定程序上防止(并沒有絕對防止)高級別的緩存資源被刪除,當一定時間之內,某一資源還未訪問,將此資源等級下滑到下一等級;
(5)從上面緩存隊列數組中的緩存資源如果被“淘汰”,是被加入到了歷史隊列。加入歷史隊列的資源,如果被再次訪問可以重新計算資源被訪問次數,并添加相應的緩存隊列中去;而如果是歷史隊列中的資源被淘汰了,則是真正意義上的被淘汰。
邏輯實現:
從上面的算法步驟中,我們來編寫代碼,并展示關鍵性的代碼。如下:
添加新資源:
public void offer(Object object) {if (object == null) {throw new NullPointerException();}CacheBean cacheBean = new CacheBean(object);cacheBean.setTimes(1);cacheBean.setLastVisitTime(System.currentTimeMillis());CacheQueue firstQueue = cacheQueueList.get(0);CacheBean pollObject = firstQueue.offer(cacheBean);if (pollObject == null) {return;}historyQueue.offer(pollObject);}訪問某一資源:
public void visitting(Object object) {if (object == null) {throw new NullPointerException();}CacheBean cacheBean = new CacheBean(object);// 先在緩存隊列里找CacheBean tmpBean = null;int currentLevel = 0;boolean needUp = false;for (CacheQueue cacheQueue : cacheQueueList) {if (cacheQueue.contains(cacheBean)) {tmpBean = cacheQueue.get(cacheBean);if (tmpBean.getTimes() < timesDistance * (currentLevel + 1)) {tmpBean.setTimes(tmpBean.getTimes() + 1);cacheQueue.visiting(tmpBean);return;} else {tmpBean.setTimes(tmpBean.getTimes() + 1);cacheQueue.remove(tmpBean);needUp = true;}break;}currentLevel++;}// 是否需要升級if (needUp) {int times = tmpBean.getTimes();times = times > cacheQueueList.size() * timesDistance ? cacheQueueList.size() * timesDistance : times;CacheQueue queue = cacheQueueList.get(times / timesDistance);queue.offer(tmpBean);return;}// 如果數據在history中被重新訪問,則重新計算其優先級,移到目標隊列的頭部if (historyQueue.contains(cacheBean)) {CacheBean reVisitBean = historyQueue.revisiting(cacheBean);reVisitBean.setTimes(reVisitBean.getTimes() + 1);System.out.println(reVisitBean);int times = reVisitBean.getTimes();times = times > cacheQueueList.size() * timesDistance ? cacheQueueList.size() * timesDistance : times;CacheQueue queue = cacheQueueList.get(times / timesDistance);CacheBean cb = queue.offer(reVisitBean);if (cb != null) {historyQueue.offer(cb);}return;}}Ref:
http://flychao88.iteye.com/blog/1977653
http://www.cs.cmu.edu/~christos/courses/721-resources/p297-o_neil.pdf
http://flychao88.iteye.com/blog/1977642
GitHub源碼下載:
https://github.com/William-Hai/LRU-Cache
https://github.com/William-Hai/MultiQueue-Cache
總結
以上是生活随笔為你收集整理的操作系统:基于页面置换算法的缓存原理详解(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 操作系统:基于页面置换算法的缓存原理详解
- 下一篇: Java:如何正确地使用异常详解