一、緩存置換的要點
緩存在計算機領域運用的非常多,計算機組成、操作系統以及我們的應用,很多地方都使用了緩存或利用了其思想。那麼爲什麼要有緩存以及緩存又有哪些地方是需要注意的?下面我將一一介紹。
爲什麼要有緩存?這個問題在不同的具體應用中答案不同。那麼具體到實際情況中,在計算機組成原理中我們知道,在CPU與主內存之間是存在多級緩存的,這個緩存存在的目的就是提高CPU與主內存之間交換數據的速率。而在我們的應用中,我們經常遇見的需求就是查詢,那麼怎麼樣讓查詢的反應時間更快呢?一個策略就是在中間增加一層緩存,每次查詢數據不必非要去數據庫查詢,可能我們要的數據在緩存中已經存在了,這裏它就提高了我們應用的敏捷性。
有了緩存就會有一個新的問題等待着我們,那就是緩存中的數據怎麼更新。這其實也是一個比較複雜的問題,在具體項目中採取的解決方法也不盡相同。我們在這裏主要討論的是怎麼替換緩存中的數據。
二、FIFO(先進先出算法)
它的含義就是當緩存已滿時,需要替換新的數據時按照先進先出的策略去替換。即把最早放進緩存中的數據給清除掉,給新來的數據騰個位置。思想很簡單,下面看看具體實現。
實現思路:思路應該算比較簡單,直接使用隊列來實現,緩存未滿時緩存數據從隊首入隊,緩存已滿時,隊尾元素刪除,隊首入隊。那麼這個隊列應該怎麼實現呢?由於我們存在頻繁的刪除元素和添加元素的操作,所以我們可以使用鏈表作爲其存儲結構。最終我選擇了雙向鏈表來實現這個算法。雙向鏈表的實現如下:實現起來不難,都有註釋,就不羅嗦了。
//雙向鏈表
public class DoubleLinkedList<K,V> {
private class Node<K,V>{
K key;
V value;
Node next;
Node before;
public Node() {
}
public Node(K key, V value) {
this(key, value, null, null);
}
public Node(K key, V value, Node next, Node before) {
this.key = key;
this.value = value;
this.next = next;
this.before = before;
}
}
private Node head;
private Node tail;
private int size;
public DoubleLinkedList() {
head = new Node();
tail = null;
head.next = tail;
size = 0;
}
//頭部添加節點
public void addFirst(K key, V value) {
if(containsKey(key)) {
throw new IllegalStateException("Key is exist");
}
Node newNode = new Node(key, value);
newNode.next = head.next;
newNode.before = head;
if(head.next != null){
head.next.before = newNode;
}
head.next = newNode;
size ++;
if(size == 1){
tail = newNode;
}
}
//尾部添加節點
public void addLast(K key, V value) {
Node newNode = new Node(key, value);
newNode.before = tail.before;
newNode.before.next = newNode;
tail = newNode;
size ++;
}
//刪除尾部節點
public V removeLast() {
if(tail == null){
throw new IllegalStateException("DoubleLinkedList is empty.");
}
V value = (V) tail.value;
Node node = tail.before;
tail = node;
tail.next = null;
size--;
return value;
}
//刪除指定節點
public V removeNode(K key) {
V value;
if(!containsKey(key)){
throw new IllegalArgumentException("Element doesn't exist.");
}
Node target = findNode(key);
value = (V)target.value;
if(target != null){
Node before = target.before;
before.next = target.next;
target.before = null; //切斷target節點與before節點的聯繫
target.next.before = before;
target.next = null; //切斷target節點與next節點的聯繫
}
return value;
}
//查詢節點的值
public V find(K key) {
Node cur = head.next;
while(cur != null){
if(cur.key.equals(key)){
return (V)cur.value;
}
cur = cur.next;
}
return null;
}
//查詢指定的節點,可能返回null
public Node findNode(K key) {
Node cur = head.next;
while(cur != null){
if(key.equals(cur.key)) {
return cur;
}
cur = cur.next;
}
return null;
}
//修改節點
public void setValue(K key, V value) {
if(containsKey(key)) {
throw new IllegalArgumentException("Key doesn't exist");
}
Node cur = head.next;
while(cur != null) {
if(cur.key.equals(key)){
cur.value = value;
}
cur = cur.next;
}
}
//判斷key是否存在
public boolean containsKey(K key){
Node cur = head.next;
while(cur != null) {
if(cur.key.equals(key)){
return true;
}
cur = cur.next;
}
return false;
}
//獲取雙向鏈表中元素的個數
public Integer getSize(){
return size;
}
}
而FIFO算法實現如下:
public class FIFO<K,V> {
//緩存池
private DoubleLinkedList<K,V> cache;
//緩存池容量
private Integer capacity;
//模擬數據庫
private HashMap<K,V> map;
//初始容量爲10
public FIFO() {
this(10);
}
//自定義緩存池容量
public FIFO(Integer capacity) {
this.cache = new DoubleLinkedList();
this.capacity = capacity;
map = new HashMap<>();
}
//從緩存中查找元素
public V get(K key) {
V value = cache.find(key);
/*
* 緩存池中沒有,假設數據庫或內存中存在
* 更新緩存
*/
if(value == null) {
//緩存已滿,刪除最先進入緩存的元素
if(cache.getSize() == capacity) {
cache.removeLast();
cache.addFirst(key,map.get(key));
}else { //緩存池未滿
cache.addFirst(key, map.get(key));
}
value = map.get(key);
}
return value;
}
}
三、LRU(最近最不經常使用算法)
那麼什麼是LRU算法呢?它的意思其實是每次替換緩存中的元素時,優先替換最近一段時間都沒有使用的元素。它的實現利用雙向鏈表也很簡單了。緩存已滿需要替換元素時,將鏈表尾部元素刪除,從鏈表頭部添加元素,這樣子就代表了最近使用過的元素都在鏈表頭部位置,而鏈表尾部位置的元素都是最近沒有使用過的。下面是LRU的實現:
/**
* 緩存置換策略的LRU算法(Least recently used)
* 即最近最少使用算法,最近一段時間未被使用的緩存,將來被使用的概率也很小,類似於局部性原理
* 將最近(時間)使用過的元素放置在雙向鏈表頭部,代表最近使用過的,鏈表尾部就代表最近最少使用的
* 當有新元素加入緩存時且緩存已滿時將最近最少使用的元素刪除移到鏈表頭部,
* 使用雙向鏈表實現,每次從緩存中查詢元素時,如果緩存中存在,則將元素移到鏈表頭部
* 若緩存中不存在,則將元素存入緩存中
* 此時若緩存已滿,則將鏈表尾部元素刪除,將元素添加到鏈表頭部
* 若沒滿,直接添加到鏈表頭部
*/
public class LRU<K,V> {
private DoubleLinkedList<K,V> cache; //緩存容器
private Integer capacity; //緩存容量
//代表數據庫
private HashMap<K,V> map;
//緩存默認容量爲20
public LRU() {
cache = new DoubleLinkedList<>();
capacity = 20;
}
//自定義緩存容量
public LRU(Integer capacity){
cache = new DoubleLinkedList<>();
this.capacity = capacity;
map = new HashMap<>();
}
//從緩存中查詢元素
public V get(K key){
if(cache.containsKey(key)){ //緩存中已存在
//將元素移動到鏈表頭部
V value = cache.removeNode(key);
cache.addFirst(key,value);
return value;
}
//緩存中不存在
if(cache.getSize() == capacity) { //緩存已滿
cache.removeLast(); //刪除最近最少使用的元素
cache.addFirst(key, map.get(key)); //從內存或數據庫查找存入緩存
}else { //緩存中不存在且緩存未滿
cache.addFirst(key, map.get(key)); //從內存或數據庫查找存入緩存
}
return map.get(key);
}
}