guava底層源碼簡析

摘要

guava的緩存相信不少人都有用到,緩存

Cache<String, String> cache = CacheBuilder.newBuilder()
        .expireAfterWrite(100, TimeUnit.SECONDS)
        .maximumSize(10).build();

也經常使用的方法是設置過時時間。但使用過程當中會遇到一些問題:當過時時間到了,緩存中的對象真的會當即被釋放嗎?當緩存達到容量之後,如何高效的剔除緩存?guava cache的底層數據結構是如何的?帶着這些問題,一塊兒來看看guava cache的源碼數據結構

介紹一下guava Cache基本框架

輸入圖片說明

  • LoacalCache:實現了currentMap接口,保存了一些配置信息,例如失效時間、容量等。是保存全部緩存最外層的容器
  • segment:爲了高併發,借鑑了currentMap中的分段鎖機制,segment能夠理解是LocalCache中的一部分,不一樣的segment之間併發不受影響。每次操做根據key進行hash,保證了同一個key的put和set都在同一個segment中。segment中還有兩個分別隊列用於保存軟引用或者弱引用對象回收後的引用
  • refrenceEntry:保存一個緩存key-val的對象,相似map中的entry,只不過map中entry保存的對象的直接進行,而refrenceEntry這是在中間多了一層valueReference
  • valueReference:若是是強引用,則直接保存對象的直接引用,固然也可使用軟引用的方法。

其實經過和CurrentHashMap最類比比較好理解,只不過guava緩存在其基礎上加強了緩存過時的機制:併發

  1. 最大對象個數限制
  2. 超時機制
  3. 弱引用或者軟引用

guava會oom嗎

答案是確定的,當咱們設置緩存用不過時(或者很長),緩存的對象不限個數(或者很大),例如框架

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緩存到期就會當即清除嗎

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

guava如何找出最久未使用的緩存

在上面也說到了,是用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的對象。這樣作的好處就彌補了 鏈表的缺點

  • 判斷一個ReferenceEntry是否在隊列中,只要判斷該ReferenceEntry的前一個引用是不是NullEntry,不須要便利整個鏈表

而且能夠很方便的更新和刪除鏈表中的節點,由於每次訪問的時候均可能須要更新該鏈表,放入到鏈表的尾部,這樣,每次從access中拿出的頭節點就是最久未使用的。 而且,若是按照訪問時間來刪除緩存的時候,只要從隊列裏找出第一個訪問沒有超時的對象,那麼以前遍歷的緩存都是應該刪除的,這樣就不須要遍歷整個緩存的對象來判斷。

對應的writeQueue用來保存最久未更新的緩存隊列,實現方式和accessQueue同樣。

總結

能夠看出,guava緩存的原型是CurrentHashMap,在其基礎上考慮若是判斷緩存是否過時。底層的一些數據結構也是用的十分巧妙。若是能仔細的看看源碼,相信對你也有必定的幫助

相關文章
相關標籤/搜索