Weigher 接口
Weigher
?是 Caffeine 緩存庫中一個非常重要的函數式接口,它用于計算緩存中每個條目(entry)的權重(weight)。這個權重值主要用于基于容量的驅逐策略,特別是當你希望緩存的總大小不是基于條目數量,而是基于條目占用的“成本”(例如內存大小)時。
Weigher
?是一個泛型接口,接收鍵(K)和值(V)的類型作為參數。
// ... existing code ...
@NullMarked
@FunctionalInterface
public interface Weigher<K, V> {/*** Returns the weight of a cache entry. There is no unit for entry weights; rather they are simply* relative to each other.** @param key the key to weigh* @param value the value to weigh* @return the weight of the entry; must be non-negative*/int weigh(K key, V value);// ... existing code ...
}
語義分析:
@FunctionalInterface
: 這表明?Weigher
?是一個函數式接口,它只包含一個抽象方法?weigh
。這意味著可以使用 Lambda 表達式來方便地創建它的實例。int weigh(K key, V value)
: 這是接口的核心方法。- 它接收一個緩存條目的?
key
?和?value
?作為輸入。 - 它返回一個?
int
?類型的權重值。 - 關鍵語義:
- 權重無單位: Javadoc 中明確指出,權重沒有固定的單位(比如字節),它只是一個相對值。Caffeine 使用這些相對值來計算緩存的總權重。
- 非負性: 返回的權重值必須是非負的?(
>= 0
)。如果返回負值,會導致未定義的行為,后續我們會看到 Caffeine 如何通過?boundedWeigher
?來保證這一點。 - 使用場景: 當你使用?
Caffeine.newBuilder().maximumWeight(long)
?來配置緩存時,就必須提供一個?Weigher
?實現。Caffeine 會累加所有條目的權重,當總權重超過?maximumWeight
?時,就會觸發驅逐策略。
- 它接收一個緩存條目的?
Weigher
?接口內部定義了兩個靜態工廠方法和兩個內部實現類,以提供常用功能和增強安全性。
singletonWeigher()
// ... existing code .../*** Returns a weigher where an entry has a weight of {@code 1}.** @param <K> the type of keys* @param <V> the type of values* @return a weigher where an entry has a weight of {@code 1}*/static <K, V> Weigher<K, V> singletonWeigher() {@SuppressWarnings("unchecked")var instance = (Weigher<K, V>) SingletonWeigher.INSTANCE;return instance;}
// ... existing code ...
語義分析:
- 此方法返回一個默認的?
Weigher
?實現,該實現為每個緩存條目都返回權重?1
。 - 這實際上等價于基于條目數量的驅逐策略,即?
maximumWeight(N)
?和?maximumSize(N)
?在這種情況下效果相同。 - 它通過內部的?
SingletonWeigher
?enum 實現,這是一種高效且線程安全的單例模式。
boundedWeigher(Weigher<K, V> delegate)
// ... existing code .../*** Returns a weigher that enforces that the weight is non-negative.** @param delegate the weigher to weighs the entry* @param <K> the type of keys* @param <V> the type of values* @return a weigher that enforces that the weight is non-negative*/static <K, V> Weigher<K, V> boundedWeigher(Weigher<K, V> delegate) {return new BoundedWeigher<>(delegate);}
}
語義分析:
- 這是一個裝飾器(Decorator)方法。它接收一個用戶提供的?
Weigher
?(delegate),并返回一個新的?Weigher
?實例。 - 這個新的實例會調用用戶提供的?
Weigher
?來計算權重,但會額外增加一個檢查:確保返回的權重值是非負的。如果用戶實現返回了負數,BoundedWeigher
?會拋出?IllegalArgumentException
,從而保證了權重的合法性。 - Caffeine 內部在構建緩存時,會使用此方法來包裝用戶提供的?
Weigher
,以增加健壯性。
內部實現?SingletonWeigher
?和?BoundedWeigher
// ... existing code ...
enum SingletonWeigher implements Weigher<Object, Object> {INSTANCE;@Override public int weigh(Object key, Object value) {return 1;}
}final class BoundedWeigher<K, V> implements Weigher<K, V>, Serializable {private static final long serialVersionUID = 1;@SuppressWarnings("serial")final Weigher<? super K, ? super V> delegate;BoundedWeigher(Weigher<? super K, ? super V> delegate) {this.delegate = requireNonNull(delegate);}@Overridepublic int weigh(K key, V value) {int weight = delegate.weigh(key, value);requireArgument(weight >= 0);return weight;}Object writeReplace() {return delegate;}
}
語義分析:
SingletonWeigher
:- 使用?
enum
?實現單例,簡潔、高效、線程安全。 weigh
?方法簡單地返回?1
。
- 使用?
BoundedWeigher<K, V>
:- 這是一個 final 類,實現了?
Weigher
?和?Serializable
?接口。 - 構造函數接收一個?
delegate
?(用戶的?Weigher
?實現)。 weigh
?方法先調用?delegate.weigh
,然后使用?requireArgument(weight >= 0)
?檢查結果。writeReplace()
?方法是一個序列化技巧。當?BoundedWeigher
?實例被序列化時,實際被寫入流的是其內部的?delegate
?對象。這可以防止序列化不必要的包裝層。
- 這是一個 final 類,實現了?
BoundedWeigher 的名字意味著它為一個 Weigher 提供了邊界檢查的功能。它通過包裝(裝飾)另一個 Weigher,強制執行了 Weigher 接口文檔中“權重必須為非負數”的契約,為程序增加了健壯性。
Java 枚舉的結構
表面上看,enum
?是一個特殊的關鍵字,但實際上,它只是一個語法糖。當 Java 編譯器遇到?enum
?定義時,它會做幾件關鍵的事情:
- 創建一個?
final
?類:每個枚舉類型都會被編譯成一個?final
?的類,這個類隱式地繼承自?java.lang.Enum
。【所以自己實現的枚舉類不能再繼承】 - 創建?
public static final
?實例:枚舉中聲明的每一個常量(在?SingletonWeigher
?中就是?INSTANCE
)都會成為這個?final
?類的一個?public static final
?字段。這個字段的類型就是該枚舉類本身。 - 私有構造函數:枚舉的構造函數是隱式?
private
?的。你不能在外部通過?new
?來創建枚舉的實例。 - JVM 保證單例:JVM 在類加載的初始化階段,會執行一個靜態代碼塊(
<clinit>
),在這個階段,它會調用私有構造函數來創建并初始化所有枚舉常量實例。這個過程由 JVM 保證是線程安全的,并且每個枚舉常量在整個 JVM 生命周期中只會被實例化一次。
總結
Weigher
?接口為 Caffeine 提供了一種靈活的、基于權重的容量驅逐機制。開發者可以通過實現這個接口,根據業務需求自定義緩存條目的“成本”(例如,一個圖片對象的權重可以是其占用的字節數,一個列表的權重可以是其元素個數)。接口本身的設計通過靜態方法和內部類提供了默認實現和安全保障,使得該功能既強大又易于使用。
Async內部類AsyncWeigher
?
AsyncWeigher
?是?Async
?工具類中的一個靜態內部類。它的核心目的是為異步緩存中的條目計算權重。
在?AsyncCache
?中,緩存的值(Value)是一個?CompletableFuture<V>
,而不是直接的?V
。這意味著當你向緩存中放入一個條目時,實際的值可能還在計算中,尚未完成。這就帶來一個問題:如果我們要根據值的大小來計算權重,那么在一個?CompletableFuture
?還未完成時,我們是無法知道最終值的權重的。
AsyncWeigher
?就是為了解決這個問題而設計的。它充當了一個適配器(Adapter)或者說裝飾器(Decorator),將一個普通的?Weigher<K, V>
(計算最終值的權重)包裝成一個?Weigher<K, CompletableFuture<V>>
(計算?Future
?值的權重)。
我們來看一下它的代碼結構:
// ... existing code .../*** A weigher for asynchronous computations. When the value is being loaded this weigher returns* {@code 0} to indicate that the entry should not be evicted due to a size constraint. If the* value is computed successfully then the entry must be reinserted so that the weight is updated* and the expiration timeouts reflect the value once present. This can be done safely using* {@link java.util.Map#replace(Object, Object, Object)}.*/static final class AsyncWeigher<K, V> implements Weigher<K, CompletableFuture<V>>, Serializable {private static final long serialVersionUID = 1L;final Weigher<K, V> delegate;AsyncWeigher(Weigher<K, V> delegate) {this.delegate = requireNonNull(delegate);}@Overridepublic int weigh(K key, CompletableFuture<V> future) {return isReady(future) ? delegate.weigh(key, future.join()) : 0;}Object writeReplace() {return delegate;}}static boolean isReady(@Nullable CompletableFuture<?> future) {return (future != null) && future.isDone() && !future.isCompletedExceptionally();}
// ... existing code ...
implements Weigher<K, CompletableFuture<V>>, Serializable
:- 它實現了?
Weigher
?接口,但請注意泛型類型:鍵是?K
,而值是?CompletableFuture<V>
。這表明它的?weigh
?方法接收的是一個?Future
?對象。 - 實現?
Serializable
?接口是為了讓包含它的緩存實例可以被序列化。
- 它實現了?
final Weigher<K, V> delegate;
:- 這是一個關鍵字段,它持有一個“真正”的?
Weigher
?實例,這個實例知道如何根據最終的?V
?類型的值來計算權重。AsyncWeigher
?的工作就是委托給它。
- 這是一個關鍵字段,它持有一個“真正”的?
AsyncWeigher(Weigher<K, V> delegate)
:- 構造函數接收一個用戶定義的?
Weigher<K, V>
,并保存到?delegate
?字段。
- 構造函數接收一個用戶定義的?
?核心邏輯:weigh
?方法
weigh
?方法是?AsyncWeigher
?的核心所在。
// ... existing code ...@Overridepublic int weigh(K key, CompletableFuture<V> future) {return isReady(future) ? delegate.weigh(key, future.join()) : 0;}
// ... existing code ...
這里的邏輯非常清晰,可以分為兩種情況:
當?
CompletableFuture
?已經就緒 (isReady):isReady(future)
?是?Async
?類中的一個輔助方法,它檢查?future
?是否已正常完成并且結果不為?null
。- 如果?
future
?已經就緒,代碼會調用?future.join()
?來獲取最終的計算結果(類型為?V
)。 - 然后,它調用?
delegate.weigh(key, future.join())
,即使用用戶提供的原始?Weigher
?來計算這個真實值的權重,并返回該權重。
當?
CompletableFuture
?尚未就緒:- 如果?
future
?仍在計算中、計算失敗或結果為?null
,isReady(future)
?會返回?false
。 - 在這種情況下,
weigh
?方法直接返回?0
。
- 如果?
這個設計的精妙之處在于:
- 保護未完成的條目:對于正在加載的緩存條目,其權重為?
0
。這意味著在基于權重的驅逐策略下,這個條目幾乎不會因為容量問題被驅逐。這給了異步任務足夠的時間去完成,避免了“剛開始加載就被驅逐”的尷尬情況。 - 延遲權重計算:它將權重的實際計算推遲到值真正可用時。
Javadoc 中的重要提示
AsyncWeigher
?的 Javadoc 包含一段非常重要的說明:
If the value is computed successfully then the entry must be reinserted so that the weight is updated and the expiration timeouts reflect the value once present. This can be done safely using {@link java.util.Map#replace(Object, Object, Object)}.
中文解釋:當?CompletableFuture
?成功計算出值后,這個條目 必須被重新插入(reinserted) 到緩存中。
為什么需要這樣做?
因為當?Future
?完成后,它的權重從?0
?變成了一個實際的值。但緩存系統不會自動重新計算已有條目的權重。因此,需要手動觸發一次更新操作(例如?cache.put(key, future)
?或?cache.asMap().replace(key, future, future)
),這次操作會再次調用?AsyncWeigher.weigh
?方法。此時,由于?future
?已經就緒,weigh
?方法會返回真實的權重,緩存的總權重也隨之更新,驅逐策略便能正確工作。
Caffeine 的?AsyncLoadingCache
?在內部處理了?Future
?完成后的這個重入邏輯。
序列化處理:writeReplace
// ... existing code ...Object writeReplace() {return delegate;}
// ... existing code ...
這是一個 Java 序列化的優化。當?AsyncWeigher
?對象被序列化時,writeReplace
?方法會被調用。它不序列化?AsyncWeigher
?本身,而是返回其內部的?delegate
?對象。當反序列化時,會得到?delegate
?對象。Caffeine 在構建緩存時,如果發現是異步緩存,會再次用?AsyncWeigher
?包裝這個?delegate
。這樣做可以避免序列化不必要的包裝類,保持序列化數據的簡潔。
總結
AsyncWeigher
?是一個巧妙的裝飾器,它解決了在異步緩存中如何計算條目權重這一核心問題。它的策略是:
- 加載中,權重為0:保護正在進行的計算不被驅逐。
- 加載完成,計算真實權重:當?
Future
?完成后,通過委托給用戶提供的?Weigher
?來計算真實權重。 - 依賴重入更新:這個機制依賴于?
Future
?完成后對緩存條目的更新操作來刷新權重。
通過這種方式,AsyncWeigher
?完美地將同步的?Weigher
?模型適配到了異步?AsyncCache
?的場景中。