Guava Cache:核心引數深度剖析和原始碼分析
目錄
2.3 加權器weigher和最大加權值maximumWeight
2.4 寫後超時expireAfterWrite和讀後超時expireAfterRead
1、核心引數概覽
2、核心引數分功能分析
2.1 初始容量initialCapacity
1、主要作用:
initialCapacity表示初始容量,在快取建立的過程中進行設定。注意事項:
- 不可重複設定:初始容量只能設定一次有效值,否則會丟擲異常;
- 最小值限制:初始容量應該大於等於0,否則會丟擲異常;
- 最大值限制:initialCapacity會從2的30次方、maximumWeight、CacheBuilder的傳入值中取最小值;
- 併發性相關:initialCapacity最好設定成段數segmentCount(segmentCount的計算參加:? )的整數倍,因為initialCapacity用來設定段的初始容量segmentCapacity,segmentCapacity取值即initialCapacity除以segmentCount並向上取整。
2、核心程式碼:
1)CacheBuilder中設定初始容量的程式碼:
public CacheBuilder<K, V> initialCapacity(int initialCapacity) {
checkState(
this.initialCapacity == UNSET_INT,
"initial capacity was already set to %s",
this.initialCapacity);
checkArgument(initialCapacity >= 0);
this.initialCapacity = initialCapacity;
return this;
}
2)LocalCache構造器中initialCapacity取值:
int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = (int) Math.min(initialCapacity, maxWeight);
}
3)LocalCache構造器中initialCapacity用來計算段的初始容量:
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
}
2.2 最大容量maximumSize
1、主要作用:
用來形容快取的最大容量,以避免快取過多導致記憶體洩露。注意事項:
- 實質:maximumSize主要是用來設定maxWeight,在快取內部是通過maxWeight來限制快取容量的;
- 不可重複設定:最大容量只能設定一次有效值,否則會丟擲異常;最大容量和最大權重maxWeight只允許設定一個,否則會丟擲異常;
和最大權重對比:
- 相同性:在快取沒有特殊加權計算的情況下,兩者等價,只需設定任意即可;
- 差異性1:有特殊加權值計算(指定weigher)的場景,必須也只能使用最大權重maximumWeight;
- 差異性2:在沒有特殊加權值計算(指定weigher)的場景,必須也只能使用最大大小maximumSize;
- 差異性3:在快取例項內部,成員變數使用的是最大權重maximumWeight,maximumSize會通過轉化成maxWeight來生效。
2、核心程式碼:
1)CacheBuilder中設定最大容量的程式碼:
public CacheBuilder<K, V> maximumSize(long maximumSize) {
checkState(
this.maximumSize == UNSET_INT, "maximum size was already set to %s", this.maximumSize);
checkState(
this.maximumWeight == UNSET_INT,
"maximum weight was already set to %s",
this.maximumWeight);
checkState(this.weigher == null, "maximum size can not be combined with weigher");
checkArgument(maximumSize >= 0, "maximum size must not be negative");
this.maximumSize = maximumSize;
return this;
}
2)獲取最大加權值:maximumSize和maximumWeight是否生效取決於是否指定了weigher
long getMaximumWeight() {
if (expireAfterWriteNanos == 0 || expireAfterAccessNanos == 0) {
return 0;
}
return (weigher == null) ? maximumSize : maximumWeight;
}
2.3 加權器weigher和最大加權值maximumWeight
1、加權的主要作用:
加權器weigher主要用來根據快取的key-value來設定不同的加權值。加權值表示佔據的記憶體的基本單後設資料的多少。注意事項:
- 預設值:CacheBuilder中可以不設定weigher,此時weigher為null;構造快取LocalCache例項時,如果weigher為null則使用OneWeigher.INSTANCE;
- 和最大容量引數的關係:指定weigher時,最大容量只能使用maximumWeight;不指定weigher時,最大容量只能使用maximumSize;
- 自定義方式:實現weigher介面
2、最大加權值的作用:
最大加權值,在CacheBuilder中使用欄位名maximumWeight,在LocalCache中使用欄位名maxWeight,用來描述快取的最大容量。注意事項:
- 使用場景:只有在設定了weigher的時候,才會使用maximumWeight;否則使用maximumSize;
- 內部機制:在快取LocalCache的內部,使用的是maxWeight;如果沒有指定weighter,會將CacheBuilder中的maximumSize讀取為maxWeight;
- 生效方式:對每個分段計算一個最大分段加權值;
- 預設值:-1,此時不會限制快取容量;-1無法顯示設定,不設定即為-1.
3、核心程式碼:
1)實現weigher的例子:
private static class MyWeigher implements Weigher<String, String> {
@Override
public int weigh(String key, String value) {
return value.length() / 1000;
}
}
2)LocalCache中呼叫Builder的方法獲取最大加權值:
// LocalCache的構造器中獲取maxWeight
maxWeight = builder.getMaximumWeight();
// CacheBuilder的getMaximumWeight方法
long getMaximumWeight() {
if (expireAfterWriteNanos == 0 || expireAfterAccessNanos == 0) {
return 0;
}
return (weigher == null) ? maximumSize : maximumWeight;
}
3)對段生效的方式:
// 構造器中maxWeight對段生效的程式碼
// segmentCapacity = initialCapacity 除以 segmentCount 向上取整
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
}
// segmentSize = 不小於segmentCapacity的 最小的 2的整數冪
// segmentSize用作段的初始容量
int segmentSize = 1;
while (segmentSize < segmentCapacity) {
segmentSize <<= 1;
}
// 是否限制容量
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) {
// 從第餘數段開始,段容量減1,以保證各段容量之和等於總容量
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());
}
}
// 是否會根據容量進行淘汰
boolean evictsBySize() {
return maxWeight >= 0;
}
2.4 寫後超時expireAfterWrite和讀後超時expireAfterRead
1、寫後超時的作用:
寫後超時expireAfterWrite描述的是,快取寫入多長時間後會失效。快取失效後,如果再次方法,如果設定了載入器CacheLoader,會重新載入;如果未設定CacheLoader,或者CacheLoader未獲取到相應快取,則會返回異常,提示獲取不到相應的快取。
2、訪問後超時的作用:
訪問後超時expireAfterAccess描述的是,快取讀取多長時間後會失效。每次訪問(包括讀和寫)都會重置時間。快取失效後,如果再次方法,如果設定了載入器CacheLoader,會重新載入;如果未設定CacheLoader,或者CacheLoader未獲取到相應快取,則會返回異常,提示獲取不到相應的快取。
3、注意事項:
- 每次寫入的時候,也會重置訪問的時間
4、核心程式碼:
1)記錄寫操作:
@GuardedBy("this")
void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) {
// we are already under lock, so drain the recency queue immediately
drainRecencyQueue();
totalWeight += weight;
if (map.recordsAccess()) {
entry.setAccessTime(now);
}
if (map.recordsWrite()) {
entry.setWriteTime(now);
}
accessQueue.add(entry);
writeQueue.add(entry);
}
2)記錄讀操作:
void recordRead(ReferenceEntry<K, V> entry, long now) {
if (map.recordsAccess()) {
entry.setAccessTime(now);
}
recencyQueue.add(entry);
}
3)快取超時淘汰:
@GuardedBy("this")
void expireEntries(long now) {
drainRecencyQueue();
ReferenceEntry<K, V> e;
while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
throw new AssertionError();
}
}
}
4)超限導致的淘汰:
@GuardedBy("this")
void evictEntries(ReferenceEntry<K, V> newest) {
if (!map.evictsBySize()) {
return;
}
drainRecencyQueue();
// If the newest entry by itself is too heavy for the segment, don't bother evicting
// anything else, just that
if (newest.getValueReference().getWeight() > maxSegmentWeight) {
if (!removeEntry(newest, newest.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
while (totalWeight > maxSegmentWeight) {
ReferenceEntry<K, V> e = getNextEvictable();
if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
}
2.5 寫後重新整理refreshAfterWrite
1、主要作用:
寫後重新整理refreshAfterWrite主要作用是,在快取寫入一定時間後,再次訪問會先使用CacheLoader去重新整理快取;如果重新整理失敗,或者有其他任務正在重新整理快取,則會返回現有的快取值。注意事項:
- 非同步和同步:重新整理是非同步進行的,但是第一次請求重新整理的服務執行緒,會阻塞等待非同步重新整理完成;
- 預設值:如果重新整理快取失敗,則會返回現有的舊值;
- 併發性:對於同一個key,如果已經正在重新整理了,則後續的所有任務不會阻塞,而是直接返回現在的舊值。
2、refreshAfterWrite和expireAfterWrite對比:
- 在指定CacheLoader的情況下,refreshAfterWrite在CacheLoader載入不到時,會返回現有的舊值;expireAfterWrite在CacheLoader載入不到結果時,會丟擲異常提示獲取不到快取;
- 在未指定CacheLoader的情況下,不允許設定refreshAfterWrite,否則會報錯;但是可以設定expireAfterWrite,但是此時在get方法中必須指定CacheLoader;
- 併發訪問超時的情況下,refreshAfterWrite的超時機制,會使第一次訪問的執行緒阻塞等待重新整理執行緒的結果,但是重新整理過程中如果有其他執行緒訪問,會直接返回現在的舊值;而expireAfterWrite的超時機制,會清除讓現有的值,並讓全部訪問這個key的快取的執行緒,都阻塞等待非同步重新整理的結果。
3、核心程式碼:
1)第一條執行緒發現超過refreshNanos,執行重新整理;後面的執行緒訪問到正在重新整理的快取時,直接返回舊值:
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;
}
2)第一條觸發refresh邏輯的執行緒,阻塞等待非同步執行結果:
@Nullable
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
// 插入一個LoadingValueReference,這樣後面的執行緒訪問到的時候,可以知道這個快取正在被重新整理
final LoadingValueReference<K, V> loadingValueReference =
insertLoadingValueReference(key, hash, checkTime);
if (loadingValueReference == null) {
return null;
}
// 非同步執行,生成future佔位符
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;
}
3)非同步重新整理快取的具體邏輯:
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方法去設定快取的值,並記錄統計結果
getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception thrown during refresh", t);
loadingValueReference.setException(t);
}
}
},
directExecutor());
return loadingFuture;
}
2.6 併發級別concurrencyLevel
1、主要作用:
併發級別concurrencyLevel用來控制快取的併發處理能力。注意事項:
- concurrencyLevel最大值是2的16次冪(65535),最小值是1,預設值是4;
- concurrencyLevel是通過設定分段數量segmentCount,來生效的;
- concurrencyLevel並不一定完全等同於segmentCount,segmentCount的取值還跟最大加權值maxWeight有關。
2、分段數量segmentCount的取值規則:
- segmentCount是2的整數倍;
- segmentCount在允許的取值範圍內取最大值;
- concurrencyLevel的約束:1/2 * segmentCount滿足:小於concurrencyLevel ;
- maxWeight的約束:如果maxWeight < 0(不限制快取最大容量),則對segmentCount無影響;如果設定了有效的maxWeight,則 1/2 * segmentCount 小於等於1/20 * maxWeight。
3、concurrencyLevel取值的特點:
- 在maxWeight影響不明顯的情況下(不設定maxWeight,或者maxWeight遠大於concurrencyLevel的情況),concurrencyLevel實際生效的值,是不小於它的最小的2的整數倍。例如concurrencyLevel = 7或者concurrencyLevel = 8時,如果可以忽略maxWeight的影響,segmentCount計算得到的數值為8;
- 設定了maxWeight的情況,concurrencyLevel應該明顯小於最大加權值maxWeight:即concurrencyLevel設定成任意大於等於(1/20 * maxWeight + 1)的數值,和設定成(1/20 * maxWeight + 1)的效果是一樣的。
4、核心程式碼
1)segmentCount的計算邏輯:
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
// 這時的segmentShift還是表示segmentCount是2的多少次冪
++segmentShift;
// segmentCount是滿足while條件的最大值的2倍
segmentCount <<= 1;
}
// 最終的segmentShift用於取hash的高位的相應位數,用來計算尋找一個元素在哪個segment中
this.segmentShift = 32 - segmentShift;
// 掩碼,取hash的低位的相應位數的值,即為在segment中的角標
segmentMask = segmentCount - 1;
// 是否限制容量
boolean evictsBySize() {
return maxWeight >= 0;
}
2)concurrencyLevel取值限制:
// 構造器中獲取concurrencyLevel.最大值是2的16次冪
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
// CacheBuilder的方法,用來提供設定的concurrencyLevel,或者預設的concurrencyLevel。預設為4
int getConcurrencyLevel() {
return (concurrencyLevel == UNSET_INT) ? DEFAULT_CONCURRENCY_LEVEL : concurrencyLevel;
}
2.7 時間源ticker
1、主要作用:
時間源工具Ticker是快取內部獲取時間的工具,納秒級精度。Ticker用來在快取內部測量流逝的時間,從而計算快取是否超時、是否需要重新整理等。注意事項:
- 使用者可以實現Ticker介面,以定製自己需要的Ticker;
- 一般在有測試需求的時候才需要定製Ticker,比如快取超時時間的1小時,可以通過讓Ticker的read方法返回的1小時後的納秒時間戳,來觀察1小時候的快取變化;
- 預設使用Ticker.SYSTEM_TICKER,呼叫System.nanoTime()獲取時間戳。
2、重要程式碼:
1)定製Ticker:
private static class MyTicker extends Ticker {
private long start = Ticker.systemTicker().read();
private long elapsedTime = 0L;
@Override
public long read() {
return start + elapsedTime;
}
// 開發給使用者,用來加速時間流逝
public void setElapsedTime(long nanos) {
this.elapsedTime = nanos;
}
}
2.8 快取移除監聽器removalListener
1、主要作用:
快取移除監聽器removalListener,在快取被移除的時候會收到一條通知,通知攜帶被移除快取的key、value、移除原因。
2、移除原因RemovalCause:
- EXPLICIT:使用者顯式移除了快取;非被驅逐,即wasEvicted方法返回false;
- REPLACED:使用者顯式替換了快取;非被驅逐,即wasEvicted方法返回false;
- COLLECTED:被垃圾回收器回收了key或者value。被驅逐,即wasEvicted方法返回true;
- EXPIRED:快取超時被移除。被驅逐,即wasEvicted方法返回true;
- SIZE:容量超限被移除。被驅逐,即wasEvicted方法返回true。
3、重要程式碼:
1)定製RemovalListener:
private static class MyRemoveListener implements RemovalListener<String, String> {
@Override
public void onRemoval(RemovalNotification<String, String> notification) {
System.out.println(String.format("remove, key: %s, value: %s, cause: %s, wasEvicted: %s", notification.getKey(),
notification.getValue(), notification.getCause(), notification.wasEvicted()));
}
}
2.9 快取載入器CacheLoader
1、主要作用:
CacheLoader用於載入快取。
CacheBuilder提供了build()方法的兩種過載,提供了兩種設定CacheLoader的方式:
- 在構造快取時設定CacheLoader:使用build(CacheLoader loader)方法構造快取,此時會構造LoadingCache的例項,傳入的loader會被設定成預設的loader;
- 在獲取快取時指定CacheLoader 1:使用build()方法構造快取,會構造LocalCache.LocalMaualCache的例項。由於沒有指定預設的loader,需要在get方法中傳入loader,用於獲取不到快取、快取超時、快取需要重新整理時載入快取;
- 在獲取快取時指定CacheLoader 2:如果使用build(CacheLoader loader)方法構造快取構造LoadingCache的例項,依然可以在get方法中傳入CacheLoader,此時如果存在獲取不到快取、快取超時、快取需要重新整理等情況,會優先使用get方法中傳入的CacheLoader載入快取。
2、重要程式碼:
1)定製CacheLoader:
private static class Loader extends CacheLoader<String, String> {
@Override
public String load(String key) throws Exception {
return "value";
}
}
相關文章
- Guava 原始碼分析(Cache 原理)Guava原始碼
- spark核心原始碼深度剖析Spark原始碼
- Guava 原始碼分析(Cache 原理【二階段】)Guava原始碼
- Flutter Dio原始碼分析(三)--深度剖析Flutter原始碼
- Guava 原始碼分析之 EventBus 原始碼分析Guava原始碼
- ThreadLocal原始碼深度剖析thread原始碼
- Guava CacheGuava
- Java原始碼分析:Guava之不可變集合ImmutableMap的原始碼分析Java原始碼Guava
- Guava Cache 原理分析與最佳實踐Guava
- Spring cache原始碼分析Spring原始碼
- Spring AOP 原理原始碼深度剖析Spring原始碼
- linux系統下poll和epoll核心原始碼剖析Linux原始碼
- workerman 框架原始碼核心分析和註解框架原始碼
- Axios原始碼深度剖析 – AJAX新王者iOS原始碼
- Axios原始碼深度剖析 - AJAX新王者iOS原始碼
- Toast原始碼深度分析AST原始碼
- YARN 核心原始碼分析Yarn原始碼
- mmap核心原始碼分析原始碼
- 深度 Mybatis 3 原始碼分析(一)SqlSessionFactoryBuilder原始碼分析MyBatis原始碼SQLSessionUI
- Mybatis(五)--原始碼分析傳入單個list引數和多個list引數寫法MyBatis原始碼
- (翻譯)Google Guava CacheGoGuava
- cache2go – cachetable原始碼分析Go原始碼
- ArrayDeque(JDK雙端佇列)原始碼深度剖析JDK佇列原始碼
- guava cache大量的WARN日誌的問題分析Guava
- jQuery 原始碼剖析(一) - 核心功能函式jQuery原始碼函式
- 【Visual Leak Detector】核心原始碼剖析(VLD 2.5.1)原始碼
- 【Visual Leak Detector】核心原始碼剖析(VLD 1.0)原始碼
- ZStack原始碼剖析之核心庫鑑賞——Defer原始碼
- Qt核心剖析: 尋找 QObject 的原始碼薦QTObject原始碼
- jQuery原始碼剖析(三) - Callbacks 原理分析jQuery原始碼
- spark 原始碼分析之十三 -- SerializerManager剖析Spark原始碼
- Faiss原始碼剖析:類結構分析AI原始碼
- Google guava原始碼之EventBusGoGuava原始碼
- Guava原始碼淺析——JoinerGuava原始碼
- Java集合原始碼剖析——ArrayList原始碼剖析Java原始碼
- 【Java集合原始碼剖析】ArrayList原始碼剖析Java原始碼
- 【Java集合原始碼剖析】Vector原始碼剖析Java原始碼
- 【Java集合原始碼剖析】HashMap原始碼剖析Java原始碼HashMap