Guava Cache源碼淺析


1. 簡介

Guava Cache是指在JVM的內存中緩存數據,相比較於傳統的數據庫或redis存儲,訪問內存中的數據會更加高效,無網絡開銷。

根據Guava官網介紹,下面的這幾種情況可以考慮使用Guava Cache:

1. 願意消耗一些內存空間來提升速度。

2. 預料到某些鍵會被多次查詢。

3. 緩存中存放的數據總量不會超出內存容量。

因此,Guava Cache特別適合存儲那些訪問量大、不經常變化、數據量不是很大的數據,以改善程序性能。

2. 類圖

 

Guava Cache的類圖中,主要涉及了5個類:CacheBuilder、LocalCache、Segment、EntryFactory和ReferenceEntry,大部分業務邏輯都在前面三個類,依次介紹如下:

2.1 CacheBuilder

CacheBuilder是一個用於構建Cache的類,是建造者模式的一個例子,主要的方法有:

  • maximumSize(long maximumSize): 設置緩存存儲的所有元素的最大個數。
  • maximumWeight(long maximumWeight): 設置緩存存儲的所有元素的最大權重。
  • expireAfterAccess(long duration, TimeUnit unit): 設置元素在最后一次訪問多久后過期。
  • expireAfterWrite(long duration, TimeUnit unit): 設置元素在寫入緩存后多久過期。
  • concurrencyLevel(int concurrencyLevel): 設置並發水平,即允許多少線程無沖突的訪問Cache,默認值是4,該值越大,LocalCache中的segment數組也會越大,訪問效率越高,當然空間占用也大一些。
  • removalListener(RemovalListener<? super K1, ? super V1> listener): 設置元素刪除通知器,在任意元素無論何種原因被刪除時會調用該通知器。
  • setKeyStrength(Strength strength): 設置元素的key是強引用,還是弱引用,默認強引用,並且該屬性也指定了EntryFactory使用是強引用還是弱引用。
  • setValueStrength(Strength strength) : 設置元素的value是強引用,還是弱引用,默認強引用。

2.2 LocalCache

 LocalCache是一個支持並發訪問的Hash Map,它實現了ConcurrentMap,其內部會持有一個segment數組,元素的增刪改查都是通過調用segment的對應方法來實現的,

其主要的方法有:

  • get(Object key): 查詢一個key,內部實現是調用了Segment的get方法。
  • public V put(K key, V value): 添加一個對象到cache中,內部實現是調用了Segment的put方法。
  • remove(Object key) : 刪除一個key,內部實現是調用了Segment的remove方法。
  • replace(K key, V value):更新一個key,內部實現是調用了Segment的update方法。

2.3 Segment

 segment是實際元素的持有者,它內部持有一個table數組,數組的每個元素又對應一個鏈表,鏈表上則保存了實際的元素,它的主要方法對應LocalCache提供的增刪改查的接口,這里就不再啰嗦了。

2.4 EntryFactory

 EntryFactory是entry的創建工廠,可支持創建強引用、弱引用、強讀引用、強寫引用、強讀寫引用、弱讀引用、弱寫引用、弱讀寫引用等類型的元素。

強引用和弱引用就是java四種引用類型里面的強弱引用,默認是強引用,而讀引用是指創建的元素會記錄最后一次的訪問時間,如果用戶在CahceBuilder中調用了expireAfterAccess或者maximumWeight則會使用讀引用類型的工廠,寫引用類型也是同樣的邏輯。

2.5 ReferenceEntry

 ReferenceEntry是元素的接口定義,它的實現類就是EntryFactory中創建的元素,包含了8種類型的元素,元素中至少包含了key、value和hash三個字段,其中hash是當前元素的hash值,如果是讀引用則會多一個accessTime字段,以強引用的構造方法為例:

static class StrongEntry<K, V> extends AbstractReferenceEntry<K, V> {
    final K key;

    StrongEntry(K key, int hash, @Nullable ReferenceEntry<K, V> next) {
      this.key = key;
      this.hash = hash;
      this.next = next;
    }

    @Override
    public K getKey() {
      return this.key;
    }

    // The code below is exactly the same for each entry type.

    final int hash;
    final @Nullable ReferenceEntry<K, V> next;
    volatile ValueReference<K, V> valueReference = unset();

 

強讀引用的代碼如下:

 StrongAccessEntry(K key, int hash, @Nullable ReferenceEntry<K, V> next) {
      super(key, hash, next); // 繼承了StrongEntry,並多了accessTime
    }

    // The code below is exactly the same for each access entry type.

    volatile long accessTime = Long.MAX_VALUE;

2.6 LocalCache示例

上面對LoacheCache所涉及的主要的類都做了介紹,下面畫一張示例圖給個直觀感受,該例子中的Cache中包含的segment數組大小為4(默認值是4),第二個segment的table數組大小為4,其中第二個table中的鏈表中有3個元素(簡便起見,其他segment和table中的元素就不畫了),

 

 

3. 主要方法

上面介紹了幾個主要的類,下面從使用者的角度來把這幾個類串聯起來,主要包含了:創建Cache、添加對象、訪問對象和刪除對象。

3.1 創建Cache

 創建一個Cache的實現代碼如下:

LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
      .maximumSize(10000) // 最大元素個數
      .expireAfterWrite(Duration.ofMinutes(10)) // 元素寫入10分鍾后期
      .removalListener(MY_LISTENER)  // 自定義的一個監聽器
      .build(
          new CacheLoader<Key, Graph>() {  // 元素加載器,當查詢元素不存在時,會自動調用該方法進行加載,然后再返回元素
            public Graph load(Key key) throws AnyException {
              return createExpensiveGraph(key);
            }
         });
 }

 

3.2 添加元素

添加元素訪問的是LocalCache的put方法(注意這個方法是沒有鎖的),代碼如下:

@Override
  public V put(K key, V value) {
    checkNotNull(key);
    checkNotNull(value);
    int hash = hash(key);   // 首先計算key的hash值,並根據hash選定segment,再調用segment的put方法
    return segmentFor(hash).put(key, hash, value, false);
  }

/**
   * Returns the segment that should be used for a key with the given hash.
   *
   * @param hash the hash code for the key
   * @return the segment
   */
  Segment<K, V> segmentFor(int hash) {
    // 
    return segments[(hash >>> segmentShift) & segmentMask];
  }

再看下segment中的put方法(注意這個方法是有鎖的):

@Nullable
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
      lock();  // 一開始先加鎖
      try {
        long now = map.ticker.read(); // 當前時間,單位納秒
        preWriteCleanup(now); // 刪除過期元素

        int newCount = this.count + 1;
        if (newCount > this.threshold) { // 必要時先擴容
          expand();
          newCount = this.count + 1;
        }

        //  根據hash再定位在table中的位置
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        // 取得table中對應位置的鏈表的首個元素
        ReferenceEntry<K, V> first = table.get(index);

        // 遍歷該鏈表,如果已在鏈表中則更新值.
        for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            // We found an existing entry.

            ValueReference<K, V> valueReference = e.getValueReference();
            V entryValue = valueReference.get();

            if (entryValue == null) {
              ++modCount;
              if (valueReference.isActive()) {
                enqueueNotification(
                    key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
                setValue(e, key, value, now);
                newCount = this.count; // count remains unchanged
              } else {
                setValue(e, key, value, now);
                newCount = this.count + 1;
              }
              this.count = newCount; // write-volatile
              evictEntries(e);
              return null;
            } else if (onlyIfAbsent) {
              // Mimic
              // "if (!map.containsKey(key)) ...
              // else return map.get(key);
              recordLockedRead(e, now);
              return entryValue;
            } else {
              // clobber existing entry, count remains unchanged
              ++modCount;
              enqueueNotification(
                  key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
              setValue(e, key, value, now);
              evictEntries(e);
              return entryValue;
            }
          }
        }

        // 在鏈表中未找到,則創建一個新的元素,並添加在鏈表的頭部,即2.6章節示例中的table[1]和entry1之間.
        ++modCount;// 鏈表更新操作次數加1
        ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
        setValue(newEntry, key, value, now);
        table.set(index, newEntry);// 添加到鏈表頭部
        newCount = this.count + 1;
        this.count = newCount; // segment內的元素個數加1
        evictEntries(newEntry);
        return null;
      } finally {
        unlock();
        postWriteCleanup(); // 前面刪除元素時,會把刪除通知加入到隊列中,在這里遍歷刪除通知隊列並發出通知
      }
    }

添加方法的代碼如上所示,重點有兩個地方:

1. LocalCache的put方法中是不加鎖的,而Segment中的put方法是加鎖的,因此在訪問量很大的時候,可以通過提高concurrencyLevel的值來提高segment數組大小,減少鎖沖突。

2. 在執行put方法時,會“順便”執行清理操作,刪除過期的元素,因為Guava Cache沒有后台線程,因此刪除操作是在每次的put操作和一定次數的read操作時執行的,且清理的是當前segment的過期元素,這也告訴我們過期的元素並不是立即被刪除的,即內存不是立即釋放的,會隨着我們的讀寫操作來釋放的,當然如果Guava Cache本身訪問量不大,導致累積了大量過期元素后,再來訪問可能會有較大的訪問耗時

3.3 訪問元素

 訪問元素訪問的是LocalCache的get方法(注意這個方法是沒有鎖的),代碼如下:

public @Nullable V getIfPresent(Object key) {
// 和put一樣,先對key做hash,再定位segment,然后調用get訪問
int hash = hash(checkNotNull(key)); V value = segmentFor(hash).get(key, hash); if (value == null) { globalStatsCounter.recordMisses(1); } else { globalStatsCounter.recordHits(1); } return value; }

繼續看segment的get方法(注意這個方法是沒有鎖的):

@Nullable
    V get(Object key, int hash) {
      try {
        if (count != 0) { // read-volatile
          long now = map.ticker.read();
         // 查詢存活的元素
          ReferenceEntry<K, V> e = getLiveEntry(key, hash, now);
          if (e == null) {
            return null;
          }

          V value = e.getValueReference().get();
          if (value != null) {
            recordRead(e, now);
            // 檢查是否需要刷新元素
            return scheduleRefresh(e, e.getKey(), hash, value, now, map.defaultLoader);
          }
         // 刪除非強引用的隊列,包含key隊列和value隊列
          tryDrainReferenceQueues();
        }
        return null;
      } finally {
        postReadCleanup();// 檢查是否有過期元素待刪除
      }
    }

下面再看下getLiveEntry和postReadCleanup方法:

@Nullable
    ReferenceEntry<K, V> getLiveEntry(Object key, int hash, long now) {
      ReferenceEntry<K, V> e = getEntry(key, hash);
      if (e == null) {
        return null;
      } else if (map.isExpired(e, now)) { // 檢查元素是否過期
        tryExpireEntries(now);
        return null;
      }
      return e;
    }
@Nullable
    ReferenceEntry<K, V> getEntry(Object key, int hash) {
      // 根據hash定位table中位置的鏈表,並進行遍歷,檢查hash是否相等
      for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {
        if (e.getHash() != hash) {
          continue;
        }

        K entryKey = e.getKey();
        if (entryKey == null) { // 被垃圾回收期回收,清理引用隊列
          tryDrainReferenceQueues();
          continue;
        }

        if (map.keyEquivalence.equivalent(key, entryKey)) {
          return e;
        }
      }

      return null;
    }

    /** Returns first entry of bin for given hash. */
    ReferenceEntry<K, V> getFirst(int hash) {
      // read this volatile field only once 
      AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
      return table.get(hash & (table.length() - 1));
    }

 
void postReadCleanup() {
     // DRAIN_THRESHOLD=63,即每讀64次會執行一次清理操作
      if ((readCount.incrementAndGet() & DRAIN_THRESHOLD) == 0) {
        cleanUp();
      }
    }

讀方法相對要簡單一些,重點有兩個地方:

1. 查找到元素后檢查是否過期,過期則刪除,否則返回。

2. put方法每次調用都執行清理方法,get方法每調用64次get方法,才會執行一次清理。

注意,前面示例中的CacheBuilder創建LocalCache時,添加了元素加載器,當get方法中發現元素不存在時

 

3.4 刪除元素

 刪掉元素是invalidate()接口,該接口最終調用了segment的remove方法實現,如下:

V remove(Object key, int hash) {
      lock();  // 和put有些類似,先加鎖,再搜索,然后從鏈表刪除
      try {
        long now = map.ticker.read();
        preWriteCleanup(now);

        int newCount = this.count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            ValueReference<K, V> valueReference = e.getValueReference();
            V entryValue = valueReference.get();

            RemovalCause cause;
            if (entryValue != null) {
              cause = RemovalCause.EXPLICIT;
            } else if (valueReference.isActive()) {
              cause = RemovalCause.COLLECTED;
            } else {
              // currently loading
              return null;
            }

            ++modCount;
            // 刪除方法有些特別,看下面分析
            ReferenceEntry<K, V> newFirst =
                removeValueFromChain(first, e, entryKey, hash, entryValue, valueReference, cause);
            newCount = this.count - 1;
            table.set(index, newFirst);
            this.count = newCount; // write-volatile
            return entryValue;
          }
        }

        return null;
      } finally {
        unlock();
        postWriteCleanup();
      }
    }

// removeValueFromChain調用了removeEntryFromChain
@GuardedBy("this")
    @Nullable
    ReferenceEntry<K, V> removeEntryFromChain(
        ReferenceEntry<K, V> first, ReferenceEntry<K, V> entry) {
      int newCount = count;
      ReferenceEntry<K, V> newFirst = entry.getNext();
     // 刪除元素時,沒有直接從鏈表上面摘除,而是遍歷first和entry之間的元素,並拷貝新建新的元素構建鏈表
      for (ReferenceEntry<K, V> e = first; e != entry; e = e.getNext()) {
        ReferenceEntry<K, V> next = copyEntry(e, newFirst);
        if (next != null) {
          newFirst = next;
        } else {
          removeCollectedEntry(e);
          newCount--;
        }
      }
      this.count = newCount;
      return newFirst;
    }

注意刪除的時候,並沒有直接從鏈表摘除,而是做了一次遍歷新建了一個鏈表,舉個例子:

 為什么要做一次遍歷呢?先看一下StrongEntry的定義:

static class StrongEntry<K, V> extends AbstractReferenceEntry<K, V> {
    final K key;
    final int hash;
    final @Nullable ReferenceEntry<K, V> next;
    volatile ValueReference<K, V> valueReference = unset();
}

key,hash和next都是final的,通過這種新建鏈表的方式,可以保證當前的並發讀線程是能讀到數據的(讀方法無鎖),即使是過期的,這其實就是CopyOnWrite的思想。

4. 小結

從上面分析可以看出,guava cache是一款非常優秀的本地緩存組件,為了得到更好的效率,減少寫操作鎖沖突(讀操作無鎖),可以將concurrencyLevel設置為當前CPU核數的2兩倍

初始化代碼如下:

Cache<String, Integer> lcache = CacheBuilder.newBuilder()
                  .maximumSize(100)
                  .concurrencyLevel(Runtime.getRuntime().availableProcessors()*2) // 當前CPU核數*2
.expireAfterWrite(30, TimeUnit.SECONDS) .build();

之后就可以通過put和getIfPresent來進行元素訪問了,例如:

// 賦值
for
(int i=0; i<10000; i++) { lcache.put(String.valueOf(i), i); } // 查詢 Integer value = lcache.getIfPresent("10");

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM