鍍金池/ 教程/ Java/ 緩存
不可變集合
排序: Guava 強(qiáng)大的”流暢風(fēng)格比較器”
強(qiáng)大的集合工具類:java.util.Collections 中未包含的集合工具
新集合類型
常見(jiàn) Object 方法
I/O
前置條件
字符串處理:分割,連接,填充
散列
原生類型
數(shù)學(xué)運(yùn)算
使用和避免 null
Throwables:簡(jiǎn)化異常和錯(cuò)誤的傳播與檢查
google Guava 包的 ListenableFuture 解析
事件總線
緩存
函數(shù)式編程
區(qū)間
集合擴(kuò)展工具類
Google-Guava Concurrent 包里的 Service 框架淺析
google Guava 包的 reflection 解析

緩存

范例


    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .removalListener(MY_LISTENER)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) throws AnyException {
                        return createExpensiveGraph(key);
                    }
            });

適用性

緩存在很多場(chǎng)景下都是相當(dāng)有用的。例如,計(jì)算或檢索一個(gè)值的代價(jià)很高,并且對(duì)同樣的輸入需要不止一次獲取值的時(shí)候,就應(yīng)當(dāng)考慮使用緩存。

Guava Cache 與 ConcurrentMap 很相似,但也不完全一樣。最基本的區(qū)別是 ConcurrentMap 會(huì)一直保存所有添加的元素,直到顯式地移除。相對(duì)地,Guava Cache 為了限制內(nèi)存占用,通常都設(shè)定為自動(dòng)回收元素。在某些場(chǎng)景下,盡管 LoadingCache 不回收元素,它也是很有用的,因?yàn)樗鼤?huì)自動(dòng)加載緩存。

通常來(lái)說(shuō),Guava Cache 適用于:

  • 你愿意消耗一些內(nèi)存空間來(lái)提升速度。
  • 你預(yù)料到某些鍵會(huì)被查詢一次以上。
  • 緩存中存放的數(shù)據(jù)總量不會(huì)超出內(nèi)存容量。(Guava Cache 是單個(gè)應(yīng)用運(yùn)行時(shí)的本地緩存。它不把數(shù)據(jù)存放到文件或外部服務(wù)器。如果這不符合你的需求,請(qǐng)嘗試 Memcached 這類工具)

如果你的場(chǎng)景符合上述的每一條,Guava Cache 就適合你。

如同范例代碼展示的一樣,Cache 實(shí)例通過(guò) CacheBuilder 生成器模式獲取,但是自定義你的緩存才是最有趣的部分。

:如果你不需要 Cache 中的特性,使用 ConcurrentHashMap 有更好的內(nèi)存效率——但 Cache 的大多數(shù)特性都很難基于舊有的 ConcurrentMap 復(fù)制,甚至根本不可能做到。

加載

在使用緩存前,首先問(wèn)自己一個(gè)問(wèn)題:有沒(méi)有合理的默認(rèn)方法來(lái)加載或計(jì)算與鍵關(guān)聯(lián)的值?如果有的話,你應(yīng)當(dāng)使用 CacheLoader。如果沒(méi)有,或者你想要覆蓋默認(rèn)的加載運(yùn)算,同時(shí)保留"獲取緩存-如果沒(méi)有-則計(jì)算"[get-if-absent-compute]的原子語(yǔ)義,你應(yīng)該在調(diào)用 get 時(shí)傳入一個(gè) Callable 實(shí)例。緩存元素也可以通過(guò) Cache.put 方法直接插入,但自動(dòng)加載是首選的,因?yàn)樗梢愿菀椎赝茢嗨芯彺鎯?nèi)容的一致性。

CacheLoader

LoadingCache 是附帶 CacheLoader 構(gòu)建而成的緩存實(shí)現(xiàn)。創(chuàng)建自己的 CacheLoader 通常只需要簡(jiǎn)單地實(shí)現(xiàn) V load(K key) throws Exception 方法。例如,你可以用下面的代碼構(gòu)建 LoadingCache:


    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) throws AnyException {
                        return createExpensiveGraph(key);
                    }
                });

    ...
    try {
        return graphs.get(key);
    } catch (ExecutionException e) {
        throw new OtherException(e.getCause());
    }

從 LoadingCache 查詢的正規(guī)方式是使用 get(K)方法。這個(gè)方法要么返回已經(jīng)緩存的值,要么使用 CacheLoader 向緩存原子地加載新值。由于 CacheLoader 可能拋出異常,LoadingCache.get(K)也聲明為拋出 ExecutionException 異常。如果你定義的 CacheLoader 沒(méi)有聲明任何檢查型異常,則可以通過(guò) getUnchecked(K)查找緩存;但必須注意,一旦 CacheLoader 聲明了檢查型異常,就不可以調(diào)用 getUnchecked(K)。


    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
            .expireAfterAccess(10, TimeUnit.MINUTES)
            .build(
                new CacheLoader<Key, Graph>() {
                    public Graph load(Key key) { // no checked exception
                        return createExpensiveGraph(key);
                    }
                });

    ...
    return graphs.getUnchecked(key);

getAll(Iterable<? extends K>)方法用來(lái)執(zhí)行批量查詢。默認(rèn)情況下,對(duì)每個(gè)不在緩存中的鍵,getAll 方法會(huì)單獨(dú)調(diào)用 CacheLoader.load 來(lái)加載緩存項(xiàng)。如果批量的加載比多個(gè)單獨(dú)加載更高效,你可以重載 CacheLoader.loadAll 來(lái)利用這一點(diǎn)。getAll(Iterable)的性能也會(huì)相應(yīng)提升。

注:CacheLoader.loadAll 的實(shí)現(xiàn)可以為沒(méi)有明確請(qǐng)求的鍵加載緩存值。例如,為某組中的任意鍵計(jì)算值時(shí),能夠獲取該組中的所有鍵值,loadAll 方法就可以實(shí)現(xiàn)為在同一時(shí)間獲取該組的其他鍵值。校注:getAll(Iterable<? extends K>)方法會(huì)調(diào)用 loadAll,但會(huì)篩選結(jié)果,只會(huì)返回請(qǐng)求的鍵值對(duì)。

Callable

所有類型的 Guava Cache,不管有沒(méi)有自動(dòng)加載功能,都支持 get(K, Callable)方法。這個(gè)方法返回緩存中相應(yīng)的值,或者用給定的 Callable 運(yùn)算并把結(jié)果加入到緩存中。在整個(gè)加載方法完成前,緩存項(xiàng)相關(guān)的可觀察狀態(tài)都不會(huì)更改。這個(gè)方法簡(jiǎn)便地實(shí)現(xiàn)了模式"如果有緩存則返回;否則運(yùn)算、緩存、然后返回"。


    Cache<Key, Graph> cache = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .build(); // look Ma, no CacheLoader
    ...
    try {
        // If the key wasn't in the "easy to compute" group, we need to
        // do things the hard way.
        cache.get(key, new Callable<Key, Graph>() {
            @Override
            public Value call() throws AnyException {
                return doThingsTheHardWay(key);
            }
        });
    } catch (ExecutionException e) {
        throw new OtherException(e.getCause());
    }

顯式插入

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

緩存回收

一個(gè)殘酷的現(xiàn)實(shí)是,我們幾乎一定沒(méi)有足夠的內(nèi)存緩存所有數(shù)據(jù)。你你必須決定:什么時(shí)候某個(gè)緩存項(xiàng)就不值得保留了?Guava Cache 提供了三種基本的緩存回收方式:基于容量回收、定時(shí)回收和基于引用回收。

基于容量的回收(size-based eviction)

如果要規(guī)定緩存項(xiàng)的數(shù)目不超過(guò)固定值,只需使用 CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒(méi)有使用或總體上很少使用的緩存項(xiàng)?!妫涸诰彺骓?xiàng)的數(shù)目達(dá)到限定值之前,緩存就可能進(jìn)行回收操作——通常來(lái)說(shuō),這種情況發(fā)生在緩存項(xiàng)的數(shù)目逼近限定值時(shí)。

另外,不同的緩存項(xiàng)有不同的“權(quán)重”(weights)——例如,如果你的緩存值,占據(jù)完全不同的內(nèi)存空間,你可以使用 CacheBuilder.weigher(Weigher)指定一個(gè)權(quán)重函數(shù),并且用 CacheBuilder.maximumWeight(long)指定最大總重。在權(quán)重限定場(chǎng)景中,除了要注意回收也是在重量逼近限定值時(shí)就進(jìn)行了,還要知道重量是在緩存創(chuàng)建時(shí)計(jì)算的,因此要考慮重量計(jì)算的復(fù)雜度。


    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .maximumWeight(100000)
        .weigher(new Weigher<Key, Graph>() {
            public int weigh(Key k, Graph g) {
                return g.vertices().size();
            }
        })
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) { // no checked exception
                    return createExpensiveGraph(key);
                }
            });

定時(shí)回收(Timed Eviction)

CacheBuilder 提供兩種定時(shí)回收的方法:

  • expireAfterAccess(long, TimeUnit):緩存項(xiàng)在給定時(shí)間內(nèi)沒(méi)有被讀/寫訪問(wèn),則回收。請(qǐng)注意這種緩存的回收順序和基于大小回收一樣。
  • expireAfterWrite(long, TimeUnit):緩存項(xiàng)在給定時(shí)間內(nèi)沒(méi)有被寫訪問(wèn)(創(chuàng)建或覆蓋),則回收。如果認(rèn)為緩存數(shù)據(jù)總是在固定時(shí)候后變得陳舊不可用,這種回收方式是可取的。

如下文所討論,定時(shí)回收周期性地在寫操作中執(zhí)行,偶爾在讀操作中執(zhí)行。

測(cè)試定時(shí)回收

對(duì)定時(shí)回收進(jìn)行測(cè)試時(shí),不一定非得花費(fèi)兩秒鐘去測(cè)試兩秒的過(guò)期。你可以使用 Ticker 接口和 CacheBuilder.ticker(Ticker)方法在緩存中自定義一個(gè)時(shí)間源,而不是非得用系統(tǒng)時(shí)鐘。

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

通過(guò)使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache 可以把緩存設(shè)置為允許垃圾回收:

  • CacheBuilder.weakKeys():使用弱引用存儲(chǔ)鍵。當(dāng)鍵沒(méi)有其它(強(qiáng)或軟)引用時(shí),緩存項(xiàng)可以被垃圾回收。因?yàn)槔厥諆H依賴恒等式(==),使用弱引用鍵的緩存用==而不是 equals 比較鍵。
  • CacheBuilder.weakValues():使用弱引用存儲(chǔ)值。當(dāng)值沒(méi)有其它(強(qiáng)或軟)引用時(shí),緩存項(xiàng)可以被垃圾回收。因?yàn)槔厥諆H依賴恒等式(==),使用弱引用值的緩存用==而不是 equals 比較值。
  • CacheBuilder.softValues():使用軟引用存儲(chǔ)值。軟引用只有在響應(yīng)內(nèi)存需要時(shí),才按照全局最近最少使用的順序回收??紤]到使用軟引用的性能影響,我們通常建議使用更有性能預(yù)測(cè)性的緩存大小限定(見(jiàn)上文,基于容量回收)。使用軟引用值的緩存同樣用==而不是 equals 比較值。

顯式清除

任何時(shí)候,你都可以顯式地清除緩存項(xiàng),而不是等到它被回收:

移除監(jiān)聽器

通過(guò) CacheBuilder.removalListener(RemovalListener),你可以聲明一個(gè)監(jiān)聽器,以便緩存項(xiàng)被移除時(shí)做一些額外操作。緩存項(xiàng)被移除時(shí),RemovalListener 會(huì)獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。

請(qǐng)注意,RemovalListener 拋出的任何異常都會(huì)在記錄到日志后被丟棄[swallowed]。


    CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
    public DatabaseConnection load(Key key) throws Exception {
    return openConnection(key);
    }
    };

    RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
    public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
    DatabaseConnection conn = removal.getValue();
    conn.close(); // tear down properly
    }
    };

    return CacheBuilder.newBuilder()
    .expireAfterWrite(2, TimeUnit.MINUTES)
    .removalListener(removalListener)
    .build(loader);

警告:默認(rèn)情況下,監(jiān)聽器方法是在移除緩存時(shí)同步調(diào)用的。因?yàn)榫彺娴木S護(hù)和請(qǐng)求響應(yīng)通常是同時(shí)進(jìn)行的,代價(jià)高昂的監(jiān)聽器方法在同步模式下會(huì)拖慢正常的緩存請(qǐng)求。在這種情況下,你可以使用 RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽器裝飾為異步操作。

清理什么時(shí)候發(fā)生?

使用 CacheBuilder 構(gòu)建的緩存不會(huì)"自動(dòng)"執(zhí)行清理和回收工作,也不會(huì)在某個(gè)緩存項(xiàng)過(guò)期后馬上清理,也沒(méi)有諸如此類的清理機(jī)制。相反,它會(huì)在寫操作時(shí)順帶做少量的維護(hù)工作,或者偶爾在讀操作時(shí)做——如果寫操作實(shí)在太少的話。

這樣做的原因在于:如果要自動(dòng)地持續(xù)清理緩存,就必須有一個(gè)線程,這個(gè)線程會(huì)和用戶操作競(jìng)爭(zhēng)共享鎖。此外,某些環(huán)境下線程創(chuàng)建可能受限制,這樣 CacheBuilder 就不可用了。

相反,我們把選擇權(quán)交到你手里。如果你的緩存是高吞吐的,那就無(wú)需擔(dān)心緩存的維護(hù)和清理等工作。如果你的 緩存只會(huì)偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創(chuàng)建自己的維護(hù)線程,以固定的時(shí)間間隔調(diào)用 Cache.cleanUp()。ScheduledExecutorService 可以幫助你很好地實(shí)現(xiàn)這樣的定時(shí)調(diào)度。

刷新

刷新和回收不太一樣。正如 LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個(gè)過(guò)程可以是異步的。在刷新操作進(jìn)行時(shí),緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。

如果刷新過(guò)程拋出異常,緩存將保留舊值,而異常會(huì)在記錄到日志后被丟棄[swallowed]。

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


    //有些鍵不需要刷新,并且我們希望刷新是異步完成的
    LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .refreshAfterWrite(1, TimeUnit.MINUTES)
        .build(
            new CacheLoader<Key, Graph>() {
                public Graph load(Key key) { // no checked exception
                    return getGraphFromDatabase(key);
                }

                public ListenableFuture<Key, Graph> reload(final Key key, Graph prevGraph) {
                    if (neverNeedsRefresh(key)) {
                        return Futures.immediateFuture(prevGraph);
                    }else{
                        // asynchronous!
                        ListenableFutureTask<Key, Graph> task=ListenableFutureTask.create(new Callable<Key, Graph>() {
                            public Graph call() {
                                return getGraphFromDatabase(key);
                            }
                        });
                        executor.execute(task);
                        return task;
                    }
                }
            });

CacheBuilder.refreshAfterWrite(long, TimeUnit)可以為緩存增加自動(dòng)定時(shí)刷新功能。和 expireAfterWrite 相反,refreshAfterWrite 通過(guò)定時(shí)刷新可以讓緩存項(xiàng)保持可用,但請(qǐng)注意:緩存項(xiàng)只有在被檢索時(shí)才會(huì)真正刷新(如果 CacheLoader.refresh 實(shí)現(xiàn)為異步,那么檢索不會(huì)被刷新拖慢)。因此,如果你在緩存上同時(shí)聲明 expireAfterWrite 和 refreshAfterWrite,緩存并不會(huì)因?yàn)樗⑿旅つ康囟〞r(shí)重置,如果緩存項(xiàng)沒(méi)有被檢索,那刷新就不會(huì)真的發(fā)生,緩存項(xiàng)在過(guò)期時(shí)間后也變得可以回收。4

其他特性

統(tǒng)計(jì)

CacheBuilder.recordStats()用來(lái)開啟 Guava Cache 的統(tǒng)計(jì)功能。統(tǒng)計(jì)打開后,Cache.stats()方法會(huì)返回 CacheStats 對(duì)象以提供如下統(tǒng)計(jì)信息:

此外,還有其他很多統(tǒng)計(jì)信息。這些統(tǒng)計(jì)信息對(duì)于調(diào)整緩存設(shè)置是至關(guān)重要的,在性能要求高的應(yīng)用中我們建議密切關(guān)注這些數(shù)據(jù)。

asMap 視圖

asMap 視圖提供了緩存的 ConcurrentMap 形式,但 asMap 視圖與緩存的交互需要注意:

  • cache.asMap()包含當(dāng)前所有加載到緩存的項(xiàng)。因此相應(yīng)地,cache.asMap().keySet()包含當(dāng)前所有已加載鍵;
  • asMap().get(key)實(shí)質(zhì)上等同于 cache.getIfPresent(key),而且不會(huì)引起緩存項(xiàng)的加載。這和 Map 的語(yǔ)義約定一致。
  • 所有讀寫操作都會(huì)重置相關(guān)緩存項(xiàng)的訪問(wèn)時(shí)間,包括 Cache.asMap().get(Object)方法和 Cache.asMap().put(K, V)方法,但不包括 Cache.asMap().containsKey(Object)方法,也不包括在 Cache.asMap()的集合視圖上的操作。比如,遍歷 Cache.asMap().entrySet()不會(huì)重置緩存項(xiàng)的讀取時(shí)間。

中斷

緩存加載方法(如 Cache.get)不會(huì)拋出 InterruptedException。我們也可以讓這些方法支持 InterruptedException,但這種支持注定是不完備的,并且會(huì)增加所有使用者的成本,而只有少數(shù)使用者實(shí)際獲益。詳情請(qǐng)繼續(xù)閱讀。

Cache.get 請(qǐng)求到未緩存的值時(shí)會(huì)遇到兩種情況:當(dāng)前線程加載值;或等待另一個(gè)正在加載值的線程。這兩種情況下的中斷是不一樣的。等待另一個(gè)正在加載值的線程屬于較簡(jiǎn)單的情況:使用可中斷的等待就實(shí)現(xiàn)了中斷支持;但當(dāng)前線程加載值的情況就比較復(fù)雜了:因?yàn)榧虞d值的 CacheLoader 是由用戶提供的,如果它是可中斷的,那我們也可以實(shí)現(xiàn)支持中斷,否則我們也無(wú)能為力。

如果用戶提供的 CacheLoader 是可中斷的,為什么不讓 Cache.get 也支持中斷?從某種意義上說(shuō),其實(shí)是支持的:如果 CacheLoader 拋出 InterruptedException,Cache.get 將立刻返回(就和其他異常情況一樣);此外,在加載緩存值的線程中,Cache.get 捕捉到 InterruptedException 后將恢復(fù)中斷,而其他線程中 InterruptedException 則被包裝成了 ExecutionException。

原則上,我們可以拆除包裝,把 ExecutionException 變?yōu)?InterruptedException,但這會(huì)讓所有的 LoadingCache 使用者都要處理中斷異常,即使他們提供的 CacheLoader 不是可中斷的。如果你考慮到所有非加載線程的等待仍可以被中斷,這種做法也許是值得的。但許多緩存只在單線程中使用,它們的用戶仍然必須捕捉不可能拋出的 InterruptedException 異常。即使是那些跨線程共享緩存的用戶,也只是有時(shí)候能中斷他們的 get 調(diào)用,取決于那個(gè)線程先發(fā)出請(qǐng)求。

對(duì)于這個(gè)決定,我們的指導(dǎo)原則是讓緩存始終表現(xiàn)得好像是在當(dāng)前線程加載值。這個(gè)原則讓使用緩存或每次都計(jì)算值可以簡(jiǎn)單地相互切換。如果老代碼(加載值的代碼)是不可中斷的,那么新代碼(使用緩存加載值的代碼)多半也應(yīng)該是不可中斷的。

如上所述,Guava Cache 在某種意義上支持中斷。另一個(gè)意義上說(shuō),Guava Cache 不支持中斷,這使得 LoadingCache 成了一個(gè)有漏洞的抽象:當(dāng)加載過(guò)程被中斷了,就當(dāng)作其他異常一樣處理,這在大多數(shù)情況下是可以的;但如果多個(gè)線程在等待加載同一個(gè)緩存項(xiàng),即使加載線程被中斷了,它也不應(yīng)該讓其他線程都失?。ú东@到包裝在 ExecutionException 里的 InterruptedException),正確的行為是讓剩余的某個(gè)線程重試加載。為此,我們記錄了一個(gè) bug。然而,與其冒著風(fēng)險(xiǎn)修復(fù)這個(gè) bug,我們可能會(huì)花更多的精力去實(shí)現(xiàn)另一個(gè)建議 AsyncLoadingCache,這個(gè)實(shí)現(xiàn)會(huì)返回一個(gè)有正確中斷行為的 Future 對(duì)象。

上一篇:常見(jiàn) Object 方法下一篇:I/O