Guava cacha 機制及源碼分析

一、ehcahce 何時用比較好;
二、問題:當有個消息的key不在guava裏面的話,若是大量的消息過來,會同時請求數據庫嗎?仍是隻有一個請求數據庫,其餘的等待第一個把數據從DB加載到Guava中html

回答:是的,其餘的都會等待load,直到數據加載完畢;
二、recency queue 幹嗎用的:java

目前沒看出來,可是應該是爲了LRU隊列也就是快速刪除算法,由於recency queue的隊列,若是讀的話,會往recency queue和 access queue中寫入數據,若是寫的話,首先要清空recency queue隊列,而後在recency queue中,而後再在access queue中寫入隊列;因此應該會爲了快速刪除過時數據準備的queue:mysql

 

背景:

目前在網安部項目中,會接收到LBS消息 高峯期的QPS大約爲5000,目前是直接經過LBS消息的訂單ID查詢 查詢訂單接口的數據,因爲涉及到上游部署,或者網絡抖動的問題,當上遊積壓時,訂單常常會報警。所以考慮對緩存作一次調研。git

在多線程高併發場景中每每是離不開cache的,須要根據不一樣的應用場景來須要選擇不一樣的cache,好比分佈式緩存如Redis、memcached,還有本地(進程內)緩存如ehcache、GuavaCachegithub

   緩存分爲本地緩存和分佈式緩存, 爲何要使用本地緩存呢?由於本地緩存比IO更高效,比分佈式緩存更穩定。。redis

   分佈式緩存主要爲redis或memcached之類的稱爲分佈式緩存,算法

優勢:sql

    Redis 容量大,能夠持久化,能夠實現分佈式的緩存,能夠處理每秒百萬級的併發,是專業的緩存服務,數據庫

    redis可單獨部署,多個項目之間能夠共享,本地內存沒法共享;apache

在多實例的狀況下,各實例共用一份緩存數據,緩存具備一致性。

缺點:

須要保持redis或memcached服務的高可用,整個程序架構上較爲複雜,硬件成本較高

本地緩存主要爲Ecache和 guava Cache

區別:

  1. Ehcache支持持久化到本地磁盤,Guava不能夠;
  2. Ehcache有現成的集羣解決方案,Guava沒有。不過我的感受比較雞肋,對JVM級別的緩存來說過重了;
  3. Ehcache jar包龐大,Guava Cache只是Guava jar包中的工具之一,並且後者遠遠小於Ehcache;
  4. 兩種緩存當緩存過時或者沒有命中的時候均可以經過load接口重載數據,調用方式略有不一樣。二者的主要區別是Ehcache的緩存load的時候,容許用戶返回null,而Guava Cache則不容許返回爲null,由於Guava Cache是根據value的值是否爲null來判斷是否須要load,因此不容許返回爲null,可是使用的時候能夠使用空對象替換。不容許返回null是一個很好的考慮;
  5. Ehcache有內存佔用大小統計,Guava Cache沒有,須要本身開發;
  6. Ehcache在put緩存的時候,對K、V都作了包裝,對GC有必定影響。

適用Ehcache的狀況

  1. 須要使用持久化功能須要,緩存穩定,以避免持久化的數據不許確影響結果。
  2. 有集羣解決方案。

適用Guava cache的狀況
Guava cache說簡單點就是一個支持LRU的ConCurrentHashMap,它沒有Ehcache那麼多的各類特性,只是提供了增、刪、改、查、刷新規則和時效規則設定等最基本的元素。作一個jar包中的一個功能之一,Guava cache極度簡潔並能知足覺大部分人的要求。 

Guava Cache 優勢:

  1. 採用鎖分段技術,鎖粒度減少,加大併發。
  2. API優雅,簡單可用,支持多種回收方式。
  3. 自帶統計功能。
  4. Guava Cache的超時機制不是精確的;    

缺點:

  1. 受內存大小限制不能存儲太多數據
  2. 單JVM有效,非分佈式緩存。多臺服務可能有不一樣效果。
  3. 不能持久化本地緩存;

使用場景

願意花費一部份內存來提升速度 -- 以空間換時間
期待有些關鍵字會被屢次查詢 -- 熱點數據
不須要持久化   

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

總結

Ehcache有着全面的緩存特性,可是略重。Guava cache有最基本的緩存特性,很輕。 

兩種類型都是成熟的緩存框架,因爲不須要保存到本地磁盤 考慮到Ehcahce 比較重,而Guava 比較輕量,考慮使用Guava   

2、Guava Cache緩存數據結構

Guava工程包含了若干被Google的 Java項目普遍依賴 的核心庫;Google Guava Cache是一種很是優秀本地緩存解決方案,提供了基於容量,時間和引用的緩存回收方式。

   Guava Cache 其核心數據結構大致上和 ConcurrentHashMap 一致,具體細節上會有些區別。功能上,ConcurrentMap會一直保存全部添加的元素,直到顯式地移除。相對地, Guava Cache 爲了限制內存佔用,一般都設定爲自動回收元素。在某些場景下,儘管它不回收元素,也是頗有用的,由於它會自動加載緩存。 

   Guava Cache與java1.7的ConcurrentMap很類似,但也不徹底同樣。最基本的區別是ConcurrentMap會一直保存全部添加的元素,直到顯式地移除。相對地,Guava Cache爲了限制內存佔用,一般都設定爲自動回收元素。 

在這裏就會涉及到segement的概念了,咱們先把關係理清楚,首先看ConcurrentHashMap的圖示,這樣有助於咱們理解:

  

 Guava Cache中的核心類,重點了解。

2.1 LocalCache數據結構

ocalCache爲Guava Cache的核心類,先看一個該類的數據結構:  LocalCache的數據結構與ConcurrentHashMap很類似,都由多個segment組成,且各segment相對獨立,互不影響,因此能支持並行操做。每一個segment由一個table和若干隊列組成。緩存數據存儲在table中,其類型爲AtomicReferenceArray。以下圖所示 一個table 還有 5個queue;

 

對於每個Segment 放大以下:包含了一個table 和5個隊列;

 

 

LocalCache相似ConcurrentHashMap採用了分段策略,經過減少鎖的粒度來提升併發,LocalCache中數據存儲在Segment[]中,每一個segment又包含5個隊列和一個table

緩存核心類LocalCache,包含了Segment以下所示:

@GwtCompatible(emulated = true) class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> { final Segment<K, V>[] segments; @Nullable final CacheLoader<? super K, V> defaultLoader;

 內部類Segment與jdk1.7及之前的ConcurrentHashMap很是類似,都繼承於ReetrantLock, 

static class Segment<K, V> extends ReentrantLock { @Weak final LocalCache<K, V> map; final ReferenceQueue<K> keyReferenceQueue;//key引用隊列 final ReferenceQueue<V> valueReferenceQueue;//value引用隊列 final Queue<ReferenceEntry<K, V>> recencyQueue;// LRU隊列 @GuardedBy("this") final Queue<ReferenceEntry<K, V>> writeQueue; // 寫隊列 @GuardedBy("this") final Queue<ReferenceEntry<K, V>> accessQueue; //訪問隊列

 

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)); keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null; valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null; 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(); accessQueue = map.usesAccessQueue() ? new AccessQueue<K, V>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue(); } 

 

裏面有一個table,五個queue,分別表示:讀,寫,最近使用,key,value 的queue;

裏面涉及到引用的使用:  

在 JDK1.2 以前這點設計的很是簡單:一個對象的狀態只有引用沒被引用兩種區別。 

所以 1.2 以後新增了四種狀態用於更細粒度的劃分引用關係:

  • 強引用(Strong Reference):這種對象最爲常見,好比 `A a = new A();`這就是典型的強引用;這樣的強引用關係是不能被垃圾回收的。

  • 軟引用(Soft Reference):這樣的引用代表一些有用但不是必要的對象,在將發生垃圾回收以前是須要將這樣的對象再次回收。

  • 弱引用(Weak Reference):這是一種比軟引用還弱的引用關係,也是存放非必須的對象。當垃圾回收時,不管當前內存是否足夠,這樣的對象都會被回收。

  • 虛引用(Phantom Reference):這是一種最弱的引用關係,甚至無法經過引用來獲取對象,它惟一的做用就是在被回收時能夠得到通知。  

 

2.2 GuavaCache的 5個Queue的做用和 table的結構

 

基於引用的Entry,其實現類有弱引用Entry,強引用Entry等

ReferenceQueue<K> keyReferenceQueue;

已經被GC,須要內部清理的鍵引用隊列。

ReferenceQueue<V> valueReferenceQueue;

已經被GC,須要內部清理的值引用隊列。

Queue<ReferenceEntry<K, V>> recencyQueue;

記錄升級可訪問列表清單時的entries,當segment上達到臨界值或發生寫操做時該隊列會被清空。

Queue<ReferenceEntry<K, V>> writeQueue;

按照寫入時間進行排序的元素隊列,寫入一個元素時會把它加入到隊列尾部。

Queue<ReferenceEntry<K, V>> accessQueue;

按照訪問時間進行排序的元素隊列,訪問(包括寫入)一個元素時會把它加入到隊列尾部。 

table的數據結構 類型爲:AtomicReferenceArray 

Segment繼承於ReetrantLock,減少鎖粒度,提升併發效率。

AtomicReferenceArray<ReferenceEntry<K, V>> table;

相似於HasmMap中的table同樣,至關於entry的容器。

ReferenceEntry<K, V> referenceEntry; 

(a) 這5個隊列,實現了豐富的本地緩存方案。 

  這些隊列,前2個是key、value引用隊列用以加速GC回收,基於引用回收很好的利用了Java虛擬機的垃圾回收機制。

  後3個隊列記錄用戶的寫記錄、訪問記錄、高頻訪問順序隊列用以實現LRU算法。基於容量的方式內部實現採用LRU算法,

(b) AtomicReferenceArray是JUC包下的Doug Lea老李頭設計的類:一組對象引用,其中元素支持原子性更新, 這個table是自定義的一種類數組的結構,每一個元素都包含一個ReferenceEntry<k,v>鏈表,指向next entry。  採用了ReferenceEntry的方式,引用數據存儲接口,默認強引用,對應的類圖爲: 

問題1:爲什麼LRU隊列使用了 recencyQueue 隊列 由於已經有了 accessQueue 

keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null; valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null; 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(); accessQueue = map.usesAccessQueue() ? new AccessQueue<K, V>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue();

 

緣由: 由於accessQueue是非線程安全的,get的時候使用併發工具ConcurrentLinkedQueue隊列添加entry,而不用lock(),

ConcurrentLinkedQueue是一個基於連接節點的無界非阻塞線程安全隊列,其底層數據結構是使用單向鏈表實現,它採用先進先出的規則對節點進行排序,

recencyQueue 啓用條件和accessQueue同樣。每次訪問操做都會將該entry加入到隊列尾部,並更新accessTime。若是遇到寫入操做,則將該隊列內容排幹,若是accessQueue隊列中持有該這些 entry,而後將這些entry add到accessQueue隊列。注意,由於accessQueue是非線程安全的,因此若是每次訪問entry時就將該entry加入到accessQueue隊列中,就會致使併發問題。因此這裏每次訪問先將entry臨時加入到併發安全的ConcurrentLinkedQueue隊列中,也就是recencyQueue中。在寫入的時候經過加鎖的方式,將recencyQueue中的數據添加到accessQueue隊列中。 如此看來,recencyQueue是爲 accessQueue服務的。以便高效的實現expireAfterAccess功能。 關於使用recencyQueue的好處:get的時候使用併發工具ConcurrentLinkedQueue隊列添加entry,而不用lock(),一個是無阻賽鎖一個是阻塞鎖,

參考:guava cache 原理 

 
Cache相似於Map,它是存儲鍵值對的集合,然而它和Map不一樣的是它還須要處理evict、expire、dynamic load等邏輯,須要一些額外信息來實現這些操做。在面向對象思想中,常用類對一些關聯性比較強的數據作封裝,同時把操做這些數據相關的操做放到該類中。於是Guava Cache使用ReferenceEntry接口來封裝一個鍵值對,而用ValueReference來封裝Value值。這裏之因此用Reference命令,是由於Guava Cache要支持WeakReference Key和SoftReference、WeakReference value。

ValueReference
對於ValueReference,由於Guava Cache支持強引用的Value、SoftReference Value以及WeakReference Value,

於是它對應三個實現類:StrongValueReference、SoftValueReference、WeakValueReference。

爲了支持動態加載機制,它還有一個LoadingValueReference,在須要動態加載一個key的值時,先把該值封裝在LoadingValueReference中,以表達該key對應的值已經在加載了,若是其餘線程也要查詢該key對應的值,就能獲得該引用,而且等待改值加載完成,從而保證該值只被加載一次(能夠在evict之後從新加載)。在該只加載完成後,將LoadingValueReference替換成其餘ValueReference類型。對新建立的LoadingValueReference,因爲其內部oldValue的初始值是UNSET,它isActive爲false,isLoading爲false,於是此時的LoadingValueReference的isActive爲false,可是isLoading爲true。每一個ValueReference都紀錄了weight值,所謂weight從字面上理解是「該值的重量」,它由Weighter接口計算而得。weight在Guava Cache中由兩個用途:1. 對weight值爲0時,在計算由於size limit而evict是忽略該Entry(它能夠經過其餘機制evict);2. 若是設置了maximumWeight值,則當Cache中weight和超過了該值時,就會引發evict操做。可是目前還不知道這個設計的用途。最後,Guava Cache還定義了Stength枚舉類型做爲ValueReference的factory類,它有三個枚舉值:Strong、Soft、Weak,這三個枚舉值分別建立各自的ValueReference,而且根據傳入的weight值是否爲1而決定是否要建立Weight版本的ValueReference。如下是ValueReference的類
這裏ValueReference之因此要有對ReferenceEntry的引用是由於在Value由於WeakReference、SoftReference被回收時,須要使用其key將對應的項從Segment的table中移除;copyFor()函數的存在是由於在expand(rehash)從新建立節點時,對WeakReference、SoftReference須要從新建立實例(我的感受是爲了保持對象狀態不會相互影響,可是不肯定是否還有其餘緣由),而對強引用來講,直接使用原來的值便可,這裏很好的展現了對彼變化的封裝思想;notifiyNewValue只用於LoadingValueReference,它的存在是爲了對LoadingValueReference來講能更加及時的獲得CacheLoader加載的值。

ReferenceEntry
ReferenceEntry是Guava Cache中對一個鍵值對節點的抽象。和ConcurrentHashMap同樣,Guava Cache由多個Segment組成,而每一個Segment包含一個ReferenceEntry數組,每一個ReferenceEntry數組項都是一條ReferenceEntry鏈。而且一個ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry數組項中組成的鏈,在一個Segment中,全部ReferenceEntry還組成access鏈(accessQueue)和write鏈(writeQueue),這兩條都是雙向鏈表,分別經過previousAccess、nextAccess和previousWrite、nextWrite字段連接而成。在對每一個節點的更新操做都會將該節點從新鏈到write鏈和access鏈末尾,而且更新其writeTime和accessTime字段,而沒找到一個節點,都會將該節點從新鏈到access鏈末尾,並更新其accessTime字段。這兩個雙向鏈表的存在都是爲了實現採用最近最少使用算法(LRU)的evict操做(expire、size limit引發的evict)。

Guava Cache中的ReferenceEntry能夠是強引用類型的key,也能夠WeakReference類型的key,爲了減小內存使用量,還能夠根據是否配置了expireAfterWrite、expireAfterAccess、maximumSize來決定是否須要write鏈和access鏈肯定要建立的具體Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。建立不一樣類型的ReferenceEntry由其枚舉工廠類EntryFactory來實現,它根據key的Strongth類型、是否使用accessQueue、是否使用writeQueue來決定不一樣的EntryFactry實例,並經過它建立相應的ReferenceEntry實例。ReferenceEntry類圖以下:

WriteQueue和AccessQueue
爲了實現最近最少使用算法,Guava Cache在Segment中添加了兩條鏈:write鏈(writeQueue)和access鏈(accessQueue),這兩條鏈都是一個雙向鏈表,經過ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue連接而成,可是以Queue的形式表達。WriteQueue和AccessQueue都是自定義了offer、add(直接調用offer)、remove、poll等操做的邏輯,對於offer(add)操做,若是是新加的節點,則直接加入到該鏈的結尾,若是是已存在的節點,則將該節點連接的鏈尾;對remove操做,直接從該鏈中移除該節點;對poll操做,將頭節點的下一個節點移除,並返回。

對於不須要維護WriteQueue和AccessQueue的配置(即沒有expire time或size limit的evict策略)來講,咱們能夠使用DISCARDING_QUEUE以節省內存:

 

2.3  Guava Cache數據結構大類

先看一下google cache 核心類以下:

  • CacheBuilder:類,緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。

    CacheBuilder在build方法中,會把前面設置的參數,所有傳遞給LocalCache,它本身實際不參與任何計算。採用構造器模式(Builder)使得初始化參數的方法值得借鑑,代碼簡潔易讀。

  • CacheLoader:抽象類。用於從數據源加載數據,定義load、reload、loadAll等操做。

  • Cache:接口,定義get、put、invalidate等操做,這裏只有緩存增刪改的操做,沒有數據加載的操做。

  • LoadingCache:接口,繼承自Cache。定義get、getUnchecked、getAll等操做,這些操做都會從數據源load數據。

  • LocalCache:類。整個guava cache的核心類,包含了guava cache的數據結構以及基本的緩存的操做方法。

  • LocalManualCache:LocalCache內部靜態類,實現Cache接口。其內部的增刪改緩存操做所有調用成員變量localCache(LocalCache類型)的相應方法。

  • LocalLoadingCache:LocalCache內部靜態類,繼承自LocalManualCache類,實現LoadingCache接口。其全部操做也是調用成員變量localCache(LocalCache類型)的相應方法。


 

 

GuavaCache並不但願咱們設置複雜的參數,而讓咱們採用建造者模式建立Cache。GuavaCache分爲兩種Cache:CacheLoadingCache。LoadingCache繼承了Cache,他比Cache主要多了get和refresh方法。多這兩個方法能幹什麼呢?

在第四節高級特性demo中,咱們看到builder生成不帶CacheLoader的Cache實例。在類結構圖中實際上是生成了LocalManualCache類實例。而帶CacheLoader的Cache實例生成的是LocalLoadingCache。他能夠定時刷新數據,由於獲取數據的方法已經做爲構造參數方法存入了Cache實例中。一樣,在get時,不須要像LocalManualCache還須要傳入一個Callable實例。

實際上,這兩個Cache實現類都繼承自LocalCache,大部分實現都是父類作的。 

LocalManualCache和LocalLoadingCache的選擇

ManualCache能夠在get時動態設置獲取數據的方法,而LoadingCache能夠定時刷新數據。如何取捨?我認爲在緩存數據有不少種類的時候採用第一種cache。而數據單一,數據庫數據會定時刷新時採用第二種cache。   

 

   先看下cache的類實現定義

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {....}

 

   咱們看到了ConcurrentMap,因此咱們知道了一點guava cache基於ConcurrentHashMap的基礎上設計。因此ConcurrentHashMap的優勢它也具有。既然實現了      ConcurrentMap那再看下guava cache中的Segment的實現是怎樣? 

   咱們看到guava cache 中的Segment本質是一個ReentrantLock。內部定義了table,wirteQueue,accessQueue定義屬性。其中table是一個ReferenceEntry原子類數組,裏面就存放了cache的內容。wirteQueue存放的是對table的寫記錄,accessQueue是訪問記錄。guava cache的expireAfterWrite,expireAfterAccess就是藉助這個兩個queue來實現的。  

2.CacheBuilder構造器 

private static final LoadingCache<String, String> cache = CacheBuilder.newBuilder() .maximumSize(1000) .refreshAfterWrite(1, TimeUnit.SECONDS) //.expireAfterWrite(1, TimeUnit.SECONDS) //.expireAfterAccess(1,TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { System.out.println(Thread.currentThread().getName() +"==load start=="+",時間=" + new Date()); // 模擬同步重載耗時2秒 Thread.sleep(2000); String value = "load-" + new Random().nextInt(10); System.out.println( Thread.currentThread().getName() + "==load end==同步耗時2秒重載數據-key=" + key + ",value="+value+",時間=" + new Date()); return value; } @Override public ListenableFuture<String> reload(final String key, final String oldValue) throws Exception { System.out.println( Thread.currentThread().getName() + "==reload ==異步重載-key=" + key + ",時間=" + new Date()); return service.submit(new Callable<String>() { @Override public String call() throws Exception { /* 模擬異步重載耗時2秒 */ Thread.sleep(2000); String value = "reload-" + new Random().nextInt(10); System.out.println(Thread.currentThread().getName() + "==reload-callable-result="+value+ ",時間=" + new Date()); return value; } }); } }); 

 

如上圖所示:CacheBuilder參數設置完畢後最後調用build(CacheLoader )構造,參數是用戶自定義的CacheLoader緩存加載器,複寫一些方法(load,reload),返回LoadingCache接口(一種面向接口編程的思想,實際返回具體實現類)以下圖: 

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

 

實際是構造了一個LoadingCache接口的實現類:LocalCache的靜態類LocalLoadingCache,本地加載緩存類。 

LocalLoadingCache( CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { super(new LocalCache<K, V>(builder, checkNotNull(loader)));//LocalLoadingCache構造函數須要一個LocalCache做爲參數  } //構造LocalCache  LocalCache( CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) { concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);//默認併發水平是4  keyStrength = builder.getKeyStrength();//key的強引用 valueStrength = builder.getValueStrength(); keyEquivalence = builder.getKeyEquivalence();//key比較器 valueEquivalence = builder.getValueEquivalence(); maxWeight = builder.getMaximumWeight(); weigher = builder.getWeigher(); expireAfterAccessNanos = builder.getExpireAfterAccessNanos();//讀寫後有效期,超時重載 expireAfterWriteNanos = builder.getExpireAfterWriteNanos();//寫後有效期,超時重載 refreshNanos = builder.getRefreshNanos(); removalListener = builder.getRemovalListener();//緩存觸發失效 或者 GC回收軟/弱引用,觸發監聽器 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); } 

 

 

 

 3、緩存的加載

分爲同步和異步
 

Guava Cache爲了限制內存佔用,一般都設定爲自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是頗有用的,由於它會自動加載緩存。

guava cache 加載緩存主要有兩種方式:

  1. cacheLoader
  2. callable callback
一、在建立的時候:cacheLoader

建立本身的CacheLoader一般只須要簡單地實現V load(K key) throws Exception方法.

cacheLoader方式實現實例:

LoadingCache<Key, Value> cache = CacheBuilder.newBuilder()
       .build(
           new CacheLoader<Key, Value>() {
             public Value load(Key key) throws AnyException {
               return createValue(key);
             }
           });
...
try {
  return cache.get(key);
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

從LoadingCache查詢的正規方式是使用get(K)方法。這個方法要麼返回已經緩存的值,要麼使用CacheLoader向緩存原子地加載新值(經過load(String key) 方法加載)。因爲CacheLoader可能拋出異常,LoadingCache.get(K)也聲明拋出ExecutionException異常。若是你定義的CacheLoader沒有聲明任何檢查型異常,則能夠經過getUnchecked(K)查找緩存;但必須注意,一旦CacheLoader聲明瞭檢查型異常,就不能夠調用getUnchecked(K)

二、在get的時候:Callable 

這種方式不須要在建立的時候指定load方法,可是須要在get的時候實現一個Callable匿名內部類。

Callable方式實現實例:

Cache<Key, Value> cache = CacheBuilder.newBuilder()
    .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<Value>() {
    @Override
    public Value call() throws AnyException {
      return doThingsTheHardWay(key);
    }
  });
} catch (ExecutionException e) {
  throw new OtherException(e.getCause());
}

  

全部類型的Guava Cache,無論有沒有自動加載功能,都支持get(K, Callable<V>)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"若是有緩存則返回;不然運算、緩存、而後返回"。

  

CacheBuilder.refreshAfterWrite(long, TimeUnit)能夠爲緩存增長自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite經過定時刷新可讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新(若是CacheLoader.refresh實現爲異步,那麼檢索不會被刷新拖慢)。所以,若是你在緩存上同時聲明expireAfterWriterefreshAfterWrite,緩存並不會由於刷新盲目地定時重置,若是緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過時時間後也變得能夠回收。

 

統計

guava cache爲咱們實現統計功能,這在其它緩存工具裏面仍是不多有的。

7) 統計緩存使用過程當中命中率/異常率/未命中率等數據。 

緩存命中率:從緩存中獲取到數據的次數/所有查詢次數,命中率越高說明這個緩存的效率好。因爲機器內存的限制,緩存通常只能佔據有限的內存大小,緩存須要不按期的刪除一部分數據,從而保證不會佔據大量內存致使機器崩潰。

如何提升命中率呢?那就得從刪除一部分數據着手了。目前有三種刪除數據的方式,分別是:FIFO(先進先出)LFU(按期淘汰最少使用次數)LRU(淘汰最長時間未被使用)。   

guava cache 除了回收還提供一種刷新機制LoadingCache.refresh(K),他們的的區別在於,guava cache 在刷新時,其餘線程能夠繼續獲取它的舊值。這在某些狀況是很是友好的。而回收的話就必須等新值加載完成之後才能繼續讀取。並且刷新是能夠異步進行的。

若是刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日誌後被丟棄[swallowed]。
重載CacheLoader.reload(K, V)能夠擴展刷新時的行爲,這個方法容許開發者在計算新值時使用舊的值

  • CacheBuilder.recordStats()用來開啓Guava Cache的統計功能。統計打開後, Cache.stats()方法會返回CacheStats對象以提供以下統計信息:

  • hitRate():緩存命中率;

  • averageLoadPenalty():加載新值的平均時間,單位爲納秒;

  • evictionCount():緩存項被回收的總數,不包括顯式清除。
    此外,還有其餘不少統計信息。這些統計信息對於調整緩存設置是相當重要的,在性能要求高的應用中咱們建議密切關注這些數據.  

Cache初始化:

final static Cache<Integer, String> cache = CacheBuilder.newBuilder() //設置cache的初始大小爲10,要合理設置該值 
        .initialCapacity(10) //設置併發數爲5,即同一時間最多隻能有5個線程往cache執行寫入操做 
        .concurrencyLevel(5) //設置cache中的數據在寫入以後的存活時間爲10秒 
        .expireAfterWrite(10, TimeUnit.SECONDS) //構建cache實例 
        .build();

 

經常使用接口:

/** * 該接口的實現被認爲是線程安全的,便可在多線程中調用 * 經過被定義單例使用 */  
public interface Cache<K, V> { /** * 經過key獲取緩存中的value,若不存在直接返回null */ V getIfPresent(Object key); /** * 經過key獲取緩存中的value,若不存在就經過valueLoader來加載該value * 整個過程爲 "if cached, return; otherwise create, cache and return" * 注意valueLoader要麼返回非null值,要麼拋出異常,絕對不能返回null */ V get(K key, Callable<? extends V> valueLoader) throws ExecutionException; /** * 添加緩存,若key存在,就覆蓋舊值 */  
  void put(K key, V value); /** * 刪除該key關聯的緩存 */  
  void invalidate(Object key); /** * 刪除全部緩存 */  
  void invalidateAll(); /** * 執行一些維護操做,包括清理緩存 */  
  void cleanUp(); }

 

4、緩存回收機制

清理何時發生?

緩存清除的時間:

   使用CacheBuilder構建的緩存不會"自動"執行清理和回收工做,也不會在某個緩存項過時後立刻清理,也沒有諸如此類的清理機制。GuavaCache的實現代碼中沒有啓動任何線程,Cache中的全部維護操做,包括清除緩存、寫入緩存等,都須要外部調用來實現 ,

相反,它會在寫操做時順帶作少許的維護工做,或者偶爾在讀操做時作——若是寫操做實在太少的話。(問題2,如何實現的)


這樣作的緣由在於:若是要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操做競爭共享鎖。此外,某些環境下線程建立可能受限制,這樣CacheBuilder就不可用了。
相反,咱們把選擇權交到你手裏。若是你的緩存是高吞吐的,那就無需擔憂緩存的維護和清理等工做。若是你的 緩存只會偶爾有寫操做,而你又不想清理工做阻礙了讀操做,那麼能夠建立本身的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService能夠幫助你很好地實現這樣的定時調度。

回收時主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是由於WeakReference、SoftReference被垃圾回收時加入的,清理時只須要遍歷整個queue,將對應的項從LocalCache中移除便可,這裏keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference。要從LocalCache中移除須要有key,於是ValueReference須要有對ReferenceEntry的引用。這裏的移除經過LocalCache而不是Segment是由於在移除時由於expand(rehash)可能致使原來在某個Segment中的ReferenceEntry後來被移動到另外一個Segment中了。

而對後面兩個Queue,只須要檢查是否配置了相應的expire時間,而後從頭開始查找已經expire的Entry,將它們移除便可。

在put的時候,還會清理recencyQueue,即將recencyQueue中的Entry添加到accessEntry中.

guava cache基於ConcurrentHashMap的設計借鑑,在高併發場景支持線程安全,使用Reference引用命令,保證了GC的可回收到相應的數據,有效節省空間;同時write鏈和access鏈的設計,能更靈活、高效的實現多種類型的緩存清理策略,包括基於容量的清理、基於時間的清理、基於引用的清理等;

LRU緩存回收算法

LRU(Least Recently Used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是「若是數據最近被訪問過,那麼未來被訪問的概率也更高」。 

1.新數據插入到鏈表頭部;
2.每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
3.當鏈表滿的時候,將鏈表尾部的數據丟棄。

Guava Cache中藉助讀寫隊列來實現LRU算法。

Guava Cache提供了四種基本的緩存回收方式:(a)基於容量回收、(b)定時回收  (c)基於引用回收 (d)顯式清除

 

1. 基於容量回收(size-based eviction)

maximumSize(long):當緩存中的元素數量超過指定值時。

    當緩存個數超過CacheBuilder.maximumSize(long)設置的值時,優先淘汰最近沒有使用或者不經常使用的元素。同理CacheBuilder.maximumWeight(long)也是同樣邏輯。

若是要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或整體上不多使用的緩存項。——警告:在緩存項的數目達到限定值以前,緩存就可能進行回收操做——一般來講,這種狀況發生在緩存項的數目逼近限定值時。

b、定時回收(Timed Eviction)

2. 定時回收 

CacheBuilder提供兩種定時回收的方法:

  (a)按照寫入時間,最先寫入的最早回收;

          expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收同樣。

  (b)按照訪問時間,最先訪問的最先回收。

expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。若是認爲緩存數據老是在固定時候後變得陳舊不可用,這種回收方式是可取的。

清理髮生時機

使用CacheBuilder構建的緩存不會」自動」執行清理和回收工做,也不會在某個緩存項過時後立刻清理。相反,它會在寫操做時順帶作少許的維護工做,或者偶爾在讀操做時作——若是寫操做實在太少的話。

3. 基於引用回收(Reference-based Eviction) 

CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存須要時,才按照全局最近最少使用的順序回收。

 

在JDK1.2以後,Java對引用的概念進行了擴充,將引用分爲強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Refernce)、虛引用(Phantom Reference)。四種引用強度依次減弱。這四種引用除了強引用(Strong Reference)以外,其它的引用所對應的對象來JVM進行GC時都是能夠確保被回收的。因此經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache能夠把緩存設置爲容許垃圾回收:

經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache能夠把緩存設置爲容許垃圾回收:
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。

      由於垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。

  由於垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存須要時,才按照LRU(全局最近最少使用)的順序回收。

    考慮到使用軟引用的性能影響,咱們一般建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存一樣用==而不是equals比較值。

        這樣的好處就是當內存資源緊張時能夠釋放掉到緩存的內存。注意!CacheBuilder若是沒有指明默認是強引用的,GC時若是沒有元素到達指定的過時時間,內存是不能被回收的。

顯式清除 

任什麼時候候,你均可以顯式地清除緩存項,而不是等到它被回收:
  個別清除:Cache.invalidate(key)
  批量清除:Cache.invalidateAll(keys)
  清除全部緩存項:Cache.invalidateAll()

這裏說一個小技巧,因爲guava cache是存在就取不存在就加載的機制,咱們能夠對緩存數據有修改的地方顯示的把它清除掉,而後再有任務去取的時候就會去數據源從新加載,這樣就能夠最大程度上保證獲取緩存的數據跟數據源是一致的。  

 

回答:在緩存訪問或者寫的時候作了清理的操做:  

無論是get,仍是put每次都會遍歷這五個queue;

一、Get 刪除過時數據: 在getLiveValue中

一、再跟進去以前第 2189 行會發現先要判斷 count 是否大於 0,這個 count 保存的是當前Segment中緩存元素的數量,並用 volatile 修飾保證了可見性。  

 

 

 二、根據方法名稱能夠看出是判斷當前的 Entry 是否過時,該 entry 就是經過 key 查詢到的。這裏就很明顯的看出是根據根據構建時指定的過時方式來判斷當前 key 是否過時了。

  

若是過時就往下走,嘗試進行過時刪除(須要加鎖,保證操做此Segment的線程安全)。

 

獲取當前緩存的總數量

自減一(前面獲取了鎖,因此線程安全)

刪除並將更新的總數賦值到 count。

而在查詢時候順帶作了這些事情,可是若是該緩存遲遲沒有訪問也會存在數據不能被回收的狀況,不過這對於一個高吞吐的應用來講也不是問題。

 

 

 二、Put刪除數據:

 

 

刪除包含了兩部分:(a)回收弱,軟引用queue(b)  刪除 access和write隊列 中過時時間的數據

 

 

 (a)回收keyReference 和 valueReference 隊列 弱,軟引用queue

 

(b)刪除 access和write隊列 中過時時間的數據

   

5、GuavaCache 的實現

GuavaCache的工做流程獲取數據->若是存在,返回數據->計算獲取數據->存儲返回。因爲特定的工做流程,使用者必須在建立Cache或者獲取數據時指定不存在數據時應當怎麼獲取數據。GuavaCache採用LRU的工做原理,使用者必須指定緩存數據的大小,當超過緩存大小時,一定引起數據刪除。GuavaCache還可讓用戶指定緩存數據的過時時間,刷新時間等等不少有用的功能。

建立本地緩存

a.CacheLoader

/** * CacheLoader 當檢索不存在的時候,會自動的加載信息的! */
private static LoadingCache<String, String> loadingCache = CacheBuilder .newBuilder() .maximumSize(2) .expireAfterWrite(10, TimeUnit.SECONDS) .concurrencyLevel(2) .recordStats() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { String value = map.get(key); log.info(" load value by key; key:{},value:{}", key, value); return value; } }); public static String getValue(String key) { try { return loadingCache.get(key); } catch (Exception e) { log.warn(" get key error ", e); return null; } }

 b.Callable

private static Cache<String, String> cacheCallable = CacheBuilder .newBuilder() .maximumSize(2) .expireAfterWrite(10, TimeUnit.SECONDS) .concurrencyLevel(2) .recordStats() .build(); /** * Callable 若是有緩存則返回;不然運算、緩存、而後返回 */
public static String getValue1(String key) { try { return cacheCallable.get(key, new Callable<String>() { @Override public String call() throws Exception { String value = map.get(key); log.info(" load value by key; key:{},value:{}", key, value); return value; } }); } catch (Exception e) { log.warn(" get key error ", e); return null; } }

 

5.一、Guava Cache使用了 builder模式

使用構造器重載咱們須要定義不少構造器,爲了應對使用者不一樣的需求(有些可能只須要id,有些須要id和name,有些只須要name,......),理論上咱們須要定義2^4 = 16個構造器,這只是4個參數,若是參數更多的話,那將是指數級增加,確定是不合理的。要麼你定義一個所有參數的構造器,使用者只能多傳入一些不須要的屬性值來匹配你的構造器。很明顯這種構造器重載的方式對於多屬性的狀況是不完美的。 (問題3 當構造函數的屬性比較多,時候能夠使用) 

 6、CacheBuilder有3種失效重載模式

    這裏面有幾個參數expireAfterWrite、expireAfterAccess、maximumSize其實這幾個定義的都是過時策略。expireAfterWrite適用於一段時間cache可能會發先變化場景。expireAfterAccess是包括expireAfterWrite在內的,由於read和write操做都被定義的access操做。另外expireAfterAccess,expireAfterAccess都是受到maximumSize的限制。當緩存的數量超過了maximumSize時,guava cache會要據LRU算法淘汰掉最近沒有寫入或訪問的數據。這

裏的maximumSize指的是緩存的個數並非緩存佔據內存的大小。 若是想限制緩存佔據內存的大小能夠配置maximumWeight參數。

      看代碼:

CacheBuilder.newBuilder().weigher(new Weigher<String, Object>() { @Override public int weigh(String key, Object value) { return 0; //the value.size()  } }).expireAfterWrite(10, TimeUnit.SECONDS).maximumWeight(500).build();

 

  weigher返回每一個cache value佔據內存的大小,這個大小是由使用者自身定義的,而且put進內存時就已經肯定後面就再不會發生變更。maximumWeight定義了全部cache value加起的weigher的總和不能超過的上限。

    注意一點就是maximumWeight與maximumSize二者只能生效一個是不能同時使用的! 

1.expireAfterWrite

當 建立 或 寫以後的 固定 有效期到達時,數據會被自動從緩存中移除, 

2.expireAfterAccess

指明每一個數據實體:當 建立 或 寫 或 讀 以後的 固定值的有效期到達時,數據會被自動從緩存中移除。讀寫操做都會重置訪問時間,但asMap方法不會。 

3.refreshAfterWrite

指明每一個數據實體:當 建立 或 寫 以後的 固定值的有效期到達時,且新請求過來時,數據會被自動刷新(注意不是刪除是異步刷新,不會阻塞讀取,先返回舊值,異步重載到數據返回後複寫新值)。  

3.數據過時重載

數據過時不會自動重載,而是經過get操做時執行過時重載。具體就是上面追蹤到了CacheBuilder構造的LocalLoadingCache,類圖以下:

返回LocalCache.LocalLoadingCache後

就能夠調用以下方法: 

static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> implements LoadingCache<K, V> { LocalLoadingCache( CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { super(new LocalCache<K, V>(builder, checkNotNull(loader))); } // LoadingCache methods 
 @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } @Override public V getUnchecked(K key) { try { return get(key); } catch (ExecutionException e) { throw new UncheckedExecutionException(e.getCause()); } } @Override public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { return localCache.getAll(keys); } @Override public void refresh(K key) { localCache.refresh(key); } @Override public final V apply(K key) { return getUnchecked(key); } // Serialization Support 
                                                                                                                   
    private static final long serialVersionUID = 1; @Override Object writeReplace() { return new LoadingSerializationProxy<K, V>(localCache); } } 

  

 刷新: 

V scheduleRefresh( ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) { if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos) && !entry.getValueReference().isLoading()) { V newValue = refresh(key, hash, loader, true);//重載數據 
        if (newValue != null) {//重載數據成功,直接返回 
          return newValue; } }//不然返回舊值 
      return oldValue; } 

 刷新核心方法: 

V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) { final LoadingValueReference<K, V> loadingValueReference = insertLoadingValueReference(key, hash, checkTime); if (loadingValueReference == null) { return null; } //異步重載數據 
      ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader); if (result.isDone()) { try { return Uninterruptibles.getUninterruptibly(result); } catch (Throwable t) { // don't let refresh exceptions propagate; error was already logged 
 } } return null; } ListenableFuture<V> loadAsync( final K key, final int hash, final LoadingValueReference<K, V> loadingValueReference, CacheLoader<? super K, V> loader) { final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader); loadingFuture.addListener( new Runnable() { @Override public void run() { try { getAndRecordStats(key, hash, loadingValueReference, loadingFuture); } catch (Throwable t) { logger.log(Level.WARNING, "Exception thrown during refresh", t); loadingValueReference.setException(t); } } }, directExecutor()); return loadingFuture; } public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) { try { stopwatch.start(); V previousValue = oldValue.get(); if (previousValue == null) { V newValue = loader.load(key); return set(newValue) ? futureValue : Futures.immediateFuture(newValue); } ListenableFuture<V> newValue = loader.reload(key, previousValue); if (newValue == null) { return Futures.immediateFuture(null); } // To avoid a race, make sure the refreshed value is set into loadingValueReference // *before* returning newValue from the cache query. 
        return transform( newValue, new com.google.common.base.Function<V, V>() { @Override public V apply(V newValue) { LoadingValueReference.this.set(newValue); return newValue; } }, directExecutor()); } catch (Throwable t) { ListenableFuture<V> result = setException(t) ? futureValue : fullyFailedFuture(t); if (t instanceof InterruptedException) { Thread.currentThread().interrupt(); } return result; } } 

如上圖,最終刷新調用的是CacheBuilder中預先設置好的CacheLoader接口實現類的reload方法實現的異步刷新。

 返回get主方法,若是當前segment中找不到key對應的實體,同步阻塞重載數據: 

V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException { ReferenceEntry<K, V> e; ValueReference<K, V> valueReference = null; LoadingValueReference<K, V> loadingValueReference = null; boolean createNewEntry = true; lock(); try { // re-read ticker once inside the lock 
        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 (e = first; e != null; e = e.getNext()) { K entryKey = e.getKey(); if (e.getHash() == hash && entryKey != null                                                                                   
              && map.keyEquivalence.equivalent(key, entryKey)) { valueReference = e.getValueReference(); if (valueReference.isLoading()) {//若是正在重載,那麼不須要從新再新建實體對象 
              createNewEntry = false; } else { V value = valueReference.get(); if (value == null) {//若是被GC回收,添加進移除隊列,等待remove監聽器執行 
 enqueueNotification( entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED); } else if (map.isExpired(e, now)) {//若是緩存過時,添加進移除隊列,等待remove監聽器執行 // This is a duplicate check, as preWriteCleanup already purged expired // entries, but let's accomodate an incorrect expiration queue. 
 enqueueNotification( entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED); } else {//不在重載,直接返回value 
 recordLockedRead(e, now); statsCounter.recordHits(1); // we were concurrent with loading; don't consider refresh 
                return value; } // immediately reuse invalid entries 
 writeQueue.remove(e); accessQueue.remove(e); this.count = newCount; // write-volatile 
 } break; } } //須要新建實體對象 
        if (createNewEntry) { loadingValueReference = new LoadingValueReference<K, V>(); if (e == null) { e = newEntry(key, hash, first); e.setValueReference(loadingValueReference); table.set(index, e);//把新的ReferenceEntry<K, V>引用實體對象添加進table 
          } else { e.setValueReference(loadingValueReference); } } } finally { unlock(); postWriteCleanup(); } //須要新建實體對象 
      if (createNewEntry) { try { // Synchronizes on the entry to allow failing fast when a recursive load is // detected. This may be circumvented when an entry is copied, but will fail fast most // of the time. 
          synchronized (e) {//同步重載數據 
            return loadSync(key, hash, loadingValueReference, loader); } } finally { statsCounter.recordMisses(1); } } else { // 重載中,說明實體已存在,等待重載完畢 
        return waitForLoadingValue(e, key, valueReference); } } 

  

 

   七、GuavaCache使用 

首先定義一個須要存儲的Bean,對象Man:

/** * @author jiangmitiao * @version V1.0 * @Title: 標題 * @Description: Bean * @date 2016/10/27 10:01 */
public class Man { //身份證號
    private String id; //姓名
    private String name; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Man{" +
                "id='" + id + '\'' +
                ", name='" + name + '\'' +
                '}'; } }

接下來咱們寫一個Demo:

import com.google.common.cache.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.*; /** * @author jiangmitiao * @version V1.0 * @Description: Demo * @date 2016/10/27 10:00 */
public class GuavaCachDemo { private LoadingCache<String,Man> loadingCache; //loadingCache
    public void InitLoadingCache() { //指定一個若是數據不存在獲取數據的方法
        CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() { @Override public Man load(String key) throws Exception { //模擬mysql操做
                Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("LoadingCache測試 從mysql加載緩存ing...(2s)"); Thread.sleep(2000); logger.info("LoadingCache測試 從mysql加載緩存成功"); Man tmpman = new Man(); tmpman.setId(key); tmpman.setName("其餘人"); if (key.equals("001")) { tmpman.setName("張三"); return tmpman; } if (key.equals("002")) { tmpman.setName("李四"); return tmpman; } return tmpman; } }; //緩存數量爲1,爲了展現緩存刪除效果
        loadingCache = CacheBuilder.newBuilder().maximumSize(1).build(cacheLoader); } //獲取數據,若是不存在返回null
    public Man getIfPresentloadingCache(String key){ return loadingCache.getIfPresent(key); } //獲取數據,若是數據不存在則經過cacheLoader獲取數據,緩存並返回
    public Man getCacheKeyloadingCache(String key){ try { return loadingCache.get(key); } catch (ExecutionException e) { e.printStackTrace(); } return null; } //直接向緩存put數據
    public void putloadingCache(String key,Man value){ Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("put key :{} value : {}",key,value.getName()); loadingCache.put(key,value); } }

 

接下來,咱們寫一些測試方法,檢測一下

public class Test { public static void main(String[] args){ GuavaCachDemo cachDemo = new GuavaCachDemo() System.out.println("使用loadingCache"); cachDemo.InitLoadingCache(); System.out.println("使用loadingCache get方法 第一次加載"); Man man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 第一次加載"); man = cachDemo.getIfPresentloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 第一次加載"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加載過"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加載過,可是已經被剔除掉,驗證從新加載"); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 已加載過"); man = cachDemo.getIfPresentloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache put方法 再次get"); Man newMan = new Man(); newMan.setId("001"); newMan.setName("額外添加"); cachDemo.putloadingCache("001",newMan); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); } }

 

  

 

   guava cache使用簡介

     guava cache 是利用CacheBuilder類用builder模式構造出兩種不一樣的cache加載方式CacheLoader,Callable,共同邏輯都是根據key是加載value。不一樣的地方在於CacheLoader的定義比較寬泛,是針對整個cache定義的,能夠認爲是統一的根據key值load value的方法,而Callable的方式較爲靈活,容許你在get的時候指定load方法。看如下代碼 

Cache<String,Object> cache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(500).build(); cache.get("key", new Callable<Object>() { //Callable 加載
 @Override public Object call() throws Exception { return "value"; } }); LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder() .expireAfterAccess(30, TimeUnit.SECONDS).maximumSize(5) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return "value"; } });

 

 

   

  

加載 

CacheLoader

若是有合理的默認方法來加載或計算與鍵關聯的值。

LoadingCache是附帶CacheLoader構建而成的緩存實現。建立本身的CacheLoader一般只須要簡單地實現V load(K key) throws Exception方法。

從LoadingCache查詢的正規方式是使用get(K)方法。這個方法要麼返回已經緩存的值,要麼使用CacheLoader向緩存原子地加載新值。因爲CacheLoader可能拋出異常,LoadingCache.get(K)也聲明爲拋出ExecutionException異常。

Callable

若是沒有合理的默認方法來加載或計算與鍵關聯的值,或者想要覆蓋默認的加載運算,同時保留「獲取緩存-若是沒有-則計算」[get-if-absent-compute]的原子語義。
全部類型的Guava Cache,無論有沒有自動加載功能,都支持get(K, Callable<V>)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"若是有緩存則返回;不然運算、緩存、而後返回"。 

 

但自動加載是首選的,由於它能夠更容易地推斷全部緩存內容的一致性。
使用cache.put(key, value)方法能夠直接向緩存中插入值,這會直接覆蓋掉給定鍵以前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載範疇以外,因此相比於Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 應該老是優先使用。

 

 

統計

CacheBuilder.recordStats():用來開啓Guava Cache的統計功能。統計打開後,Cache.stats()方法會返回CacheS tats 對象以提供以下統計信息:

hitRate():緩存命中率;
averageLoadPenalty():加載新值的平均時間,單位爲納秒;
evictionCount():緩存項被回收的總數,不包括顯式清除。
此外,還有其餘不少統計信息。這些統計信息對於調整緩存設置是相當重要的,在性能要求高的應用中咱們建議密切關注這些數據。

Demo3:

public class GuavaCacheDemo3 { static Cache<String, Object> testCache = CacheBuilder.newBuilder() .weakValues() .recordStats() .build(); public static void main(String[] args){ Object obj1 = new Object(); testCache.put("1234",obj1); obj1 = new String("123"); System.gc(); System.out.println(testCache.getIfPresent("1234")); System.out.println(testCache.stats()); } }

運行結果 

 

三、Guava Cache 主要的類圖:

 
CacheBuilder

緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。
主要採用builder的模式,CacheBuilder的每個方法都返回這個CacheBuilder知道build方法的調用。
注意build方法有重載,帶有參數的爲構建一個具備數據加載功能的緩存,不帶參數的構建一個沒有數據加載功能的緩存。

LocalManualCache

做爲LocalCache的一個內部類,在構造方法裏面會把LocalCache類型的變量傳入,而且調用方法時都直接或者間接調用LocalCache裏面的方法。

LocalLoadingCache

能夠看到該類繼承了LocalManualCache並實現接口LoadingCache。
覆蓋了get,getUnchecked等方法。

  

二、localManualCache如何實現的

 
上一篇文章講了LocalCache是如何經過Builder構建出來的,這篇文章重點是講localCache的原理,首先經過類圖理清涉及到相關類的關係,以下圖咱們能夠看到,guava Cache的核心就是LocalCache,LocalCache實現了ConcurrentMap,並繼承了抽象的map,關於ConcurrentMap的實現能夠看這篇文章,講的是併發hashmap的實現,對理解這篇文章有幫助。對於構造LocalCache最直接的兩個相關類是LocalManualCache和LocalLoadingCache。

LocalManualCache和LocalLoadingCache

 

 

 

 

 

那麼這個LoadingCache究竟是什麼做用呢,其實就是LocalCache對外暴露了實現的方法,全部暴露的方法都是實現了這個接口,LocalLoadingCache就是實現了這個接口, 

特殊的是它是LocalCache的內部靜態類,這個LocalLoadingCache內部靜態類只是下降了LocalCache的複雜度,它是徹底獨立於LocalCache的。下邊是咱們使用的方法都是LocalCache接口的方法  

說完了LocalLoadingCache咱們看下LocalManualCache的做用,LocalManualCache是LocalLoadingCache的父類,LocalManualCache實現了Cache,因此LocalManualCache具備了全部Cache的方法,LocalLoadingCache是繼承自LocalManualCache一樣得到了Cache的全部方法,可是LocalLoadingCache能夠選擇的重載LocalManualCache中的方法,這樣的設計有很大的靈活性;guava cache的內部實現用的LocalCache,可是對外暴露的是LocalLoadingCache,很好隱藏了細節,總結來講

一、LocalManualCache實現了Cache,具備了全部cache方法。

二、LocalLoadingCache實現了LoadingCache,具備了全部LoadingCache方法。

三、LocalLoadingCache繼承了LocalManualCache,那麼對外暴露的LocalLoadingCache的方法既有自身須要的,又有cache應該具備的。

四、經過LocalLoadingCache和LocalManualCache的父子關係實現了LocalCache的細節。

Guava Cache究竟是如何進行緩存的
咱們如今經過類圖和源碼的各類繼承關係理清了這兩個LocalLoadingCache和LocalManualCache的重要關係,下邊咱們再繼續深刻,經過咱們經常使用的get方法進入:

/** * LocalLoadingCache中的get方法,localCache是父類LocalManualCache的 */ @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } /** * 這個get和getOrLoad是AccessQueue中的方法,AccessQueue是何方神聖呢,咱們經過類圖梳理一下他們的關係 */ V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException { int hash = hash(checkNotNull(key)); return segmentFor(hash).get(key, hash, loader); } V getOrLoad(K key) throws ExecutionException { return get(key, defaultLoader); }

 


很明顯這是隊列,這兩個隊列的做用以下

WriteQueue:按照寫入時間進行排序的元素隊列,寫入一個元素時會把它加入到隊列尾部。
AccessQueue:按照訪問時間進行排序的元素隊列,訪問(包括寫入)一個元素時會把它加入到隊列尾部。 

 

咱們來看下ReferenceEntry接口的代碼,具有了一個Entry所須要的元素

interface ReferenceEntry<K, V> { /** * Returns the value reference from this entry. */ ValueReference<K, V> getValueReference(); /** * Sets the value reference for this entry. */
void setValueReference(ValueReference<K, V> valueReference); /** * Returns the next entry in the chain. */ @Nullable ReferenceEntry<K, V> getNext(); /** * Returns the entry's hash. */
int getHash(); /** * Returns the key for this entry. */ @Nullable K getKey(); /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. * New entries are added at the tail of the list at write time; stale entries are expired from * the head of the list. */

/** * Returns the time that this entry was last accessed, in ns. */
long getAccessTime();

 

 

五、Guava Cache 如何加載數據


無論性能,仍是可用性來講, Guava Cache 絕對是本地緩存類庫中首要推薦的工具類。其提供的 Builder模式 的CacheBuilder生成器來建立緩存的方式,十分方便,而且各個緩存參數的配置設置,相似於函數式編程的寫法,也特別棒。 

在官方文檔中,提到三種方式加載 <key,value> 到緩存中。分別是:

LoadingCache 在構建緩存的時候,使用build方法內部調用 CacheLoader 方法加載數據;

在使用get方法的時候,若是緩存不存在該key或者key過時等,則調用 get(K, Callable<V>) 方式加載數據;

使用粗暴直接的方式,直接想緩存中put數據。
須要說明的是,若是不能經過key快速計算出value時,則仍是不要在初始化的時候直接調用 CacheLoader 加載數據到緩存中。

 

 

 

加載

在使用緩存前,首先問本身一個問題:有沒有合理的默認方法來加載或計算與鍵關聯的值?若是有的話,你應當使用CacheLoader。若是沒有,或者你想要覆蓋默認的加載運算,同時保留」獲取緩存-若是沒有-則計算」[get-if-absent-compute]的原子語義,你應該在調用get時傳入一個Callable實例。緩存元素也能夠經過Cache.put方法直接插入,但自動加載是首選的,由於它能夠更容易地推斷全部緩存內容的一致性。自動加載就是createCacheLoader中的,當cache.get(key)不存在的時候,會主動的去加載值的信息並放進緩存中去。

Guava Cache有如下兩種建立方式:
建立 CacheLoader
LoadingCache是附帶CacheLoader構建而成的緩存實現。建立本身的CacheLoader一般只須要簡單地實現V load(K key) throws Exception方法。例如,你能夠用下面的代碼構建LoadingCache:
CacheLoader: 當檢索不存在的時候,會自動的加載信息的!

public static com.google.common.cache.CacheLoader<String, Employee> createCacheLoader() { return new com.google.common.cache.CacheLoader<String, Employee>() { @Override public Employee load(String key) throws Exception { log.info("加載建立key:" + key); return new Employee(key, key + "dept", key + "id"); } }; } LoadingCache<String, Employee> cache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterAccess(30L, TimeUnit.MILLISECONDS) .build(createCacheLoader());

 


建立 Callable
全部類型的Guava Cache,無論有沒有自動加載功能,都支持get(K, Callable)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式」若是有緩存則返回;不然運算、緩存、而後返回」。

Cache<Key, Value> 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<Value>() { @Override public Value call() throws AnyException { return doThingsTheHardWay(key); } }); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }

  

七、刷新

 

刷新和回收不太同樣。正如LoadingCache.refresh(K)所聲明,刷新表示爲鍵加載新值,這個過程能夠是異步的。在刷新操做進行時,緩存仍然能夠向其餘線程返回舊值,而不像回收操做,讀緩存的線程必須等待新值加載完成。 


CacheBuilder.refreshAfterWrite(long, TimeUnit)能夠爲緩存增長自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite經過定時刷新可讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新(若是CacheLoader.refresh實現爲異步,那麼檢索不會被刷新拖慢)。所以,若是你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存並不會由於刷新盲目地定時重置,若是緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過時時間後也變得能夠回收。 

 

 

 

 

2.1 Guava Cache使用示例

import java.util.Date; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; /** * @author tao.ke Date: 14-12-20 Time: 下午1:55 * @version \$Id$ */
public class CacheSample { private static final Logger logger = LoggerFactory.getLogger(CacheSample.class); // Callable形式的Cache
private static final Cache<String, String> CALLABLE_CACHE = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS).maximumSize(1000).recordStats() .removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { logger.info("Remove a map entry which key is {},value is {},cause is {}.", notification.getKey(), notification.getValue(), notification.getCause().name()); } }).build(); // CacheLoader形式的Cache
private static final LoadingCache<String, String> LOADER_CACHE = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(1000).recordStats().build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return key + new Date(); } }); public static void main(String[] args) throws Exception { int times = 4; while (times-- > 0) { Thread.sleep(900); String valueCallable = CALLABLE_CACHE.get("key", new Callable<String>() { @Override public String call() throws Exception { return "key" + new Date(); } }); logger.info("Callable Cache ----->>>>> key is {},value is {}", "key", valueCallable); logger.info("Callable Cache ----->>>>> stat miss:{},stat hit:{}",CALLABLE_CACHE.stats().missRate(),CALLABLE_CACHE.stats().hitRate()); String valueLoader = LOADER_CACHE.get("key"); logger.info("Loader Cache ----->>>>> key is {},value is {}", "key", valueLoader); logger.info("Loader Cache ----->>>>> stat miss:{},stat hit:{}",LOADER_CACHE.stats().missRate(),LOADER_CACHE.stats().hitRate()); } } }

上述代碼,簡單的介紹了 Guava Cache 的使用,給了兩種加載構建Cache的方式。在 Guava Cache 對外提供的方法中, recordStats 和 removalListener 是兩個頗有趣的接口,能夠很好的幫咱們完成統計功能和Entry移除引發的監聽觸發功能。

此外,雖然在 Guava Cache 對外方法接口中提供了豐富的特性,可是若是咱們在實際的代碼中不是頗有須要的話,建議不要設置這些屬性,由於會額外佔用內存而且會多一些處理計算工做,不值得。

Guava Cache 分析前置知識
Guava Cache 就是借鑑Java的 ConcurrentHashMap 的思想來實現一個本地緩存,可是它內部代碼實現的時候,仍是有不少很是精彩的設計實現,而且若是對 ConcurrentHashMap 內部具體實現不是很清楚的話,經過閱讀 Cache 的實現,對 ConcurrentHashMap 的實現基本上會有個全面的瞭解。

3.1 Builder模式
設計模式之 Builder模式 在Guava中不少地方獲得的使用。 Builder模式 是將一個複雜對象的構造與其對應配置屬性表示的分離,也就是能夠使用基本相同的構造過程去建立不一樣的具體對象。

Builder模式典型的結構圖如:

 

 

 

Builder:爲建立一個Product對象的各個部件制定抽象接口;

ConcreteBuilder:具體的建造者,它負責真正的生產;

Director:導演, 建造的執行者,它負責發佈命令;

Product:最終消費的產品
 

Builder模式 的關鍵是其中的Director對象並不直接返回對象,而是經過(BuildPartA,BuildPartB,BuildPartC)來一步步進行對象的建立。固然這裏Director能夠提供一個默認的返回對象的接口(即返回通用的複雜對象的建立,即不指定或者特定惟一指定BuildPart中的參數)。

Tips:在 Effective Java 第二版中, Josh Bloch 在第二章中就提到使用Builder模式處理須要不少參數的構造函數。他不只展現了Builder的使用,也描述了相這種方法相對使用帶不少參數的構造函數帶來的好處。

下面給出一個使用Builder模式來構造對象,這種方式優勢和不足(代碼量增長)很是明顯。

import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; /** * @author tao.ke Date: 14-12-22 Time: 下午8:57 * @version \$Id$ */
public class BuilderPattern { /** * 姓名 */
private String name; /** * 年齡 */
private int age; /** * 性別 */
private Gender gender; public static BuilderPattern newBuilder() { return new BuilderPattern(); } public BuilderPattern setName(String name) { this.name = name; return this; } public BuilderPattern setAge(int age) { this.age = age; return this; } public BuilderPattern setGender(Gender gender) { this.gender = gender; return this; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } enum Gender { MALE, FEMALE } public static void main(String[] args) { BuilderPattern bp = BuilderPattern.newBuilder().setAge(10).setName("zhangsan").setGender(Gender.FEMALE); system.out.println(bp.toString()); } }

 

 

 

   

3.6 Guava ListenableFuture接口
咱們強烈地建議你在代碼中多使用 ListenableFuture 來代替JDK的 Future, 由於:

大多數Futures 方法中須要它。

轉到 ListenableFuture 編程比較容易。

Guava提供的通用公共類封裝了公共的操做方方法,不須要提供Future和 ListenableFuture 的擴展方法。

建立ListenableFuture實例

首先須要建立 ListeningExecutorService 實例,Guava 提供了專門的方法把JDK中提供 ExecutorService對象轉換爲 ListeningExecutorService 。而後經過submit方法就能夠建立一個ListenableFuture實例了。

代碼片斷以下:

ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); ListenableFuture explosion = service.submit(new Callable() { public Explosion call() { return pushBigRedButton(); } }); Futures.addCallback(explosion, new FutureCallback() { // we want this handler to run immediately after we push the big red button!
public void onSuccess(Explosion explosion) { walkAwayFrom(explosion); } public void onFailure(Throwable thrown) { battleArchNemesis(); // escaped the explosion!
} });

也就是說,對於異步的方法,我能夠經過監聽器來根據執行結果來判斷接下來的處理行爲。

ListenableFuture 鏈式操做

使用ListenableFuture 最重要的理由是它能夠進行一系列的複雜鏈式的異步操做。

通常,使用AsyncFunction來完成鏈式異步操做。不一樣的操做能夠在不一樣的Executors中執行,單獨的ListenableFuture 能夠有多個操做等待。

>

Tips: AsyncFunction接口常被用於當咱們想要異步的執行轉換而不形成線程阻塞時,儘管Future.get()方法會在任務沒有完成時形成阻塞,可是AsyncFunction接口並不被建議用來異步的執行轉換,它常被用於返回Future實例。

下面給出這個鏈式操做完成一個簡單的異步字符串轉換操做:

import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; /** * @author tao.ke Date: 14-12-26 Time: 下午5:28 * @version \$Id$ */
public class ListenerFutureChain { private static final ExecutorService executor = Executors.newFixedThreadPool(2); private static final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(executor); public void executeChain() { AsyncFunction<String, String> asyncFunction = new AsyncFunction<String, String>() { @Override public ListenableFuture<String> apply(final String input) throws Exception { ListenableFuture<String> future = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("STEP1 >>>" + Thread.currentThread().getName()); return input + "|||step 1 ===--===||| "; } }); return future; } }; AsyncFunction<String, String> asyncFunction2 = new AsyncFunction<String, String>() { @Override public ListenableFuture<String> apply(final String input) throws Exception { ListenableFuture<String> future = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("STEP2 >>>" + Thread.currentThread().getName()); return input + "|||step 2 ===--===---||| "; } }); return future; } }; ListenableFuture startFuture = executorService.submit(new Callable() { @Override public Object call() throws Exception { System.out.println("BEGIN >>>" + Thread.currentThread().getName()); return "BEGIN--->"; } }); ListenableFuture future = Futures.transform(startFuture, asyncFunction, executor); ListenableFuture endFuture = Futures.transform(future, asyncFunction2, executor); Futures.addCallback(endFuture, new FutureCallback() { @Override public void onSuccess(Object result) { System.out.println(result); System.out.println("=======OK======="); } @Override public void onFailure(Throwable t) { t.printStackTrace(); } }); } public static void main(String[] args) { System.out.println("========START======="); System.out.println("MAIN >>>" + Thread.currentThread().getName()); ListenerFutureChain chain = new ListenerFutureChain(); chain.executeChain(); System.out.println("========END======="); executor.shutdown(); // System.exit(0);
} } 輸出: ========START======= MAIN >>>main BEGIN >>>pool-2-thread-1
========END======= STEP1 >>>pool-2-thread-2 STEP2 >>>pool-2-thread-1 BEGIN--->|||step 1 ===--===||| |||step 2 ===--===---||| 
=======OK=======

 

從輸出能夠看出,代碼是異步完成字符串操做的。

CacheBuilder實現
寫過Cache的,或者其餘一些工具類的同窗知道,爲了讓工具類更靈活,咱們須要對外提供大量的參數配置給使用者設置,雖然這帶有一些好處,可是因爲參數太多,使用者開發構造對象的時候過於繁雜。

上面提到過參數配置過多,能夠使用Builder模式。Guava Cache也同樣,它爲咱們提供了CacheBuilder工具類來構造不一樣配置的Cache實例。可是,和本文上面提到的構造器實現有點不同,它構造器返回的是另一個對象,所以,這意味着在實現的時候,對象構造函數須要有Builder參數提供配置屬性。

4.1 CacheBuilder構造LocalCache實現
首先,咱們先看看Cache的構造函數:

/** * 從builder中獲取相應的配置參數。 */ LocalCache(CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) { concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS); 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); } //.......
}

從構造函數能夠看到,Cache的全部參數配置都是從Builder對象中獲取的,Builder完成了做爲該模式最典型的應用,多配置參數構建對象。

在Cache中只提供一個構造函數,可是在上面代碼示例中,咱們演示了兩種構建緩存的方式:自動加載;手動加載。那麼,通常會存在一個完成二者之間的過渡 adapter 組件,接下來看看Builder在內部是如何完成建立緩存對象過程的。

OK,你猜到了。在 LocalCache 中確實提供了兩種過渡類,一個是支持自動加載value的 LocalLoadingCache 和只能在鍵值找不到的時候手動調用獲取值方法的 LocalManualCache 。

LocalManualCache實現

static class LocalManualCache<K, V> implements Cache<K, V>, Serializable { final LocalCache<K, V> localCache; LocalManualCache(CacheBuilder<? super K, ? super V> builder) { this(new LocalCache<K, V>(builder, null)); } private LocalManualCache(LocalCache<K, V> localCache) { this.localCache = localCache; } // Cache methods
 @Override @Nullable public V getIfPresent(Object key) { return localCache.getIfPresent(key); } @Override public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException { checkNotNull(valueLoader); return localCache.get(key, new CacheLoader<Object, V>() { @Override public V load(Object key) throws Exception { return valueLoader.call(); } }); } //......
 @Override public CacheStats stats() { SimpleStatsCounter aggregator = new SimpleStatsCounter(); aggregator.incrementBy(localCache.globalStatsCounter); for (Segment<K, V> segment : localCache.segments) { aggregator.incrementBy(segment.statsCounter); } return aggregator.snapshot(); } // Serialization Support

private static final long serialVersionUID = 1; Object writeReplace() { return new ManualSerializationProxy<K, V>(localCache); } }

從代碼實現看出其實是一個adapter組件,而且絕大部分實現都是直接調用LocalCache的方法,或者加一些參數判斷和聚合。在它核心的構造函數中,就是直接調用LocalCache構造函數,對於loader對象直接設null值。

LocalLoadingCache實現

LocalLoadingCache 實現繼承了``類,其主要對get相關方法作了重寫。

static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> implements LoadingCache<K, V> { LocalLoadingCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { super(new LocalCache<K, V>(builder, checkNotNull(loader))); } // LoadingCache methods
 @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } @Override public V getUnchecked(K key) { try { return get(key); } catch (ExecutionException e) { throw new UncheckedExecutionException(e.getCause()); } } @Override public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { return localCache.getAll(keys); } @Override public void refresh(K key) { localCache.refresh(key); } @Override public final V apply(K key) { return getUnchecked(key); } // Serialization Support

private static final long serialVersionUID = 1; @Override Object writeReplace() { return new LoadingSerializationProxy<K, V>(localCache); } } } 提供了這些adapter類以後,builder類就能夠建立 LocalCache ,以下: // 獲取value能夠經過key計算出
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); }

 

4.2 CacheBuilder參數設置
CacheBuilder 在爲咱們提供了構造一個Cache對象時,會構造各個成員對象的初始值(默認值)。瞭解這些默認值,對於咱們分析Cache源碼實現時,一些判斷條件的設置緣由,仍是頗有用的。

初始參數值設置

在 ConcurrentHashMap 中,咱們知道有個併發水平(CONCURRENCY_LEVEL),這個參數決定了其容許多少個線程併發操做修改該數據結構。這是由於這個參數是最後map使用的segment個數,而每一個segment對應一個鎖,所以,對於一個map來講,併發環境下,理論上最大能夠有segment個數的線程同時安全地操做修改數據結構。那麼是否是segment的值能夠設置很大呢?顯然不是,要記住維護一個鎖的成本仍是挺高的,此外若是涉及全表操做,那麼性能就會很是很差了。

其餘一些初始參數值的設置以下所示:

private static final int DEFAULT_INITIAL_CAPACITY = 16; // 默認的初始化Map大小
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 int UNSET_INT = -1; boolean strictParsing = true; int initialCapacity = UNSET_INT; int concurrencyLevel = UNSET_INT; long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; long expireAfterWriteNanos = UNSET_INT; long expireAfterAccessNanos = UNSET_INT; long refreshNanos = UNSET_INT;

 


初始對象引用設置

在Cache中,咱們除了超時時間,鍵值引用屬性等設置外,還關注命中統計狀況,這就須要統計對象來工做。CacheBuilder提供了初始的null 統計對象和空統計對象。

此外,還會設置到默認的引用類型等設置,代碼以下:

/** * 默認空的緩存命中統計類 */
static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER = Suppliers.ofInstance(new StatsCounter() { //......省略空override
 @Override public CacheStats snapshot() { return EMPTY_STATS; } }); 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();//這裏構造簡單地統計類實現
} }; /** * 自定義的空RemovalListener,監聽到移除通知,默認空處理。 */
enum NullListener implements RemovalListener<Object, Object> { INSTANCE; @Override public void onRemoval(RemovalNotification<Object, Object> notification) { } } /** * 默認權重類,任何對象的權重均爲1 */
enum OneWeigher implements Weigher<Object, Object> { INSTANCE; @Override public int weigh(Object key, Object value) { return 1; } } static final Ticker NULL_TICKER = new Ticker() { @Override public long read() { return 0; } }; /** * 默認的key等同判斷 * @return */ Equivalence<Object> getKeyEquivalence() { return firstNonNull(keyEquivalence, getKeyStrength().defaultEquivalence()); } /** * 默認value的等同判斷 * @return */ Equivalence<Object> getValueEquivalence() { return firstNonNull(valueEquivalence, getValueStrength().defaultEquivalence()); } /** * 默認的key引用 * @return */ Strength getKeyStrength() { return firstNonNull(keyStrength, Strength.STRONG); } /** * 默認爲Strong 屬性的引用 * @return */ Strength getValueStrength() { return firstNonNull(valueStrength, Strength.STRONG); } <K1 extends K, V1 extends V> Weigher<K1, V1> getWeigher() { return (Weigher<K1, V1>) Objects.firstNonNull(weigher, OneWeigher.INSTANCE); }

 


其中,在咱們不設置緩存中鍵值引用的狀況下,默認都是採用強引用及相對應的屬性策略來初始化的。此外,在上面代碼中還能夠看到,統計類 SimpleStatsCounter 是一個簡單的實現。裏面主要是簡單地緩存累加,此外因爲多線程下Long類型的線程非安全性,因此也進行了一下封裝,下面給出命中率的實現:

public static final class SimpleStatsCounter implements StatsCounter { private final LongAddable hitCount = LongAddables.create(); private final LongAddable missCount = LongAddables.create(); private final LongAddable loadSuccessCount = LongAddables.create(); private final LongAddable loadExceptionCount = LongAddables.create(); private final LongAddable totalLoadTime = LongAddables.create(); private final LongAddable evictionCount = LongAddables.create(); public SimpleStatsCounter() {} @Override public void recordHits(int count) { hitCount.add(count); } @Override public CacheStats snapshot() { return new CacheStats( hitCount.sum()); } /** * Increments all counters by the values in {@code other}. */
public void incrementBy(StatsCounter other) { CacheStats otherStats = other.snapshot(); hitCount.add(otherStats.hitCount()); } }

所以,CacheBuilder的一些參數對象等得初始化就完成了。能夠看到這些默認的初始化,有兩套引用:Null對象和Empty對象,顯然Null會更省空間,但咱們在建立的時候將決定不使用某特性的時候,就會使用Null來建立,不然使用Empty來完成初始化工做。在分析Cache的時候,寫後超時隊列和讀後超時隊列也存在兩個版本。

LocalCache實現
在設計實現上, LocalCache 的併發策略和 concurrentHashMap 的併發策略是一致的,也是根據分段鎖來提升併發能力,分段鎖能夠很好的保證 併發讀寫的效率。所以,該map支持非阻塞讀和不一樣段之間併發寫。

若是最大的大小指定了,那麼基於段來執行操做是最好的。使用頁面替換算法來決定當map大小超過指定值時,哪些entries須要被驅趕出去。頁面替換算法的數據結構保證Map臨時一致性:對一個segment寫排序是一致的;可是對map進行更新和讀不能直接馬上 反應在數據結構上。 雖然這些數據結構被lock鎖保護,可是其結構決定了批量操做能夠避免鎖競爭出現。在線程之間傳播的批量操做致使分攤成本比不強制大小限制的操做要稍微高一點。

此外, LoacalCache 使用LRU頁面替換算法,是由於該算法簡單,而且有很高的命中率,以及O(1)的時間複雜度。須要說明的是, LRU算法是基於頁面而不是全局實現的,因此可能在命中率上不如全局LRU算法,可是應該基本類似。

最後,要說明一點,在代碼實現上,頁面其實就是一個段segment。之因此說page頁,是由於在計算機專業課程上,CPU,操做系統,算法上,基本上都介紹過度頁致使優化效果的提高。

 
 

 

從圖中能夠直觀看到cache是以segment粒度來控制併發get和put等操做的,接下來首先看咱們的 LocalCache 是如何構造這些segment段的,繼續上面初始化localCache構造函數的代碼:

// 找到大於併發水平的最小2的次方的值,做爲segment數量
int segmentShift = 0; int segmentCount = 1; while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1; } this.segmentShift = 32 - segmentShift;//位 偏移數
segmentMask = segmentCount - 1;//mask碼

this.segments = newSegmentArray(segmentCount);// 構造數據數組,如上圖所示 //獲取每一個segment初始化容量,而且保證大於等於map初始容量
int segmentCapacity = initialCapacity / segmentCount; if (segmentCapacity * segmentCount < initialCapacity) { ++segmentCapacity; } //段Size 必須爲2的次數,而且剛剛大於段初始容量
int segmentSize = 1; while (segmentSize < segmentCapacity) { segmentSize <<= 1; } // 權重設置,確保權重和==map權重
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()); } } Notes:基本上都是基於2的次數來設置大小的,顯然基於移位操做比普通計算操做速度要快。此外,對於最大權重分配到段權重的設計上,很特殊。爲何呢?爲了保證二者可以相等(maxWeight==sumAll(maxSegmentWeight)),對於remainder前面的segment maxSegmentWeight的值比remainder後面的權重值大1,這樣保證最後值相等。 map get 方法 @Override @Nullable public V get(@Nullable Object key) { if (key == null) { return null; } int hash = hash(key); return segmentFor(hash).get(key, hash); } Notes:代碼很簡單,首先check key是否爲null,而後計算hash值,定位到對應的segment,執行segment實例擁有的get方法獲取對應的value值 map put 方法 @Override public V put(K key, V value) { checkNotNull(key); checkNotNull(value); int hash = hash(key); return segmentFor(hash).put(key, hash, value, false); } Notes:和get方法同樣,也是先check值,而後計算key的hash值,而後定位到對應的segment段,執行段實例的put方法,將鍵值存入map中。 map isEmpty 方法 @Override public boolean isEmpty() { long sum = 0L; Segment<K, V>[] segments = this.segments; for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0) { return false; } sum += segments[i].modCount; } if (sum != 0L) { // recheck unless no modifications
for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0) { return false; } sum -= segments[i].modCount; } if (sum != 0L) { return false; } } return true; }

 

Notes:判斷Cache是否爲空,就是分別判斷每一個段segment是否都爲空,可是因爲總體是在併發環境下進行的,也就是說存在對一個segment併發的增長和移除元素的時候,而咱們此時正在check其餘segment段。

上面這種狀況,決定了咱們不可以得到任何一個時間點真實段狀態的狀況。所以,上面的代碼引入了sum變量來計算段modCount變動狀況。modCount表示改變segment大小size的更新次數,這個在批量讀取方法期間保證它們能夠看到一致性的快照。 須要注意,這裏先獲取count,該值是volatile,所以modCount一般均可以在不須要一致性控制下,得到當前segment最新的值.

在判斷若是在第一次check的時候,發現segment發生了數據結構級別變動,則會進行recheck,就是在每一個modCount下,段仍然是空的,則判斷該map爲空。若是發現這期間數據結構發生變化,則返回非空判斷。

map 其餘方法

在Cache數據結構中,還有不少方法,和上面列出來的方法同樣,其底層核心實現都是依賴segment類實例中實現的對應方法。

此外,在總的數據結構中,還提供了一些根據builder配製制定相應地緩存策略方法。好比:

expiresAfterAccess:是否執行訪問後超時過時策略;
expiresAfterWrite:是否執行寫後超時過時策略;
usesAccessQueue:根據上面的配置決定是否須要new一個訪問隊列;
usesWriteQueue:根據上面的配置決定是否須要new一個寫隊列;
usesKeyReferences/usesValueReferences:是否須要使用特別的引用策略(非Strong引用).
等等……
5.2 引用數據結構
在介紹Segment數據結構以前,先講講Cache中引用的設計。

關於Reference引用的一些說明,在博文的上面已經介紹了,這裏就不贅述。在Guava Cache 中,主要使用三種引用類型,分別是: STRONG引用 , SOFT引用 , WEAK引用 。和Map不一樣,在Cache中,使用 ReferenceEntry 來封裝鍵值對,而且對於值來講,還額外實現了 ValueReference 引用對象來封裝對應Value對象。

ReferenceEntry節點結構

爲了支持各類不一樣類型的引用,以及不一樣過時策略,這裏構造了一個ReferenceEntry節點結構。經過下面的節點數據結構,能夠清晰的看到緩存大體操做流程。

/** * 引用map中一個entry節點。 * * 在map中得entries節點有下面幾種狀態: * valid:-live:設置了有效的key/value;-loading:加載正在處理中.... * invalid:-expired:時間過時(可是key/value可能仍然設置了);Collected:key/value部分被垃圾收集了,可是尚未被清除; * -unset:標記爲unset,表示等待清除或者從新使用。 * */
interface ReferenceEntry<K, V> { /** * 從entry中返回value引用 */ ValueReference<K, V> getValueReference(); /** * 爲entry設置value引用 */
void setValueReference(ValueReference<K, V> valueReference); /** * 返回鏈中下一個entry(解決hash碰撞存在鏈表) */ @Nullable ReferenceEntry<K, V> getNext(); /** * 返回entry的hash */
int getHash(); /** * 返回entry的key */ @Nullable K getKey(); /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. New entries are * added at the tail of the list at write time; stale entries are expired from the head of the list. */

/** * 返回該entry最近一次被訪問的時間ns */
long getAccessTime(); /** * 設置entry訪問時間ns. */
void setAccessTime(long time); /** * 返回訪問隊列中下一個entry */ ReferenceEntry<K, V> getNextInAccessQueue(); /** * Sets the next entry in the access queue. */
void setNextInAccessQueue(ReferenceEntry<K, V> next); /** * Returns the previous entry in the access queue. */ ReferenceEntry<K, V> getPreviousInAccessQueue(); /** * Sets the previous entry in the access queue. */
void setPreviousInAccessQueue(ReferenceEntry<K, V> previous); // ...... 省略write隊列相關方法,和access同樣
}

Notes:從上面代碼能夠看到除了和Map同樣,有key、value、hash和next四個屬性以外,還有訪問和寫更新兩個雙向鏈表隊列,以及entry的最近訪問時間和最近更新時間。顯然,多出來的屬性就是爲了支持緩存必需要有的過時機制。

此外,從上面的代碼能夠看出 cache支持的LRU機制其實是創建在segment上的,也就是基於頁的替換機制。

關於訪問隊列數據結構,其實質就是一個雙向的鏈表。當節點被訪問的時候,這個節點將會移除,而後把這個節點添加到鏈表的結尾。關於具體實現,將在segment中介紹。

建立不一樣類型的ReferenceEntry由其枚舉工廠類EntryFactory來實現,它根據key的Strength類型、是否使用accessQueue、是否使用writeQueue來決定不一樣的EntryFactry實例,並經過它建立相應的ReferenceEntry實例

ValueReference結構

一樣爲了支持Cache中各個不一樣類型的引用,其對Value類型進行再封裝,支持引用。看看其內部數據屬性:

/** * A reference to a value. */
interface ValueReference<K, V> { /** * Returns the value. Does not block or throw exceptions. */ @Nullable V get(); /** * Waits for a value that may still be loading. Unlike get(), this method can block (in the case of * FutureValueReference). * * @throws ExecutionException if the loading thread throws an exception * @throws ExecutionError if the loading thread throws an error */ V waitForValue() throws ExecutionException; /** * Returns the weight of this entry. This is assumed to be static between calls to setValue. */
int getWeight(); /** * Returns the entry associated with this value reference, or {@code null} if this value reference is * independent of any entry. */ @Nullable ReferenceEntry<K, V> getEntry(); /** * 爲一個指定的entry建立一個該引用的副本 * <p> * {@code value} may be null only for a loading reference. */ ValueReference<K, V> copyFor(ReferenceQueue<V> queue, @Nullable V value, ReferenceEntry<K, V> entry); /** * 告知一個新的值正在加載中。這個只會關聯到加載值引用。 */
void notifyNewValue(@Nullable V newValue); /** * 當一個新的value正在被加載的時候,返回true。無論是否已經有存在的值。這裏加鎖方法返回的值對於給定的ValueReference實例來講是常量。 * */ boolean isLoading(); /** * 返回true,若是該reference包含一個活躍的值,意味着在cache裏仍然有一個值存在。活躍的值包含:cache查找返回的,等待被移除的要被驅趕的值; 非激活的包含:正在加載的值, */ boolean isActive(); }

 Notes:value引用接口對象中包含了不一樣狀態的標記,以及一些加載方法和獲取具體value值對象。

爲了減小沒必要須的load加載,在value引用中增長了loading標識和wait方法等待加載獲取值。這樣,就能夠等待上一個調用loader方法獲取值,而不是重複去調用loader方法加劇系統負擔,並且能夠更快的獲取對應的值。

此外,介紹下 ReferenceQueue 引用隊列,這個隊列是JDK提供的,在檢測到適當的可到達性更改後,垃圾回收器將已註冊的引用對象添加到該隊列中。由於Cache使用了各類引用,而經過ReferenceQueue這個「監聽器」就能夠優雅的實現自動刪除那些引用不可達的key了,是否是很吊,哈哈。

在Cache分別實現了基於Strong,Soft,Weak三種形式的ValueReference實現。

這裏ValueReference之因此要有對ReferenceEntry的引用是由於在Value由於WeakReference、SoftReference被回收時,須要使用其key將對應的項從Segment段中移除; 
copyFor()函數的存在是由於在expand(rehash)從新建立節點時,對WeakReference、SoftReference須要從新建立實例(C++中的深度複製思想,就是爲了保持對象狀態不會相互影響),而對強引用來講,直接使用原來的值便可,這裏很好的展現了對彼變化的封裝思想; 
notifiyNewValue只用於LoadingValueReference,它的存在是爲了對LoadingValueReference來講能更加及時的獲得CacheLoader加載的值。

5.3 Segment 數據結構
Segment 數據結構,是ConcurrentHashMap的核心實現,也是該結構保證了其算法的高效性。在 Guava Cache 中也同樣, segment 數據結構保證了緩存在線程安全的前提下能夠高效地更新,插入,獲取對應value。

實際上,segment就是一個特殊版本的hash table實現。其內部也是對應一個鎖,不一樣的是,對於get和put操做作了一些優化處理。所以,在代碼實現的時候,爲了快速開發和利用已有鎖特性,直接 extends ReentrantLock 。

在segment中,其主要的類屬性就是一個 LoacalCache 類型的map變量。關於segment實現說明,以下:

/** * segments 維護一個entry列表的table,確保一致性狀態。因此能夠不加鎖去讀。節點的next field是不可修改的final,由於全部list的增長操做 * 是執行在每一個容器的頭部。所以,這樣子很容易去檢查變化,也能夠快速遍歷。此外,當節點被改變的時候,新的節點將被建立而後替換它們。 因爲容器的list通常都比較短(平均長度小於2),因此對於hash * tables來講,能夠工做的很好。雖說讀操做所以能夠不須要鎖進行,可是是依賴 * 使用volatile確保其餘線程完成寫操做。對於絕大多數目的而言,count變量,跟蹤元素的數量,其做爲一個volatile變量確保可見性(其內部原理能夠參考其餘相關博文)。 * 這樣一會兒變得方便的不少,由於這個變量在不少讀操做的時候都會被獲取:全部非同步的(unsynchronized)讀操做必須首先讀取這個count值,而且若是count爲0則不會 查找table * 的entries元素;全部的同步(synchronized)操做必須在結構性的改變任務bin容器以後,纔會寫操做這個count值。 * 這些操做必須在併發讀操做看到不一致的數據的時候,不採起任務動做。在map中讀操做性質能夠更容易實現這個限制。例如:沒有操做能夠顯示出 當table * 增加了,可是threshold值沒有更新,因此考慮讀的時候不要求原子性。做爲一個原則,全部危險的volatile讀和寫count變量都必須在代碼中標記。 */ final LocalCache<K, V> map; /** * 該segment區域內全部存活的元素個數 */
volatile int count; /** * 改變table大小size的更新次數。這個在批量讀取方法期間保證它們能夠看到一致性的快照: * 若是modCount在咱們遍歷段加載大小或者覈對containsValue期間被改變了,而後咱們會看到一個不一致的狀態視圖,以致於必須去重試。 * count+modCount 保證內存一致性 * * 感受這裏有點像是版本控制,好比數據庫裏的version字段來控制數據一致性 */
int modCount; /** * 每一個段表,使用樂觀鎖的Array來保存entry The per-segment table. */
volatile AtomicReferenceArray<ReferenceEntry<K, V>> table; // 這裏和concurrentHashMap不一致,緣由是這邊元素是引用,直接使用不會線程安全 /** * A queue of elements currently in the map, ordered by write time. Elements are added to the tail of the queue * on write. */ @GuardedBy("Segment.this") final Queue<ReferenceEntry<K, V>> writeQueue; /** * A queue of elements currently in the map, ordered by access time. Elements are added to the tail of the queue * on access (note that writes count as accesses). */ @GuardedBy("Segment.this") final Queue<ReferenceEntry<K, V>> accessQueue;

 


Notes:

在segment實現中,不少地方使用count變量和modCount變量來保持線程安全,從而省掉lock開銷。

在本文上面的圖中說明了每一個segment就是一個節點table,和jdk實現不一致,這裏爲了GC,內部維護的是一個 AtomicReferenceArray<ReferenceEntry<K, V>> 類型的列表,能夠保證安全性。

最後, LocalCache 做爲一個緩存,其必須具備訪問和寫超時特性,由於其內部維護了訪問隊列和寫隊列,隊列中的元素按照訪問或者寫時間排序,新的元素會被添加到隊列尾部。若是,在隊列中已經存在了該元素,則會先delete掉,而後再尾部add該節點,新的時間。這也就是爲何,對於 LocalCache 而言,其LRU是針對segment的,而不是全Cache範圍的。

在本文的 5.2節中知道,cache會根據初始化實例時配置來建立多個segment( createSegment ),而後該方法最終調用segment類的構造函數建立一個段。對於參數set,就不展現,看看構造方法中其主要操做:

// 構造函數
Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) { initTable(newEntryArray(initialCapacity)); } AtomicReferenceArray<ReferenceEntry<K, V>> newEntryArray(int size) { return new AtomicReferenceArray<ReferenceEntry<K, V>>(size); } void initTable(AtomicReferenceArray<ReferenceEntry<K, V>> newTable) { this.threshold = newTable.length() * 3 / 4; // 0.75
if (!map.customWeigher() && this.threshold == maxSegmentWeight) { // prevent spurious expansion before eviction
this.threshold++; } this.table = newTable; }

 


OK,這裏咱們已經構造好了整個localCache對象了,包括其內部每一個segment中對應的節點表。這些節點table,決定了最後全部核心操做的具體實現和操做結果。

接下來,須要看看最核心的幾個方法。

Tips:本文把這幾個方法單獨做爲幾節來講明,這也表示這幾個方法的重要性。

 

Notes:上面從緩存中直接獲取key對應value,是徹底沒有加鎖來完成的。

scheduleRefresh方法

若是配置refresh特性,到了配置的刷新間隔時間,並且節點也沒有正在加載,則應該進行refresh操做。refresh操做比較複雜。 

   

 6、Guava Cache中數據的定位 兩次Hash 相似於ConcurrentHashMap

 

其實 Guava Cache 爲了知足併發場景的使用,核心的數據結構就是按照 ConcurrentHashMap 來的,這裏也是一個 key 定位到一個具體位置的過程。 

先找到 Segment,再找具體的位置,等因而作了兩次 Hash 定位。 

640?wx_fmt=jpeg

 

主要的類:

CacheBuilder 設置參數,構建Cache

LocalCache 是核心實現,雖然builder構建的是LocalLoadingCache(帶refresh功能)和LocalManualCache(不帶refresh功能),但其實那兩個只是個殼子

一、CacheBuilder 構建器

提要:
記錄所需參數

public final class CacheBuilder<K, V> { public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build( CacheLoader<? super K1, V1> loader) { // loader是用來自動刷新的
 checkWeightWithWeigher(); return new LocalCache.LocalLoadingCache<>(this, loader); } public <K1 extends K, V1 extends V> Cache<K1, V1> build() { // 這個沒有loader,就不會自動刷新
 checkWeightWithWeigher(); checkNonLoadingCache(); return new LocalCache.LocalManualCache<>(this); } int initialCapacity = UNSET_INT; // 初始map大小
    int concurrencyLevel = UNSET_INT; // 併發度
    long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; Weigher<? super K, ? super V> weigher; Strength keyStrength; // key強、弱、軟引,默認爲強
    Strength valueStrength; // value強、弱、軟引,默認爲強
    long expireAfterWriteNanos = UNSET_INT; // 寫過時
    long expireAfterAccessNanos = UNSET_INT; //     long refreshNanos = UNSET_INT; //     Equivalence<Object> keyEquivalence; // 強引時爲equals,不然爲==
    Equivalence<Object> valueEquivalence; // 強引時爲equals,不然爲==
    RemovalListener<? super K, ? super V> removalListener; // 刪除時的監聽
    Ticker ticker; // 時間鍾,用來得到當前時間的
    Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER; // 計數器,用來記錄get或者miss之類的數據
}

 

二、LocalCache  

在上文的分析中能夠看出 Cache 中的 ReferenceEntry 是相似於 HashMap 的 Entry 存放數據的。

來看看 ReferenceEntry 的定義:

interface ReferenceEntry<K, V> { allBackListener { /** * Returns the value reference from this entry. */ ValueReference<K, V> getValueReference(); /** ify(String msg) ; * Sets the value reference for this entry. */                                                                                                                              
  void setValueReference(ValueReference<K, V> valueReference); /** * Returns the next entry in the chain. */ @Nullable ReferenceEntry<K, V> getNext(); /** * Returns the entry's hash. */                                                                                                                              
  int getHash(); /** * Returns the key for this entry. */ @Nullable K getKey(); /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. * New entries are added at the tail of the list at write time; stale entries are expired from * the head of the list. */                                                                                                                              
                                                                                                                                   
  /** * Returns the time that this entry was last accessed, in ns. */                                                                                                                              
  long getAccessTime(); /** * Sets the entry access time in ns. */                                                                                                                              
  void setAccessTime(long time); }

 包含了不少經常使用的操做,如值引用、鍵引用、訪問時間等。

根據 ValueReference<K, V> getValueReference(); 的實現: 

具備強引用和弱引用的不一樣實現。

key 也是相同的道理: 

 當使用這樣的構造方式時,弱引用的 key 和 value 都會被垃圾回收。

固然咱們也能夠顯式的回收:

/** * Discards any cached value for key {@code key}. * 單個回收 */                                                                 
 void invalidate(Object key); /** * Discards any cached values for keys {@code keys}. * * @since 11.0 */                                                                 
 void invalidateAll(Iterable<?> keys); /** * Discards all entries in the cache. */                                                                 
 void invalidateAll();

 

回調

改造了以前的例子:

loadingCache = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.SECONDS) .removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { LOGGER.info("刪除緣由={},刪除 key={},刪除 value={}",notification.getCause(),notification.getKey(),notification.getValue()); } }) .build(new CacheLoader<Integer, AtomicLong>() { @Override public AtomicLong load(Integer key) throws Exception { return new AtomicLong(0); } }); 
執行結果:
12018-07-15 20:41:07.433 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 當前緩存值=0,緩存大小=1
22018-07-15 20:41:07.442 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 緩存的全部內容={1000=0} 32018-07-15 20:41:07.443 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - job running times=10
42018-07-15 20:41:10.461 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 刪除緣由=EXPIRED,刪除 key=1000,刪除 value=1
52018-07-15 20:41:10.462 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 當前緩存值=0,緩存大小=1
62018-07-15 20:41:10.462 [main] INFO  c.crossoverjie.guava.CacheLoaderTest - 緩存的全部內容={1000=0}
能夠看出當緩存被刪除的時候會回調咱們自定義的函數,並告知刪除緣由。

那麼 Guava 是如何實現的呢? 

根據 LocalCache 中的 getLiveValue() 中判斷緩存過時時,跟着這裏的調用關係就會一直跟到: 

 removeValueFromChain() 中的:

 enqueueNotification() 方法會將回收的緩存(包含了 key,value)以及回收緣由包裝成以前定義的事件接口加入到一個本地隊列中。 

 這樣一看也沒有回調咱們初始化時候的事件啊。

不過用過隊列的同窗應該能猜出,既然這裏寫入隊列,那就確定就有消費。

咱們回到獲取緩存的地方: 

 在 finally 中執行了 postReadCleanup() 方法;其實在這裏面就是對剛纔的隊列進行了消費: 

 一直跟進來就會發現這裏消費了隊列,將以前包裝好的移除消息調用了咱們自定義的事件,這樣就完成了一次事件回調。  

 

8、Guava Cache特性:對於同一個key,只讓一個請求回源load數據,其餘線程阻塞等待結果 (問題4 如何作到的)

KeyReferenceQueue:  基於引用的Entry,其實現類有弱引用Entry,強引用Entry等 ,已經被GC,須要內部清理的鍵引用隊列。

ValueReference

對於ValueReference,由於Guava Cache支持強引用的Value、SoftReference Value以及WeakReference Value,於是它對應三個實現類:StrongValueReference、SoftValueReference、WeakValueReference。爲了支持動態加載機制,它還有一個LoadingValueReference,在須要動態加載一個key的值時,先把該值封裝在LoadingValueReference中,以表達該key對應的值已經在加載了,若是其餘線程也要查詢該key對應的值,就能獲得該引用,而且等待改值加載完成,從而保證該值只被加載一次(能夠在evict之後從新加載)。

 

 

在該只加載完成後,將LoadingValueReference替換成其餘ValueReference類型。對新建立的LoadingValueReference,因爲其內部oldValue的初始值是UNSET,它isActive爲false,isLoading爲false,於是此時的LoadingValueReference的isActive爲false,可是isLoading爲true。每一個ValueReference都紀錄了weight值,所謂weight從字面上理解是「該值的重量」,它由Weighter接口計算而得。weight在Guava Cache中由兩個用途:1. 對weight值爲0時,在計算由於size limit而evict是忽略該Entry(它能夠經過其餘機制evict);2. 若是設置了maximumWeight值,則當Cache中weight和超過了該值時,就會引發evict操做。可是目前還不知道這個設計的用途。最後,Guava Cache還定義了Stength枚舉類型做爲ValueReference的factory類,它有三個枚舉值:Strong、Soft、Weak,這三個枚舉值分別建立各自的ValueReference,而且根據傳入的weight值是否爲1而決定是否要建立Weight版本的ValueReference。 

設想高併發下的一種場景:假設咱們將name=aty存放到緩存中,並設置的有過時時間。當緩存過時後,剛好有10個客戶端發起請求,須要讀取name的值。使用Guava Cache能夠保證只讓一個線程去加載數據(好比從數據庫中),而其餘線程則等待這個線程的返回結果。這樣就能避免大量用戶請求穿透緩存。 

9、總結 

一、guava使用的場景:

在日常開發過程當中,不少狀況須要使用緩存來避免頻繁SQL查詢或者其餘耗時操做,會採起緩存這些操做結果給下一次請求使用。若是咱們的操做結果是一直不改變的,其實咱們能夠使用 ConcurrentHashMap 來存儲這些數據;可是若是這些結果在隨後時間內會改變或者咱們但願存放的數據所佔用的內存空間可控,這樣就須要本身來實現這種數據結構了。

顯然,對於這種十分常見的需求, Guava 提供了本身的工具類實現。 Guava Cache 提供了通常咱們使用緩存所須要的幾乎全部的功能,主要有:

(1) 自動將entry節點加載進緩存結構中;

(2)當緩存的數據已經超過預先設置的最大值時,使用LRU算法移除一些數據;

(3)具有根據entry節點上次被訪問或者寫入的時間來計算過時機制;

(4)緩存的key被封裝在`WeakReference`引用內;

(5)緩存的value被封裝在`WeakReference`或者`SoftReference`引用內;

(6)移除entry節點,能夠觸發監聽器通知事件;

緩存加載:CacheLoader、Callable、顯示插入(put)

緩存回收:LRU,定時(expireAfterAccessexpireAfterWrite),軟弱引用,顯示刪除(Cache接口方法invalidateinvalidateAll)

監聽器:CacheBuilder.removalListener(RemovalListener)

清理緩存時間:只有在獲取數據時才或清理緩存LRU,使用者能夠單起線程採用Cache.cleanUp()方法主動清理。

刷新:主動刷新方法LoadingCache.referesh(K)

信息統計:CacheBuilder.recordStats() 開啓Guava Cache的統計功能。Cache.stats() 返回CacheStats對象。(其中包括命中率等相關信息)

獲取當前緩存全部數據:cache.asMap(),cache.asMap().get(Object)會刷新數據的訪問時間(影響的是:建立時設置的在多久沒訪問後刪除數據)

 

 

     

對於Guava Cache 對於其核心實現我會作以下的設計:

    1. 定義一個CacheConfig類用於紀錄全部的配置,如CacheLoader,maximum size、expire time、key reference level、value reference level、eviction listener等。
    2. 定義一個Cache接口,該接口相似Map(或ConcurrentMap),可是爲了和Map區別開來,於是從新定義一個Cache接口。
    3. 定義一個實現Cache接口的類CacheImpl,它接收CacheConfig做爲參數的構造函數,並將CacheConfig實例保存在字段中。
    4. 在實現上模仿ConcurrentHashMap的實現方式,有一個Segment數組,其長度由配置的concurrencyLevel值決定。爲了實現最近最少使用算法(LRU),添加AccessQueue和WriteQueue字段,這兩個Queue內部採用雙鏈表,每次新建立一個Entry,就將這個Entry加入到這兩個Queue的末尾,而每讀取一個Entry就將其添加到AccessQueue的末尾,沒更新一個Entry將該Entry添加到WriteQueue末尾。爲了實現key和value上的WeakReference、SoftReference,添加ReferenceQueue<K>類型的keyReferenceQueue和valueReferenceQueue字段。
    5. 在每次調用方法以前都遍歷AccessQueue和WriteQueue,若是發現有Entry已經expire,就將該Entry從這兩個Queue上和Cache中移除。而後遍歷keyReferenceQueue和valueReference,若是發現有項存在,一樣將它們移除。在移除時若是有EvictionListener註冊着,則調用該listener。
    6. 對Segment實現,它時一個CacheEntry數組,CacheEntry是一個鏈節點,它包含hash、key、vlaue、next。CacheEntry根據是否須要包裝在WeakReference中建立WeakEntry或StrongEntry,而對value根據是否須要包裝在WeakReference、SoftReference中建立WeakValueReference、SoftValueReference、StrongValueReference。在get操做中對於須要使用CacheLoader加載的值先添加一個具備LoadingValueReference值的Entry,這樣能夠保證同一個Key只加載依次。在加載成功後將LoadingValueReference根據配置替換成其餘Weak、Soft、Strong ValueReference。
    7. 對於cache access statistics,只須要有一個類在須要的地方作一些統計計數便可。

源碼參考:guava源碼

參考:Guava學習:Cache緩存入門

參考:Guava Cache特性:對於同一個key,只讓一個請求回源load數據,其餘線程阻塞等待結果

參考:Guava cacha 機制及源碼分析

參考:本地緩存google Guava

參考:本地緩存之神-guava cache

參考:Google Guava Cache 全解析

參考:Guava源碼分析刪除過時緩存 

參考:Guava Cache 緩存實現與源碼分析

參考:Guava 源碼分析(Cache 原理【二階段】)

 參考:源碼分析:Guava Cache原理以及源碼分析

參考:Google Guava官方教程(中文版)

參考:Google Guava Cache詳解及使用場景

參考:分佈式系統緩存系列之guava cache

參考:Guava Cache 使用學習

參考:集中式內存緩存Guava Cache

參考:Java Cache系列之Guava Cache實現詳解

參考:Guava Cache源碼詳解

參考:爲何要用redis而不用map/guava cache作緩存?

相關文章
相關標籤/搜索