当前位置: 首页 > ds >正文

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 ...
}

语义分析:

  1. @FunctionalInterface: 这表明 Weigher 是一个函数式接口,它只包含一个抽象方法 weigh。这意味着可以使用 Lambda 表达式来方便地创建它的实例。
  2. 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 对象。这可以防止序列化不必要的包装层。

BoundedWeigher 的名字意味着它为一个 Weigher 提供了边界检查的功能。它通过包装(装饰)另一个 Weigher,强制执行了 Weigher 接口文档中“权重必须为非负数”的契约,为程序增加了健壮性。

Java 枚举的结构

表面上看,enum 是一个特殊的关键字,但实际上,它只是一个语法糖。当 Java 编译器遇到 enum 定义时,它会做几件关键的事情:

  1. 创建一个 final 类:每个枚举类型都会被编译成一个 final 的类,这个类隐式地继承自 java.lang.Enum。【所以自己实现的枚举类不能再继承】
  2. 创建 public static final 实例:枚举中声明的每一个常量(在 SingletonWeigher 中就是 INSTANCE)都会成为这个 final 类的一个 public static final 字段。这个字段的类型就是该枚举类本身。
  3. 私有构造函数:枚举的构造函数是隐式 private 的。你不能在外部通过 new 来创建枚举的实例。
  4. 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 ...

这里的逻辑非常清晰,可以分为两种情况:

  1. 当 CompletableFuture 已经就绪 (isReady):

    • isReady(future) 是 Async 类中的一个辅助方法,它检查 future 是否已正常完成并且结果不为 null
    • 如果 future 已经就绪,代码会调用 future.join() 来获取最终的计算结果(类型为 V)。
    • 然后,它调用 delegate.weigh(key, future.join()),即使用用户提供的原始 Weigher 来计算这个真实值的权重,并返回该权重。
  2. 当 CompletableFuture 尚未就绪:

    • 如果 future 仍在计算中、计算失败或结果为 nullisReady(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 的场景中。

http://www.xdnf.cn/news/20419.html

相关文章:

  • 蓓韵安禧DHA纯植物藻油纯净安全零添加守护母婴健康
  • 基于STM32智能阳台监控系统
  • Unity 如何使用ModbusTCP 和PLC通讯
  • 用 Go + HTML 实现 OpenHarmony 投屏(hdckit-go + WebSocket + Canvas 实战)
  • 《sklearn机器学习——绘制分数以评估模型》验证曲线、学习曲线
  • 鸿蒙Next开发指南:UIContext接口解析与全屏拉起元服务实战
  • DevOps实战(2) - 使用Arbess+GitPuk+Docker实现Java项目自动化部署
  • Rsyslog日志采集
  • 快捷:常见ocr学术数据集预处理版本汇总(适配mmocr)
  • js闭包问题
  • B.50.10.07-分布式锁核心原理与电商应用
  • 操作系统之内存管理
  • 从 0 到 1 学 sed 与 awk:Linux 文本处理的两把 “瑞士军刀”
  • 数据结构:栈和队列(下)
  • Qt控件:Item Views/Widgets
  • 国产数据库之YashanDB:新花怒放
  • 源滚滚AI编程SillyTavern酒馆配置Claude Code API教程
  • DeepSeek vs Anthropic:技术路线的正面冲突
  • Java基础 9.5
  • centos 系统如何安装open jdk 8
  • linux下快捷删除单词、行的命令
  • python中等难度面试题(1)
  • 基于cornerstone3D的dicom影像浏览器 第五章 在Displayer四个角落显示信息
  • C++数据结构命名:从规范到艺术的深度解析
  • CSDN个人博客文章全面优化过程
  • 不同行业视角下的数据分析
  • 计算机二级C语言操作题(填空、修改、设计题)——真题库(17)附解析答案
  • 打开Fiddler,浏览器就不能访问网页了
  • 超细汇总,银行测试-大额存单定期存款测试+面试(一)
  • 深度学习:归一化技术