02 初識(shí)緩存-GuavaCache

在上篇文章01 初識(shí)緩存-了解緩存中簡單了介紹了下緩存的歷程以及幾種常見的技術(shù)進(jìn)行簡單介紹,本著學(xué)習(xí)的目的本節(jié)針對(duì)GuavaCache進(jìn)行一個(gè)專題介紹,可能理解有限歡迎指正。

一 簡介

Guava Cache,是Google 出品的 Java 核心增強(qiáng)庫的緩存部分,一種非常優(yōu)秀本地緩存解決方案,提供了基于容量,時(shí)間和引用的緩存回收方式。guava cache可以按照多種策略來清理存儲(chǔ)在其中的緩存值且保持很高的并發(fā)讀寫性能?;谌萘康姆绞絻?nèi)部實(shí)現(xiàn)采用LRU算法,基于引用回收很好的利用了Java虛擬機(jī)的垃圾回收機(jī)制。其中的緩存構(gòu)造器CacheBuilder采用構(gòu)建者模式提供了設(shè)置好各種參數(shù)的緩存對(duì)象,緩存核心類LocalCache里面的內(nèi)部類Segment與jdk1.7及以前的ConcurrentHashMap非常相似,都繼承于ReetrantLock,還有六個(gè)隊(duì)列,以實(shí)現(xiàn)豐富的本地緩存方案。

二 GuavaCache使用

2.1 緩存加載器 CacheLoader

CacheLoader是GuavaCache提供的一種加載緩存的機(jī)制,可以通過指定的鍵值進(jìn)行計(jì)算/加載需要進(jìn)行匹配的緩存數(shù)據(jù)。GuavaCache雖然提供了這種方式,但是它并不要求編程者一定需要遵循它這種方案,也可以根據(jù)自定的實(shí)際需求來定制自己加載邏輯。根據(jù)GuavaCache提出的如果想復(fù)寫其緩存的加載方式,但是仍要保留“get-if-absent-compute”語義,GuavaCache在進(jìn)行緩存獲取的時(shí)候提供了一種解決方案:可以在調(diào)用get方法時(shí)傳入一個(gè)Callable實(shí)例,來達(dá)到目的。緩存的對(duì)象可以通過Cache.put直接插入,但是自動(dòng)加載是首選,因?yàn)樽詣?dòng)加載可以更加容易的判斷所有緩存信息的一致性。

Guava Cache針對(duì)開發(fā)者提供了非常友好的編程方式,使用非常簡單:

下面例子分別從不同的角度:

From a CacheLoader

LoadingCache 緩存是通過一個(gè)CacheLoader來構(gòu)建緩存。創(chuàng)建一個(gè)CacheLoader僅需要實(shí)現(xiàn)V load(K key) throws Exception方法即可。通過方法get(K)可以對(duì)LoadingCache進(jìn)行查詢。該方法要不返回已緩存的值,要不通過CacheLoader來自動(dòng)加載相應(yīng)的值到緩存中。

From a Callable

所有類型的Guava Cache,不論是否會(huì)自動(dòng)加載,都支持get(K, Callable(V))方法。當(dāng)給定鍵的緩存值已存在時(shí)則直接返回,否則通過指定的Callable方法進(jìn)行計(jì)算并將值存放到緩存中。直到加載完成時(shí),相應(yīng)的緩存才會(huì)被更改。該方法簡單實(shí)現(xiàn)了"if cached, return; otherwise create, cache and return"("如果有緩存則返回;否則運(yùn)算、緩存、然后返回")語義。

Cache.put

使用cache.put(key, value)方法可以直接向緩存中插入值,這會(huì)直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請(qǐng)注意,asMap視圖的任何方法都不能保證緩存項(xiàng)被原子地加載到緩存中。進(jìn)一步說,asMap視圖的原子運(yùn)算在Guava Cache的原子加載范疇之外,所以相比于Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 應(yīng)該總是優(yōu)先使用。

/**
 * @author mzw
 * @version V1.0.0
 * @description
 * @data 2019-05-29 14:53
 * @see
 **/
public class GuavaCache {

    private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCache.class);

    public static void main(String[] args) throws ExecutionException {

        //構(gòu)建緩存加載器
        CacheLoader<String, String> loader = new CacheLoader<String, String>() {
            @Override
            public String load(String key) {
                LOGGER.info(key + " is loaded from a cacheLoader!");
                return key;
            }
        };
        //構(gòu)建移除監(jiān)聽器:非必要
        RemovalListener<String, String> removalListener =
                removal -> LOGGER.info("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");

        //構(gòu)建緩存
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(4)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .removalListener(removalListener)
                .build(loader);

        //直接放入數(shù)據(jù)
        for (int i = 0; i < 3; i++) {
            cache.put("key" + i, "value" + i);
        }
        //獲取數(shù)據(jù)
        LOGGER.info("key2  > " + cache.getIfPresent("key2"));
        LOGGER.info("key  > " + cache.getIfPresent("key"));
        //From a CacheLoade
        LOGGER.info("key  > " + cache.get("key"));
        //From a Callable
        LOGGER.info("key  > " + cache.get("k1", () -> "dd"));
    }
}

輸出結(jié)果

17:36:12.084 [main] INFO com.maozw.quartz.cache.GuavaCache - key2  > value2
17:36:12.086 [main] INFO com.maozw.quartz.cache.GuavaCache - key  > null
17:36:12.090 [main] INFO com.maozw.quartz.cache.GuavaCache - key is loaded from a cacheLoader!
17:36:12.092 [main] INFO com.maozw.quartz.cache.GuavaCache - key  > key
17:36:12.094 [main] INFO com.maozw.quartz.cache.GuavaCache - [key0:value0] is evicted!
17:36:12.094 [main] INFO com.maozw.quartz.cache.GuavaCache - k1  > dd

2.1 緩存回收

guava中數(shù)據(jù)的移除分為被動(dòng)移除和主動(dòng)移除兩種,
被動(dòng)移除數(shù)據(jù)的方式,簡介中曾說過guava cache可以按照多種策略來清理存儲(chǔ)在其中的緩存值且保持很高的并發(fā)讀寫性能?;谌萘康姆绞絻?nèi)部實(shí)現(xiàn)采用LRU算法,基于引用回收很好的利用了Java虛擬機(jī)的垃圾回收機(jī)制。Guava Cache提供了三種基本的緩存回收方式:

基于緩存容量大小的移除

當(dāng)緩存中的元素?cái)?shù)量超過指定值時(shí),會(huì)把不常用的鍵值對(duì)從cache中移除。

  • 緩存容量大小指的是cache中的條目數(shù);

  • 并不是完全到了緩存容量大小才開始移除不常用的數(shù)據(jù)的,而是接近緩存容量大小的時(shí)候系統(tǒng)就會(huì)開始做移除的動(dòng)作;

  • 如果一個(gè)鍵值對(duì)已經(jīng)從緩存中被移除了,你再次請(qǐng)求訪問的時(shí)候,如果cachebuild是使用cacheloader方式的,那依然還是會(huì)從cacheloader中再取一次值,如果還沒有,就會(huì)拋出異常。

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(4)//緩存容量大小
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .removalListener(removalListener)
                .build(loader);

基于緩存時(shí)間的移除

guava提供了兩個(gè)基于時(shí)間移除的方法

  • expireAfterAccess(long, TimeUnit):這個(gè)方法是根據(jù)某個(gè)鍵值對(duì)最后一次訪問之后多少時(shí)間后移除。緩存項(xiàng)在給定時(shí)間內(nèi)沒有被讀/寫訪問,則回收。請(qǐng)注意這種緩存的回收順序和基于大小回收一樣。

  • expireAfterWrite(long, TimeUnit):這個(gè)方法是根據(jù)某個(gè)鍵值對(duì)被創(chuàng)建或值被替換后多少時(shí)間移除。緩存項(xiàng)在給定時(shí)間內(nèi)沒有被寫訪問(創(chuàng)建或覆蓋),則回收。如果認(rèn)為緩存數(shù)據(jù)總是在固定時(shí)候后變得陳舊不可用,這種回收方式是可取的.

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(4)//緩存容量大小
                .expireAfterWrite(10, TimeUnit.MINUTES)//緩存時(shí)間
                .removalListener(removalListener)
                .build(loader);

基于引用回收(Reference-based Eviction)

這種移除方式主要是基于java的垃圾回收機(jī)制,根據(jù)鍵或者值的引用關(guān)系決定移除

  • CacheBuilder.weakKeys():使用弱引用存儲(chǔ)鍵。當(dāng)鍵沒有其它(強(qiáng)或軟)引用時(shí),緩存項(xiàng)可以被垃圾回收。

  • CacheBuilder.weakValues():使用弱引用存儲(chǔ)值。當(dāng)值沒有其它(強(qiáng)或軟)引用時(shí),緩存項(xiàng)可以被垃圾回收。

  • CacheBuilder.softValues():使用軟引用存儲(chǔ)值。軟引用只有在響應(yīng)內(nèi)存需要時(shí),才按照全局最近最少使用的順序回收。

主動(dòng)移除數(shù)據(jù)方式

主動(dòng)移除有三種方法:

  • 單獨(dú)緩存項(xiàng)移除 Cache.invalidate(key)

  • 批量緩存項(xiàng)移除 Cache.invalidateAll(keys)

  • 清除所有緩存項(xiàng) Cache.invalidateAll()

移除監(jiān)聽器

通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個(gè)監(jiān)聽器,以便緩存項(xiàng)被移除時(shí)做一些額外操作。緩存項(xiàng)被移除時(shí),RemovalListener會(huì)獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。如果需要改成異步形式,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor) 。

緩存儲(chǔ)移除的時(shí)機(jī)

Guava cache中通過CacheBuilder構(gòu)建的緩存數(shù)據(jù)不會(huì)"自動(dòng)"執(zhí)行清理和回收工作,也不會(huì)在某個(gè)緩存項(xiàng)過期后馬上清理,它會(huì)在讀/寫操作后同步進(jìn)行清理工作,只是讀操作時(shí)可能執(zhí)行的機(jī)會(huì)會(huì)少少一些。
原因:如果自動(dòng)清理緩存,就必須存在一個(gè)線程,這個(gè)線程會(huì)和用戶操作競爭共享鎖。此外,某些環(huán)境下線程創(chuàng)建可能受限制,這樣CacheBuilder就不可用了。

同時(shí)用戶可以自主的去控制清除時(shí)機(jī),比如固定的時(shí)間間隔調(diào)用Cache.cleanUp()。

2.2 刷新

刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個(gè)過程可以是異步的。在刷新操作進(jìn)行時(shí),緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。如果刷新過程拋出異常,緩存將保留舊值,而異常會(huì)在記錄到日志后被丟棄。

重載CacheLoader.reload(K, V)可以擴(kuò)展刷新時(shí)的行為,這個(gè)方法允許開發(fā)者在計(jì)算新值時(shí)使用舊的值。
示例如下:

/**
 * @author mzw
 * @version V1.0.0
 * @description
 * @data 2019-05-29 14:53
 * @see
 **/
public class GuavaCacheDemo {

    private static final Logger LOGGER = LoggerFactory.getLogger(GuavaCacheDemo.class);
    public static void main(String[] args) throws ExecutionException, InterruptedException {

        //構(gòu)建移除監(jiān)聽器:非必要
        RemovalListener<String, String> removalListener =
                removal -> LOGGER.info("[" + removal.getKey() + ":" + removal.getValue() + "] is evicted!");

        //構(gòu)建緩存
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(5)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .removalListener(removalListener)
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String s) {
                        return s + " -> load";
                    }

                    @Override
                    public ListenableFuture<String> reload(String key, String oldValue) {
                        LOGGER.info(key + "  > reload : " + oldValue);
                        ListenableFutureTask<String> reloadTask = ListenableFutureTask.create(() -> oldValue + " -> reload");
                        Executors.newFixedThreadPool(1).submit(reloadTask);
                        return reloadTask;
                    }
                });

        //放入數(shù)據(jù)
        for (int i = 0; i < 3; i++) {
            cache.put("key" + i, "value" + i);
        }
        //獲取數(shù)據(jù)
        LOGGER.info("key  > " + cache.getIfPresent("key"));
        LOGGER.info("key1  > " + cache.getIfPresent("key1"));
        cache.refresh("key1");
        LOGGER.info("refresh : k  > " + cache.get("key1"));

    }
}

輸出結(jié)果

19:32:01.069 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - key  > null
19:32:01.071 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - key1  > value1
19:32:01.077 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - k  > aa
19:32:01.077 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - key1  > reload : value1
19:32:01.085 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - [key1:value1] is evicted!
19:32:01.086 [main] INFO com.maozw.quartz.cache.GuavaCacheDemo - refresh : k  > value1 -> reload

統(tǒng)計(jì)

簡單介紹
CacheBuilder.recordStats():用來開啟Guava Cache的統(tǒng)計(jì)功能。統(tǒng)計(jì)打開后,Cache.stats()方法會(huì)返回CacheS tats 對(duì)象以提供如下統(tǒng)計(jì)信息:
hitRate():緩存命中率;
averageLoadPenalty():加載新值的平均時(shí)間,單位為納秒;
evictionCount():緩存項(xiàng)被回收的總數(shù),不包括顯式清除。

三 Guava Cache 解讀

在解讀之前,先提一下Guava Cache的一些組件:
Guava Cache組件中核心的類和接口列舉如下:
接口:

  • Cache 頂級(jí)接口,定義get、put、invalidate等操作,以及獲取緩存統(tǒng)計(jì)數(shù)據(jù)的方法等。
  • LoadingCache 繼承自Cache,并另外提供了一些當(dāng)get數(shù)據(jù)不存在時(shí)自動(dòng)去load相關(guān)key(s)所對(duì)應(yīng)的value(s)的契約(即接口中的抽象方法),具體實(shí)現(xiàn)見LoadingCache的具體實(shí)現(xiàn)類。
  • RemovalListener 監(jiān)聽器接口,在緩存被移除的時(shí)候用來做一些操作,與下面的RemovalNotification、RemovalCause配套使用。很明顯這是個(gè)觀察者模式的應(yīng)用。
  • Weigher 權(quán)重的接口,提供int weigh(K key, V value)抽象方法,給緩存中的Entry賦予權(quán)重信息。

抽象類:

  • AbstractCache 本身是抽象類,實(shí)現(xiàn)自Cache接口,基本沒做什么實(shí)際的工作,大多數(shù)方法的實(shí)現(xiàn)只是簡單拋出UnsupportedOperationException.該抽象類提供了Cache接口的骨架,為了避免子類直接繼承Cache接口時(shí)必須實(shí)現(xiàn)所有抽象方法,這種手法在其他地方也很常見,個(gè)人覺得都算得上是一種設(shè)計(jì)模式了。
  • AbstractLoadingCache 繼承自AbstractCache并實(shí)現(xiàn)了LoadingCache接口,目的也是提供一個(gè)骨架,其中的某些方法提供了在get不到數(shù)據(jù)時(shí)會(huì)自動(dòng)Load數(shù)據(jù)的契約。
  • CacheLoader 抽象類,最核心的方法就是load,封裝load數(shù)據(jù)的操作,具體如何laod與該抽象類的具體子類有關(guān),只需要重寫laod方法,就可以在get不到數(shù)據(jù)時(shí)自動(dòng)去load數(shù)據(jù)到緩存中。
  • ForwardingCache 裝飾器模式的用法,所有對(duì)緩存的操作都委托給其他的實(shí)現(xiàn)了Cache接口的類,該抽象類中有一個(gè)抽象方法protected abstract Cache<K, V> delegate();不難推測(cè)出來,其他的方法中均使用了該代理。即類似get(key){delegate().get(key)}
  • ForwardingLoadingCache 自行推斷,不解釋。

實(shí)現(xiàn)類:

  • CacheBuilder 建造者模式的應(yīng)用,緩存構(gòu)建器。構(gòu)建緩存的入口,指定緩存配置參數(shù)并初始化本地緩存。通過該類來組裝Cache,最后調(diào)用build方法來生成Cache的實(shí)例。
  • CacheBuilderSpec 用來構(gòu)建CacheBuilder的實(shí)例,其中提供了一些配置參數(shù),用這些配置的參數(shù)來通過CacheBuilder實(shí)例最終構(gòu)建Cache實(shí)例。
  • CacheStats 緩存使用情況統(tǒng)計(jì)信息,比如命中多少次,缺失多少次等等。
  • LocalCache 本地緩存最核心的類,Cache接口實(shí)例的代理人,Cache接口提供的一些方法內(nèi)部均采委托給LocalCache實(shí)例來實(shí)現(xiàn),LocalCache的具體實(shí)現(xiàn)類似于ConcurrentHashMap,也采用了分段的方式。

CacheBuilder

CacheBuilder是緩存配置和構(gòu)建入口。

在上面例子中我們構(gòu)建緩存使用如下方式:現(xiàn)在解析CacheBuilder這個(gè)建造者的結(jié)構(gòu)

LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(4)//緩存容量大小
                .expireAfterWrite(10, TimeUnit.MINUTES)//緩存時(shí)間
                .removalListener(removalListener)
                .build(loader);

整個(gè)類內(nèi)容太長,分段進(jìn)行解說:

CacheBuilder 屬性

  • DEFAULT_INITIAL_CAPACITY://緩存的默認(rèn)初始化大小;

  • DEFAULT_CONCURRENCY_LEVEL:LocalCache默認(rèn)并發(fā)數(shù),用來評(píng)估Segment的個(gè)數(shù);

  • DEFAULT_EXPIRATION_NANOS://默認(rèn)的緩存過期時(shí)間;

  • initialCapacity://初始緩存大小

  • concurrencyLevel:用于計(jì)算有并發(fā)量

  • maximumSize:cache中最多能存放的緩存entry個(gè)數(shù)

  • expireAfterWriteNanos://緩存超時(shí)時(shí)間(起點(diǎn):緩存被創(chuàng)建或被修改)

  • expireAfterAccessNanos://緩存超時(shí)時(shí)間(起點(diǎn):緩存被創(chuàng)建或被修改或被訪問)

public final class CacheBuilder<K, V> {
  private static final int DEFAULT_INITIAL_CAPACITY = 16;
  private static final int DEFAULT_CONCURRENCY_LEVEL = 4;
  private static final int DEFAULT_EXPIRATION_NANOS = 0;
  private static final int DEFAULT_REFRESH_NANOS = 0;

  static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER = Suppliers.ofInstance(
      new StatsCounter() {
        ...//省去無關(guān)代碼
      });
  static final CacheStats EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0);

  static final Supplier<StatsCounter> CACHE_STATS_COUNTER =
      new Supplier<StatsCounter>() {
    @Override
    public StatsCounter get() {
      return new SimpleStatsCounter();
    }
  };

    ...//省去無關(guān)代碼

  private static final Logger logger = Logger.getLogger(CacheBuilder.class.getName());

  static final int UNSET_INT = -1;

  boolean strictParsing = true;

  int initialCapacity = UNSET_INT;
  int concurrencyLevel = UNSET_INT;
  long maximumSize = UNSET_INT;
  long maximumWeight = UNSET_INT;
  Weigher<? super K, ? super V> weigher;

  Strength keyStrength;//鍵的引用類型(strong、weak、soft)
  Strength valueStrength;//值的引用類型(strong、weak、soft)

  long expireAfterWriteNanos = UNSET_INT;
  long expireAfterAccessNanos = UNSET_INT;
  long refreshNanos = UNSET_INT;
  //key比較策略 
  Equivalence<Object> keyEquivalence;
  Equivalence<Object> valueEquivalence;

  RemovalListener<? super K, ? super V> removalListener;//元素被移除的監(jiān)聽器
  Ticker ticker;
  //狀態(tài)計(jì)數(shù)器,默認(rèn)為NULL_STATS_COUNTER,即不啟動(dòng)計(jì)數(shù)功能
  Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER;

    ...
}

build方法

CacheBuilder構(gòu)建緩存有兩個(gè)方法:

  • 構(gòu)建一個(gè)具有數(shù)據(jù)加載功能的緩存,調(diào)用LocalCache構(gòu)造方法

  • 構(gòu)建一個(gè)沒有數(shù)據(jù)加載功能的緩存,調(diào)用LocalCache構(gòu)造方法,但loader為null

public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
      CacheLoader<? super K1, V1> loader) {
    checkWeightWithWeigher();
    return new LocalCache.LocalLoadingCache<K1, V1>(this, loader);
}

public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    checkWeightWithWeigher();
    checkNonLoadingCache();
    return new LocalCache.LocalManualCache<K1, V1>(this);
}

至此我們就需要先了解一下LocalCache了,因?yàn)槲覀兇藭r(shí)調(diào)用該類的構(gòu)造方法LocalCache()和實(shí)例方法LocalLoadingCache()/LocalManualCache()

LocalCache

LocalCache是guava cache的核心類。

LocalCache的數(shù)據(jù)結(jié)構(gòu)與ConcurrentHashMap很相似,都由多個(gè)segment組成,且各segment相對(duì)獨(dú)立,互不影響,所以能支持并行操作。每個(gè)segment由一個(gè)table和若干隊(duì)列組成。緩存數(shù)據(jù)存儲(chǔ)在table中,其類型為AtomicReferenceArray<ReferenceEntry<K, V>>,即一個(gè)數(shù)組,數(shù)組中每個(gè)元素是一個(gè)鏈表。兩個(gè)隊(duì)列分別是writeQueue和accessQueue,用來存儲(chǔ)寫入的數(shù)據(jù)和最近訪問的數(shù)據(jù),當(dāng)數(shù)據(jù)過期,需要刷新整體緩存(見上述示例最后一次cache.getIfPresent("key5"))時(shí),遍歷隊(duì)列,如果數(shù)據(jù)過期,則從table中刪除。

LocalCache 數(shù)據(jù)結(jié)構(gòu)

Segment<K, V>[] segments;

Segment繼承于ReetrantLock,減小鎖粒度,提高并發(fā)效率。

AtomicReferenceArray<ReferenceEntry<K, V>> table;

類似于HasmMap中的table一樣,相當(dāng)于entry的容器。

ReferenceEntry<K, V> referenceEntry;

基于引用的Entry,其實(shí)現(xiàn)類有弱引用Entry,強(qiáng)引用Entry等

ReferenceQueue<K> keyReferenceQueue;

已經(jīng)被GC,需要內(nèi)部清理的鍵引用隊(duì)列。

ReferenceQueue<V> valueReferenceQueue;

已經(jīng)被GC,需要內(nèi)部清理的值引用隊(duì)列。

Queue<ReferenceEntry<K, V>> recencyQueue;

記錄升級(jí)可訪問列表清單時(shí)的entries,當(dāng)segment上達(dá)到臨界值或發(fā)生寫操作時(shí)該隊(duì)列會(huì)被清空。

Queue<ReferenceEntry<K, V>> writeQueue;

按照寫入時(shí)間進(jìn)行排序的元素隊(duì)列,寫入一個(gè)元素時(shí)會(huì)把它加入到隊(duì)列尾部。

Queue<ReferenceEntry<K, V>> accessQueue;

按照訪問時(shí)間進(jìn)行排序的元素隊(duì)列,訪問(包括寫入)一個(gè)元素時(shí)會(huì)把它加入到隊(duì)列尾部

LocalCache 構(gòu)造器

構(gòu)造器是通過CacheBuilder的方法對(duì)變量進(jìn)行初始化。具體變量解說可參照CacheBuilder 屬性解說。

  /**
   * Creates a new, empty map with the specified strategy, initial capacity and concurrency level.
   */
  LocalCache(
      CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
    concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
    //默認(rèn)為強(qiáng)引用
    keyStrength = builder.getKeyStrength();
    valueStrength = builder.getValueStrength();

    keyEquivalence = builder.getKeyEquivalence();
    valueEquivalence = builder.getValueEquivalence();

    maxWeight = builder.getMaximumWeight();
    weigher = builder.getWeigher();
    expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
    expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
    refreshNanos = builder.getRefreshNanos();

    removalListener = builder.getRemovalListener();
    removalNotificationQueue = (removalListener == NullListener.INSTANCE)
        ? LocalCache.<RemovalNotification<K, V>>discardingQueue()
        : new ConcurrentLinkedQueue<RemovalNotification<K, V>>();

    ticker = builder.getTicker(recordsTime());
    entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
    globalStatsCounter = builder.getStatsCounterSupplier().get();
    defaultLoader = loader;

    int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
    if (evictsBySize() && !customWeigher()) {
      initialCapacity = Math.min(initialCapacity, (int) maxWeight);
    }
     ...
    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel
           && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      ++segmentShift;
      segmentCount <<= 1;
    }
    this.segmentShift = 32 - segmentShift;
    segmentMask = segmentCount - 1;
    //初始化segments大小
    this.segments = newSegmentArray(segmentCount);

    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }

    int segmentSize = 1;
    while (segmentSize < segmentCapacity) {
      segmentSize <<= 1;
    }
    //初始化Segments
    if (evictsBySize()) {
      // Ensure sum of segment max weights = overall max weights
      long maxSegmentWeight = maxWeight / segmentCount + 1;
      long remainder = maxWeight % segmentCount;
      for (int i = 0; i < this.segments.length; ++i) {
        if (i == remainder) {
          maxSegmentWeight--;
        }
        this.segments[i] =
            createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
      }
    } else {
      for (int i = 0; i < this.segments.length; ++i) {
        this.segments[i] =
            createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
      }
    }
  }

Segment初始化操作

初始化容器

Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight,
        StatsCounter statsCounter) {
      this.map = map;
      this.maxSegmentWeight = maxSegmentWeight;
      this.statsCounter = checkNotNull(statsCounter);
      initTable(newEntryArray(initialCapacity));//初始化table

      keyReferenceQueue = map.usesKeyReferences()
           ? new ReferenceQueue<K>() : null;//key引用隊(duì)列

      valueReferenceQueue = map.usesValueReferences()
           ? new ReferenceQueue<V>() : null;//value引用隊(duì)列

      recencyQueue = map.usesAccessQueue()
          ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
          : LocalCache.<ReferenceEntry<K, V>>discardingQueue();

      writeQueue = map.usesWriteQueue()
          ? new WriteQueue<K, V>()
          : LocalCache.<ReferenceEntry<K, V>>discardingQueue();//寫入元素隊(duì)列

      accessQueue = map.usesAccessQueue()
          ? new AccessQueue<K, V>()
          : LocalCache.<ReferenceEntry<K, V>>discardingQueue();//訪問元素隊(duì)列
}

以上是整個(gè)初始化流程


LocalCache put加載緩存

Cache 接口聲明

@Beta
@GwtCompatible
public interface Cache<K, V> {
   void put(K key, V value);
}

LocalCache的實(shí)現(xiàn)

  • 內(nèi)部調(diào)用segmentFor(int hash)方法,該方法返回指定的散列hash鍵值匹配的段。

  • 然后調(diào)用Segment的put方法

@GwtCompatible(emulated = true)
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
@Override
  public V put(K key, V value) {
    checkNotNull(key);
    checkNotNull(value);
    int hash = hash(key);
    return segmentFor(hash).put(key, hash, value, false);
  }
}

segmentFor(hash).put(key, hash, value, false)

下面第四行代碼可以看出preWriteCleanup在每次put之前都會(huì)清理動(dòng)作,我在緩存儲(chǔ)移除的時(shí)機(jī)小段中進(jìn)行過提示,緩存的清除時(shí)機(jī)是在讀/寫操作的時(shí)候進(jìn)行的。

@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;//localCache的Count+1
    if (newCount > this.threshold) { // ensure capacity 是否要進(jìn)行擴(kuò)容
      expand();//擴(kuò)容
      newCount = this.count + 1;
    }
    //獲取當(dāng)前Entry中的HashTable的Entry數(shù)組
    AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
    int index = hash & (table.length() - 1);//計(jì)算散列值對(duì)應(yīng)table的索引位置
    ReferenceEntry<K, V> first = table.get(index);//通過索引獲取ReferenceEntry

    // Look for an existing entry.進(jìn)行遍歷 如果找到則進(jìn)行下面邏輯
    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.如果找到則進(jìn)行下面邏輯
        // 對(duì)應(yīng)的值引用
        ValueReference<K, V> valueReference = e.getValueReference();
        V entryValue = valueReference.get();//獲取值
        // cache 提供基于引用的回收策略,此處可能為null:即可能會(huì)GC了
        if (entryValue == null) {
          ++modCount;
          if (valueReference.isActive()) {
            enqueueNotification(key, hash, valueReference, RemovalCause.COLLECTED);
            setValue(e, key, value, now);//存儲(chǔ)數(shù)據(jù),并且將新增加的元素寫入兩個(gè)隊(duì)列中
            newCount = this.count; // count remains unchanged
          } else {
            setValue(e, key, value, now);
            newCount = this.count + 1;
          }
          this.count = newCount; // write-volatile
          evictEntries();//淘汰緩存
          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
          // 如果存在且值不為null 則進(jìn)行更新value
          ++modCount;
          enqueueNotification(key, hash, valueReference, RemovalCause.REPLACED);
          setValue(e, key, value, now);
          evictEntries();
          return entryValue;
        }
      }
    }

    // Create a new entry. 不存在則新創(chuàng)建newEntry
    ++modCount;
    ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
    setValue(newEntry, key, value, now);
    table.set(index, newEntry);
    newCount = this.count + 1;
    this.count = newCount; // write-volatile
    evictEntries();
    return null;
  } finally {
    unlock();
    postWriteCleanup();
  }
}

LocalCache preWriteCleanup(now);

簡單解說下preWriteCleanup(now);preWriteCleanup在每次put之前都會(huì)清理動(dòng)作

  • 通過下面代碼的調(diào)用鏈:清理的是keyReferenceQueue和valueReferenceQueue這兩個(gè)隊(duì)列,這兩個(gè)隊(duì)列是引用隊(duì)列,Guava Cache為了支持弱引用和軟引用,引入了引用清空隊(duì)列.

  • expireEntries(now) 基于過期時(shí)間的清除方式

@GuardedBy("this")
void preWriteCleanup(long now) {
  runLockedCleanup(now);
}

void runLockedCleanup(long now) {
  if (tryLock()) {
    try {
      drainReferenceQueues();
      expireEntries(now); // calls drainRecencyQueue
      readCount.set(0);
    } finally {
      unlock();
    }
  }
}
//排空鍵和值引用隊(duì)列,清除包含垃圾收集的鍵或值的內(nèi)部條目
@GuardedBy("this")
void drainReferenceQueues() {
  if (map.usesKeyReferences()) {
    drainKeyReferenceQueue();
  }
  if (map.usesValueReferences()) {
    drainValueReferenceQueue();
  }
}
//基于過期時(shí)間的清除
@GuardedBy("this")
void expireEntries(long now) {
  drainRecencyQueue();

  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();
    }
  }
}

drainKeyReferenceQueue
下面就是清理過程:

@GuardedBy("this")
void drainKeyReferenceQueue() {
  Reference<? extends K> ref;
  int i = 0;
  while ((ref = keyReferenceQueue.poll()) != null) {
    @SuppressWarnings("unchecked")
    ReferenceEntry<K, V> entry = (ReferenceEntry<K, V>) ref;
    map.reclaimKey(entry);
    if (++i == DRAIN_MAX) {
      break;
    }
  }
}

void reclaimKey(ReferenceEntry<K, V> entry) {
    int hash = entry.getHash();
    segmentFor(hash).reclaimKey(entry, hash);
}

/**
 * Removes an entry whose key has been garbage collected.
 */
boolean reclaimKey(ReferenceEntry<K, V> entry, int hash) {
  lock();
  try {
    int newCount = 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()) {
      if (e == entry) {
        ++modCount;
        ReferenceEntry<K, V> newFirst = removeValueFromChain(
            first, e, e.getKey(), hash, e.getValueReference(), RemovalCause.COLLECTED);
        newCount = this.count - 1;
        table.set(index, newFirst);
        this.count = newCount; // write-volatile
        return true;
      }
    }

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

LocalCache get獲取緩存值

Cache 接口聲明

@Beta
@GwtCompatible
public interface Cache<K, V> {
   V get(K key) throws ExecutionException;
}

LocalLoadingCache的實(shí)現(xiàn)

下面是get方法的調(diào)用鏈:最終執(zhí)行segmentFor(hash).get(key, hash, loader);

  • 首先通過key 與 散列值獲取Entry,如果獲取Entry不為null;繼續(xù)獲取對(duì)應(yīng)的value;如果value不為null,并更新訪問時(shí)間,加入recencyQueue,之后進(jìn)行判斷是否進(jìn)行刷新邏輯 返回。

  • 如果value為null,檢測(cè)是否進(jìn)行l(wèi)oading,如果是則等待,等待結(jié)果waitForLoadingValue(e, key, valueReference);;

  • 如果獲取Entry為null,則lockedGetOrLoad(key, hash, loader); ,其方法邏輯實(shí)現(xiàn)與put方法非常相似,這里就不在做介紹。有興趣的讀者可以去看看。也需要說明的時(shí)候,此時(shí)該方法相當(dāng)有就是寫緩存,所以也會(huì)進(jìn)行加鎖。

@Override
public V get(K key) throws ExecutionException {
  return localCache.getOrLoad(key);
}

V getOrLoad(K key) throws ExecutionException {
  return get(key, defaultLoader); 
}

V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
  int hash = hash(checkNotNull(key));
  return segmentFor(hash).get(key, hash, loader);
}

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
  checkNotNull(key);
  checkNotNull(loader);
  try {
    if (count != 0) { // read-volatile volatile讀會(huì)刷新緩存,盡量保證可見性,如果為0那么直接load
      // don't call getLiveEntry, which would ignore loading values
      ReferenceEntry<K, V> e = getEntry(key, hash);
      if (e != null) {//判斷通過key與hash獲取的Entry是否為null,不為null則存在
        long now = map.ticker.read();//獲取當(dāng)前的訪問時(shí)間
        V value = getLiveValue(e, now);//根據(jù)當(dāng)前訪問時(shí)間獲取Live的數(shù)據(jù)
        if (value != null) {
          recordRead(e, now);//設(shè)置entry的AccessTime。并且加入recencyQueue
          statsCounter.recordHits(1);//記錄緩存命中
          return scheduleRefresh(e, key, hash, value, now, loader)// 如果定時(shí)刷新,嘗試刷新value
        }
        //value為null,如果此時(shí)value正在刷新,那么此時(shí)等待刷新結(jié)果
        ValueReference<K, V> valueReference = e.getValueReference();
        if (valueReference.isLoading()) {
          return waitForLoadingValue(e, key, valueReference);
        }
      }
    }

    // at this point e is either null or expired;
    return lockedGetOrLoad(key, hash, loader);
  } catch (ExecutionException ee) {
    Throwable cause = ee.getCause();
    if (cause instanceof Error) {
      throw new ExecutionError((Error) cause);
    } else if (cause instanceof RuntimeException) {
      throw new UncheckedExecutionException(cause);
    }
    throw ee;
  } finally {
    postReadCleanup();//每次Put和get之后都要進(jìn)行一次Clean
  }
}

至此針對(duì) Guava Cache的使用,以及它的運(yùn)轉(zhuǎn)流程做了一個(gè)簡單的介紹??赡芩接邢奕绻胁徽_的地方請(qǐng)指正。


最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容