guava的緩存相信不少人都有用到,緩存
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(100, TimeUnit.SECONDS) .maximumSize(10).build();
也經常使用的方法是設置過時時間。但使用過程當中會遇到一些問題:當過時時間到了,緩存中的對象真的會當即被釋放嗎?當緩存達到容量之後,如何高效的剔除緩存?guava cache的底層數據結構是如何的?帶着這些問題,一塊兒來看看guava cache的源碼數據結構
其實經過和CurrentHashMap最類比比較好理解,只不過guava緩存在其基礎上加強了緩存過時的機制:併發
答案是確定的,當咱們設置緩存用不過時(或者很長),緩存的對象不限個數(或者很大),例如框架
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(100000, TimeUnit.SECONDS) .build();
不斷向guava加入緩存大字符串,最終將能oom,解決這種辦法:ide
Cache<String, String> cache = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS) .weakValues().build();
guava在建立對象放到對應Segement中的時候,默認使用強引用(StrongValueReference.class),若是指定使用弱引用的時候,就會建立的是(WeakValueReference.class),參考guava cache基本框架可能更好理解。高併發
這個也是比較推薦的方法,根據業務需求,設置合適的緩存容量、這樣超過容量之後,緩存就會按照LRU的方式回收緩存。ui
CacheBuilder.maximumSize(10)
guava清楚過時緩存的機制是什麼,是單獨使用線程來掃描嗎?不是的,是在每次進行緩存操做的時候,如get()或者put()的時候,判斷緩存是否過時。核心代碼線程
void expireEntries(long now) { drainRecencyQueue(); //多線併發的狀況下,防止誤刪access ReferenceEntry<K, V> e; while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) { if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) { throw new AssertionError(); } } }
其中 writeQueue是保存按照寫入緩存前後時間的隊列,每次get或者put均可能觸發觸發這個方法。accessQueue同理,對應的是最後訪問失效時間的功能。
所以能夠看出,一個若是一個對象放入緩存之後,不在有任何緩存操做(包括對緩存其餘key的操做),那麼該緩存不會主動過時的。不過這種狀況是極端狀況下才會出現。code
在上面也說到了,是用accessQueue,這個隊列的實現比較複雜。這個隊列實際上是按照最久未使用的順序存放的緩存對象(ReferenceEntry)的。因爲會常常進行元素的移動,例如把訪問過的對象放到隊列的最後。ReferenceEntry這個在前面框架圖裏面說到了,使用來保存key-val的,其中接口包含一些特殊方法:對象
@Override public ReferenceEntry<K, V> getNextInAccessQueue() { throw new UnsupportedOperationException(); } @Override public void setNextInAccessQueue(ReferenceEntry<K, V> next) { throw new UnsupportedOperationException(); } @Override public ReferenceEntry<K, V> getPreviousInAccessQueue() { throw new UnsupportedOperationException(); } @Override public void setPreviousInAccessQueue(ReferenceEntry<K, V> previous) { throw new UnsupportedOperationException(); }
這樣經過ReferenceEntry就能夠判斷該entry的在accessQueue中的先後節點,若是該entry不在隊列中,則返回一個NullEntry的對象。這樣作的好處就彌補了 鏈表的缺點
而且能夠很方便的更新和刪除鏈表中的節點,由於每次訪問的時候均可能須要更新該鏈表,放入到鏈表的尾部,這樣,每次從access中拿出的頭節點就是最久未使用的。 而且,若是按照訪問時間來刪除緩存的時候,只要從隊列裏找出第一個訪問沒有超時的對象,那麼以前遍歷的緩存都是應該刪除的,這樣就不須要遍歷整個緩存的對象來判斷。
對應的writeQueue用來保存最久未更新的緩存隊列,實現方式和accessQueue同樣。
能夠看出,guava緩存的原型是CurrentHashMap,在其基礎上考慮若是判斷緩存是否過時。底層的一些數據結構也是用的十分巧妙。若是能仔細的看看源碼,相信對你也有必定的幫助