轉(zhuǎn)自:http://ifeve.com/google-guava-cachesexplained/
范例
| 01 | LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
| 02 | ????????.maximumSize(1000) |
| 03 | ????????.expireAfterWrite(10, TimeUnit.MINUTES) |
| 04 | ????????.removalListener(MY_LISTENER) |
| 06 | ????????????new?CacheLoader<Key, Graph>() { |
| 07 | ????????????????public?Graph load(Key key)?throws?AnyException { |
| 08 | ????????????????????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:
| 01 | LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
| 02 | ????????.maximumSize(1000) |
| 04 | ????????????new?CacheLoader<Key, Graph>() { |
| 05 | ????????????????public?Graph load(Key key)?throws?AnyException { |
| 06 | ????????????????????return?createExpensiveGraph(key); |
| 12 | ????return?graphs.get(key); |
| 13 | }?catch?(ExecutionException e) { |
| 14 | ????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)。
| 01 | LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
| 02 | ????????.expireAfterAccess(10, TimeUnit.MINUTES) |
| 04 | ????????????new?CacheLoader<Key, Graph>() { |
| 05 | ????????????????public?Graph load(Key key) {?// no checked exception |
| 06 | ????????????????????return?createExpensiveGraph(key); |
| 11 | 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<V>)方法。這個(gè)方法返回緩存中相應(yīng)的值,或者用給定的Callable運(yùn)算并把結(jié)果加入到緩存中。在整個(gè)加載方法完成前,緩存項(xiàng)相關(guān)的可觀察狀態(tài)都不會(huì)更改。這個(gè)方法簡(jiǎn)便地實(shí)現(xiàn)了模式"如果有緩存則返回;否則運(yùn)算、緩存、然后返回"。
| 01 | Cache<Key, Graph> cache = CacheBuilder.newBuilder() |
| 02 | ????????.maximumSize(1000) |
| 03 | ????????.build();?// look Ma, no CacheLoader |
| 06 | ????// If the key wasn't in the "easy to compute" group, we need to |
| 07 | ????// do things the hard way. |
| 08 | ????cache.get(key,?new?Callable<Key, Graph>() { |
| 10 | ????????public?Value call()?throws?AnyException { |
| 11 | ????????????return?doThingsTheHardWay(key); |
| 14 | }?catch?(ExecutionException e) { |
| 15 | ????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<V>) 應(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ù)雜度。
| 01 | LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
| 02 | ????????.maximumWeight(100000) |
| 03 | ????????.weigher(new?Weigher<Key, Graph>() { |
| 04 | ????????????public?int?weigh(Key k, Graph g) { |
| 05 | ????????????????return?g.vertices().size(); |
| 09 | ????????????new?CacheLoader<Key, Graph>() { |
| 10 | ????????????????public?Graph load(Key key) {?// no checked exception |
| 11 | ????????????????????return?createExpensiveGraph(key); |
定時(shí)回收(Timed Eviction)
CacheBuilder提供兩種定時(shí)回收的方法:
- expireAfterAccess(long, TimeUnit):緩存項(xiàng)在給定時(shí)間內(nèi)沒(méi)有被讀/寫(xiě)訪問(wèn),則回收。請(qǐng)注意這種緩存的回收順序和基于大小回收一樣。
- expireAfterWrite(long, TimeUnit):緩存項(xiàng)在給定時(shí)間內(nèi)沒(méi)有被寫(xiě)訪問(wèn)(創(chuàng)建或覆蓋),則回收。如果認(rèn)為緩存數(shù)據(jù)總是在固定時(shí)候后變得陳舊不可用,這種回收方式是可取的。
如下文所討論,定時(shí)回收周期性地在寫(xiě)操作中執(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),而不是等到它被回收:
- 個(gè)別清除:Cache.invalidate(key)
- 批量清除:Cache.invalidateAll(keys)
- 清除所有緩存項(xiàng):Cache.invalidateAll()
移除監(jiān)聽(tīng)器(默認(rèn)是同步執(zhí)行的)
通過(guò)CacheBuilder.removalListener(RemovalListener),你可以聲明一個(gè)監(jiān)聽(tīng)器,以便緩存項(xiàng)被移除時(shí)做一些額外操作。緩存項(xiàng)被移除時(shí),RemovalListener會(huì)獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。
請(qǐng)注意,RemovalListener拋出的任何異常都會(huì)在記錄到日志后被丟棄[swallowed]。
| 01 | CacheLoader<Key, DatabaseConnection> loader =?new?CacheLoader<Key, DatabaseConnection> () { |
| 02 | ????public?DatabaseConnection load(Key key)?throws?Exception { |
| 03 | ????????return?openConnection(key); |
| 07 | RemovalListener<Key, DatabaseConnection> removalListener =?new?RemovalListener<Key, DatabaseConnection>() { |
| 08 | ????public?void?onRemoval(RemovalNotification<Key, DatabaseConnection> removal) { |
| 09 | ????????DatabaseConnection conn = removal.getValue(); |
| 10 | ????????conn.close();?// tear down properly |
| 14 | return?CacheBuilder.newBuilder() |
| 15 | ????.expireAfterWrite(2, TimeUnit.MINUTES) |
| 16 | ????.removalListener(removalListener) |
警告:默認(rèn)情況下,監(jiān)聽(tīng)器方法是在移除緩存時(shí)同步調(diào)用的。因?yàn)榫彺娴木S護(hù)和請(qǐng)求響應(yīng)通常是同時(shí)進(jìn)行的,代價(jià)高昂的監(jiān)聽(tīng)器方法在同步模式下會(huì)拖慢正常的緩存請(qǐng)求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽(tīng)器裝飾為異步操作。
清理什么時(shí)候發(fā)生?
使用CacheBuilder構(gòu)建的緩存不會(huì)"自動(dòng)"執(zhí)行清理和回收工作,也不會(huì)在某個(gè)緩存項(xiàng)過(guò)期后馬上清理,也沒(méi)有諸如此類的清理機(jī)制。相反,它會(huì)在寫(xiě)操作時(shí)順帶做少量的維護(hù)工作,或者偶爾在讀操作時(shí)做——如果寫(xiě)操作實(shí)在太少的話。
這樣做的原因在于:如果要自動(dòng)地持續(xù)清理緩存,就必須有一個(gè)線程,這個(gè)線程會(huì)和用戶操作競(jìng)爭(zhēng)共享鎖。此外,某些環(huán)境下線程創(chuàng)建可能受限制,這樣CacheBuilder就不可用了。
相反,我們把選擇權(quán)交到你手里。如果你的緩存是高吞吐的,那就無(wú)需擔(dān)心緩存的維護(hù)和清理等工作。如果你的 緩存只會(huì)偶爾有寫(xiě)操作,而你又不想清理工作阻礙了讀操作,那么可以創(chuàng)建自己的維護(hù)線程,以固定的時(shí)間間隔調(diào)用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實(shí)現(xiàn)這樣的定時(shí)調(diào)度。
刷新(異步,而且刷新的動(dòng)作只在讀的時(shí)候才進(jìn)行刷新,所以不會(huì)拖慢檢索的速度)
刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個(gè)過(guò)程可以是異步的。在刷新操作進(jìn)行時(shí),緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。
如果刷新過(guò)程拋出異常,緩存將保留舊值,而異常會(huì)在記錄到日志后被丟棄[swallowed]。
重載CacheLoader.reload(K, V)可以擴(kuò)展刷新時(shí)的行為,這個(gè)方法允許開(kāi)發(fā)者在計(jì)算新值時(shí)使用舊的值。
| 01 | //有些鍵不需要刷新,并且我們希望刷新是異步完成的 |
| 02 | LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() |
| 03 | ????????.maximumSize(1000) |
| 04 | ????????.refreshAfterWrite(1, TimeUnit.MINUTES) |
| 06 | ????????????new?CacheLoader<Key, Graph>() { |
| 07 | ????????????????public?Graph load(Key key) {?// no checked exception |
| 08 | ????????????????????return?getGraphFromDatabase(key); |
| 11 | ????????????????public?ListenableFuture<Key, Graph> reload(final?Key key, Graph prevGraph) { |
| 12 | ????????????????????if?(neverNeedsRefresh(key)) { |
| 13 | ????????????????????????return?Futures.immediateFuture(prevGraph); |
| 14 | ????????????????????}else{ |
| 15 | ????????????????????????// asynchronous! |
| 16 | ????????????????????????ListenableFutureTask<Key, Graph> task=ListenableFutureTask.create(new?Callable<Key, Graph>() { |
| 17 | ????????????????????????????public?Graph call() { |
| 18 | ????????????????????????????????return?getGraphFromDatabase(key); |
| 19 | ????????????????????????????} |
| 20 | ????????????????????????}); |
| 21 | ????????????????????????executor.execute(task); |
| 22 | ????????????????????????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í)間后也變得可以回收。
其他特性
統(tǒng)計(jì)
CacheBuilder.recordStats()用來(lái)開(kāi)啟Guava Cache的統(tǒng)計(jì)功能。統(tǒng)計(jì)打開(kāi)后,Cache.stats()方法會(huì)返回CacheStats對(duì)象以提供如下統(tǒng)計(jì)信息:
- averageLoadPenalty():加載新值的平均時(shí)間,單位為納秒;
- evictionCount():緩存項(xiàng)被回收的總數(shù),不包括顯式清除。
此外,還有其他很多統(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ǔ)義約定一致。
- 所有讀寫(xiě)操作都會(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)镮nterruptedException,但這會(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ì)象。
?
轉(zhuǎn)載于:https://www.cnblogs.com/YDDMAX/p/4976132.html
總結(jié)
以上是生活随笔為你收集整理的guava之cache的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
如果覺(jué)得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。