guava是google的一個開源java框架,其github地址是 https://github.com/google/guava。guava工程包含了若干被Google的 Java項目普遍依賴的核心庫,例如:集合 [collections] 、緩存 [caching] 、原生類型支持 [primitives support] 、併發庫 [concurrency libraries] 、通用註解 [common annotations] 、字符串處理 [string processing] 、I/O 等等。 全部這些工具天天都在被Google的工程師應用在產品服務中。 其中caching這一塊是我經常使用的模塊的之一,今天就來分享一下我對guava cache的一些看法。html
guava cache使用簡介java
guava cache 是利用CacheBuilder類用builder模式構造出兩種不一樣的cache加載方式CacheLoader,Callable,共同邏輯都是根據key是加載value。不一樣的地方在於CacheLoader的定義比較寬泛,是針對整個cache定義的,能夠認爲是統一的根據key值load value的方法,而Callable的方式較爲靈活,容許你在get的時候指定load方法。看如下代碼git
Cache<String,Object> cache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(500).build(); cache.get("key", new Callable<Object>() { //Callable 加載 @Override public Object call() throws Exception { return "value"; } }); LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder() .expireAfterAccess(30, TimeUnit.SECONDS).maximumSize(5) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return "value"; } });
這裏面有幾個參數expireAfterWrite、expireAfterAccess、maximumSize其實這幾個定義的都是過時策略。expireAfterWrite適用於一段時間cache可能會發先變化場景。expireAfterAccess是包括expireAfterWrite在內的,由於read和write操做都被定義的access操做。另外expireAfterAccess,expireAfterAccess都是受到maximumSize的限制。當緩存的數量超過了maximumSize時,guava cache會要據LRU算法淘汰掉最近沒有寫入或訪問的數據。這裏的maximumSize指的是緩存的個數並非緩存佔據內存的大小。 若是想限制緩存佔據內存的大小能夠配置maximumWeight參數。github
看代碼:redis
CacheBuilder.newBuilder().weigher(new Weigher<String, Object>() { @Override public int weigh(String key, Object value) { return 0; //the value.size() } }).expireAfterWrite(10, TimeUnit.SECONDS).maximumWeight(500).build();
weigher返回每一個cache value佔據內存的大小,這個大小是由使用者自身定義的,而且put進內存時就已經肯定後面就再不會發生變更。maximumWeight定義了全部cache value加起的weigher的總和不能超過的上限。算法
注意一點就是maximumWeight與maximumSize二者只能生效一個是不能同時使用的!api
guava cache的設計數組
guava cache做爲一個被普遍使用的緩存組件,設計上它有哪些過人之處?緩存
先看下cache的類實現定義安全
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {....}
咱們看到了ConcurrentMap,因此咱們知道了一點guava cache基於ConcurrentHashMap的基礎上設計。因此ConcurrentHashMap的優勢它也具有。既然實現了 ConcurrentMap那再看下guava cache中的Segment的實現是怎樣?
咱們看到guava cache 中的Segment本質是一個ReentrantLock。內部定義了table,wirteQueue,accessQueue定義屬性。其中table是一個ReferenceEntry原子類數組,裏面就存放了cache的內容。wirteQueue存放的是對table的寫記錄,accessQueue是訪問記錄。guava cache的expireAfterWrite,expireAfterAccess就是藉助這個兩個queue來實現的。
瞭解了guava cache的大概存儲結構,下面看經過對cache的操做來進行更深刻的瞭解。
put(key,val)操做。
public V put(K key, V value) { checkNotNull(key); checkNotNull(value); int hash = hash(key); return segmentFor(hash).put(key, hash, value, false); }
設置緩存大概的過程:根據key 哈希到對應的segment,而後對segment加鎖lock(),而後獲取segment.table對應的結點
int index = hash & (table.length() - 1); ReferenceEntry<K, V> first = table.get(index);
以後入隊的過程和hashMap的入隊過程相似。入隊完以後還會進行相關操做好比更新accessQueue和wiriteQueue,累加totalWeight
void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) { // we are already under lock, so drain the recency queue immediately drainRecencyQueue(); totalWeight += weight; if (map.recordsAccess()) { entry.setAccessTime(now); } if (map.recordsWrite()) { entry.setWriteTime(now); } accessQueue.add(entry); writeQueue.add(entry); }
get(key)操做 。
第一步也是先定位到所在segment
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException { int hash = hash(checkNotNull(key)); return segmentFor(hash).get(key, hash, loader); }
判斷key對應的ReferenceEntry存在
ReferenceEntry<K, V> e = getEntry(key, hash); if (e != null) { long now = map.ticker.read(); V value = getLiveValue(e, now); if (value != null) { recordRead(e, now); statsCounter.recordHits(1); return scheduleRefresh(e, key, hash, value, now, loader); } ValueReference<K, V> valueReference = e.getValueReference(); if (valueReference.isLoading()) { return waitForLoadingValue(e, key, valueReference); }
getLiveValue(e, now)若是返回了null就表示當前cache已通過期了,不爲null時recordRead(e, now)記錄最新訪問時間爲now,而後統計命中率。scheduleRefresh(e, key, hash, value, now, loader)至關於一個雙重檢查,再次檢查cache需不須要刷新,若是須要刷新看不看不能立刻拿到新值。
若是能夠返回新值,否直接拿原值返回。
這時注意valueReference.isLoading()爲true的時候就表示有其它線程正在更新該cache,其它全部線程都要wait到這個線程loading完
才能返回。
key對應的ReferenceEntry不存在:緩存沒有加載進來或者已經被remove掉。
return lockedGetOrLoad(key, hash, loader);
lockedGetOrLoad執行邏輯是先加鎖lock(),判斷當前是否有其它線程在loading該cache,若是有等待其加載完畢而後返回。否本身執行loader把值設進cache中而後返回。
try { // Synchronizes on the entry to allow failing fast when a recursive load is // detected. This may be circumvented when an entry is copied, but will fail fast most // of the time. synchronized (e) { return loadSync(key, hash, loadingValueReference, loader); } } finally { statsCounter.recordMisses(1); }
guava cache的淘汰策略
guava cache整體來講有四種淘汰策略。
一、size-based 基本於使用量。
當緩存個數超過CacheBuilder.maximumSize(long)設置的值時,優先淘汰最近沒有使用或者不經常使用的元素。同理
CacheBuilder.maximumWeight(long)也是同樣邏輯。
二、timed eviction 基於時間驅逐。
expireAfterAccess(long, TimeUnit)
僅在指定上一次讀/更新操做過了指定持續時間以後才考慮淘汰,淘汰邏輯與size-based是相似的。優先淘汰最近沒有使用或者不經常使用的元素
expireAfterWrite(long, TimeUnit) 僅在指定上一次寫/更新操做過了指定持續時間以後才考慮淘汰,淘汰邏輯與size-based是相似的。
優先淘汰最近沒有使用或者不經常使用的元素
三、Reference-based Eviction 基本於引用驅逐
在JDK1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Refernce)、虛引用(Phantom Reference)。四種引用強度依次減弱。這四種引用除了強引用(Strong Reference)以外,其它的引用所對應的對象來JVM進行GC時都是能夠確保被回收的。因此經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache能夠把緩存設置爲容許垃圾回收:
這樣的好處就是當內存資源緊張時能夠釋放掉到緩存的內存。注意!CacheBuilder若是沒有指明默認是強引用的,GC時若是沒有元素到達指定的過時時間,內存是不能被回收的。
四、顯示刪除
任什麼時候候,你均可以顯式地清除緩存項,而不是等到它被回收:
提一下guava cache 是怎麼觸發元素回收的。guava的元素回收與其它的一些框架不同好比redis,redis是起額外的線程去回收元素。而guava是進行get,put操做的時候順便把元素回收的。這樣比通常的緩存另起線程監控清理相比,能夠減小開銷,但若是長時間沒有調用方法的話,會致使不能及時的清理釋放內存空間的問題。回收時主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是由於WeakReference、SoftReference被垃圾回收時加入的,清理時只須要遍歷整個queue,將對應的項從LocalCache中移除便可,這裏keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference。而對後面兩個Queue,只須要檢查是否配置了相應的expire時間,而後從頭開始查找已經expire的Entry,將它們移除便可。
總的來講,guava cache基於ConcurrentHashMap的優秀設計借鑑,在高併發場景支持線程安全,使用Reference引用命令,保證了GC的可回收到相應的數據,有效節省空間;同時write鏈和access鏈的設計,能更靈活、高效的實現多種類型的緩存清理策略,包括基於容量的清理、基於時間的清理、基於引用的清理等;