更多精彩文章。java
《Linux生產環境上,最經常使用的一套「vim「技巧》後端
最有用系列:bash
《Linux生產環境上,最經常使用的一套「vim「技巧》數據結構
《Linux生產環境上,最經常使用的一套「Sed「技巧》併發
《Linux生產環境上,最經常使用的一套「AWK「技巧》微服務
歡迎Linux和java後端的同窗關注公衆號。
JVM內緩存是緩存體系中重要的一環,最經常使用的有FIFO/LRU/LFU三種算法。
一、FIFO是簡單的隊列,先進先出。
二、LRU是最近最少使用,優先移除最久未使用的數據。是時間維度
。
三、LFU是最近最不經常使用,優先移除訪問次數最少的數據。是統計維度
。
因爲過時也是緩存的一個重要特色。全部在設計這三種緩存算法時,須要額外的存儲空間去存儲這個過時時間。
如下將討論這三種緩存算法的操做和設計要點,暫未考慮高併發環境。
先進先出,若是緩存容量滿,則優先移出最先加入緩存的數據;其內部可使用隊列實現。
1)Object get(key):獲取保存的數據,若是數據不存在或者已通過期,則返回null。
2)void put(key,value,expireTime):加入緩存,不管此key是否已存在,均做爲新key處理(移除舊key);若是空間不足,則移除已過時的key,若是沒有,則移除最先加入緩存的key。過時時間未指定,則表示永不自動過時。
3)此題須要注意,咱們容許key是有過時時間的,這一點與普通的FIFO有所區別,因此在設計此題時須要注意。(也是面試考察點,此題偏設計而非算法)
普通的FIFO或許你們都能很簡單的寫出,此處增長了過時時間的特性,因此在設計時須要多考慮。以下示例,爲FIFO的簡易設計,還沒有考慮併發環境場景。
1)用普通的hashMap保存緩存數據。
2)咱們須要額外的map用來保存key的過時特性,例子中使用了TreeMap,將「剩餘存活時間」做爲key,利用treemap的排序特性。
public class FIFOCache {
//按照訪問時間排序,保存全部key-value
private final Map<String,Value> CACHE = new LinkedHashMap<>();
//過時數據,只保存有過時時間的key
//暫不考慮併發,咱們認爲同一個時間內沒有重複的key,若是改造的話,能夠將value換成set
private final TreeMap<Long, String> EXPIRED = new TreeMap<>();
private final int capacity;
public FIFOCache(int capacity) {
this.capacity = capacity;
}
public Object get(String key) {
//
Value value = CACHE.get(key);
if (value == null) {
return null;
}
//若是不包含過時時間
long expired = value.expired;
long now = System.nanoTime();
//已過時
if (expired > 0 && expired <= now) {
CACHE.remove(key);
EXPIRED.remove(expired);
return null;
}
return value.value;
}
public void put(String key,Object value) {
put(key,value,-1);
}
public void put(String key,Object value,int seconds) {
//若是容量不足,移除過時數據
if (capacity < CACHE.size()) {
long now = System.nanoTime();
//有過時的,所有移除
Iterator<Long> iterator = EXPIRED.keySet().iterator();
while (iterator.hasNext()) {
long _key = iterator.next();
//若是已過時,或者容量仍然溢出,則刪除
if (_key > now) {
break;
}
//一次移除全部過時key
String _value = EXPIRED.get(_key);
CACHE.remove(_value);
iterator.remove();
}
}
//若是仍然容量不足,則移除最先訪問的數據
if (capacity < CACHE.size()) {
Iterator<String> iterator = CACHE.keySet().iterator();
while (iterator.hasNext() && capacity < CACHE.size()) {
String _key = iterator.next();
Value _value = CACHE.get(_key);
long expired = _value.expired;
if (expired > 0) {
EXPIRED.remove(expired);
}
iterator.remove();
}
}
//若是此key已存在,移除舊數據
Value current = CACHE.remove(key);
if (current != null && current.expired > 0) {
EXPIRED.remove(current.expired);
}
//若是指定了過時時間
if(seconds > 0) {
long expireTime = expiredTime(seconds);
EXPIRED.put(expireTime,key);
CACHE.put(key,new Value(expireTime,value));
} else {
CACHE.put(key,new Value(-1,value));
}
}
private long expiredTime(int expired) {
return System.nanoTime() + TimeUnit.SECONDS.toNanos(expired);
}
public void remove(String key) {
Value value = CACHE.remove(key);
if(value == null) {
return;
}
long expired = value.expired;
if (expired > 0) {
EXPIRED.remove(expired);
}
}
class Value {
long expired; //過時時間,納秒
Object value;
Value(long expired,Object value) {
this.expired = expired;
this.value = value;
}
}
}
複製代碼
least recently used,最近最少使用,是目前最經常使用的緩存算法和設計方案之一,其移除策略爲「當緩存(頁)滿時,優先移除最近最久未使用的數據」,優勢是易於設計和使用,適用場景普遍。算法能夠參考leetcode 146 (LRU Cache)。
1)Object get(key):從canche中獲取key對應的數據,若是此key已過時,移除此key,並則返回null。
2)void put(key,value,expired):設置k-v,若是容量不足,則根據LRU置換算法移除「最久未被使用的key」,須要注意,根據LRU優先移除已過時的keys,若是沒有,則根據LRU移除未過時的key。若是未設定過時時間,則認爲永不自動過時。
3)此題,設計關鍵是過時時間特性,這與常規的LRU有所不一樣。畢竟「過時時間」特性在cache設計中是必要的。
1)LRU的基礎算法,須要瞭解;每次put、get時須要更新key對應的訪問時間,咱們須要一個數據結構可以保存key最近的訪問時間且可以排序。
2)既然包含過時時間特性,那麼帶有過時時間的key須要額外的數據結構保存。
3)暫時不考慮併發操做;儘可能兼顧空間複雜度和時間複雜度。
4)此題仍然偏向於設計題,而非純粹的算法題。
此題代碼與FIFO基本相同,惟一不一樣點爲get()方法,對於LRU而言,get方法須要重設訪問時間(即調整所在cache中順序)
public Object get(String key) {
//
Value value = CACHE.get(key);
if (value == null) {
return null;
}
//若是不包含過時時間
long expired = value.expired;
long now = System.nanoTime();
//已過時
if (expired > 0 && expired <= now) {
CACHE.remove(key);
EXPIRED.remove(expired);
return null;
}
//相對於FIFO,增長順序重置
CACHE.remove(key);
CACHE.put(key,value);
return value.value;
}
複製代碼
最近最不經常使用,當緩存容量滿時,移除訪問次數最少的元素,若是訪問次數相同的元素有多個,則移除最久訪問的那個。設計要求參見leetcode 460( LFU Cache)
public class LFUCache {
//主要容器,用於保存k-v
private Map<String, Object> keyToValue = new HashMap<>();
//記錄每一個k被訪問的次數
private Map<String, Integer> keyToCount = new HashMap<>();
//訪問相同次數的key列表,按照訪問次數排序,value爲相同訪問次數到key列表。
private TreeMap<Integer, LinkedHashSet<String>> countToLRUKeys = new TreeMap<>();
private int capacity;
public LFUCache(int capacity) {
this.capacity = capacity;
//初始化,默認訪問1次,主要是解決下文
}
public Object get(String key) {
if (!keyToValue.containsKey(key)) {
return null;
}
touch(key);
return keyToValue.get(key);
}
/**
* 若是一個key被訪問,應該將其訪問次數調整。
* @param key
*/
private void touch(String key) {
int count = keyToCount.get(key);
keyToCount.put(key, count + 1);//訪問次數增長
//從原有訪問次數統計列表中移除
countToLRUKeys.get(count).remove(key);
//若是符合最少調用次數到key統計列表爲空,則移除此調用次數到統計
if (countToLRUKeys.get(count).size() == 0) {
countToLRUKeys.remove(count);
}
//而後將此key的統計信息加入到管理列表中
LinkedHashSet<String> countKeys = countToLRUKeys.get(count + 1);
if (countKeys == null) {
countKeys = new LinkedHashSet<>();
countToLRUKeys.put(count + 1,countKeys);
}
countKeys.add(key);
}
public void put(String key, Object value) {
if (capacity <= 0) {
return;
}
if (keyToValue.containsKey(key)) {
keyToValue.put(key, value);
touch(key);
return;
}
//容量超額以後,移除訪問次數最少的元素
if (keyToValue.size() >= capacity) {
Map.Entry<Integer,LinkedHashSet<String>> entry = countToLRUKeys.firstEntry();
Iterator<String> it = entry.getValue().iterator();
String evictKey = it.next();
it.remove();
if (!it.hasNext()) {
countToLRUKeys.remove(entry.getKey());
}
keyToCount.remove(evictKey);
keyToValue.remove(evictKey);
}
keyToValue.put(key, value);
keyToCount.put(key, 1);
LinkedHashSet<String> keys = countToLRUKeys.get(1);
if (keys == null) {
keys = new LinkedHashSet<>();
countToLRUKeys.put(1,keys);
}
keys.add(key);
}
}
複製代碼
更加易用的cache,能夠參考guava的實現。但願這三個代碼模版,可以對你有所幫助。