Caffeine Weigher
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
的场景中。