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

Caffeine 双端队列优化揭秘:如何优雅维护访问和写入顺序

AbstractLinkedDeque

AbstractLinkedDeque 是一个设计得非常精巧的类,是 Caffeine 缓存内部实现 LRU (最近最少使用)、LFU (最不经常使用) 等淘汰策略的核心数据结构之一。

  • Abstract: 这是一个抽象类。它提供了双向链表(Deque,双端队列)的骨架实现,但将一些特定于不同策略的行为(如 containsremove)留给子类去完成。
  • LinkedDeque: 它实现了一个侵入式的双向链表。所谓“侵入式”,是指链表的指针(previousnext)是直接定义在要存储的元素 E 内部的,而不是通过一个单独的 Node 对象来包装 E

核心作用:在 Caffeine 中,这个类被用作各种顺序策略队列的基类。例如:

  • AccessOrderDeque: 用于维护元素的访问顺序(LRU 策略)。最近访问的元素会被移动到队尾。
  • WriteOrderDeque: 用于维护元素的写入/更新顺序(FIFO 或定时淘汰策略)。

设计哲学与优化

在类的顶部注释中,作者阐明了几个关键的设计决策,这些决策都是为了追求极致的性能。

// ... existing code ...
abstract class AbstractLinkedDeque<E> extends AbstractCollection<E> implements LinkedDeque<E> {// This class provides a doubly-linked list that is optimized for the virtual machine. The first// and last elements are manipulated instead of a slightly more convenient sentinel element to// avoid the insertion of null checks with NullPointerException throws in the byte code. The links// to a removed element are cleared to help a generational garbage collector if the discarded// elements inhabit more than one generation.
// ... existing code ...
  • 侵入式链表 (Intrusive Linked List): 这是最核心的优化。常规的 java.util.LinkedList 会创建一个 Node 对象来包装数据,如 Node(E element, Node prev, Node next)。这会带来额外的内存开销和间接访问。AbstractLinkedDeque 要求元素 E 本身就实现 get/setPrevious 和 get/setNext 方法。这样做的好处是:

    1. 减少内存分配:不需要为每个元素都创建一个额外的 Node 对象。
    2. 提高缓存局部性:元素和它的链表指针在内存中是连续的,CPU 缓存命中率更高。
  • 无哨兵节点 (Sentinel Node): 很多链表实现会使用一个虚拟的“哨兵”头节点和尾节点,这可以简化 add 和 remove 操作的逻辑(因为不需要处理 first 或 last 为 null 的边界情况)。但作者在这里明确指出,他们不使用哨兵节点,而是直接操作 first 和 last 引用。

    • 原因:避免在生成的字节码中插入不必要的 null 检查和 NullPointerException 抛出逻辑,使代码对 JVM 更友好,执行路径更短。
  • 辅助 GC: 注释中提到 The links to a removed element are cleared to help a generational garbage collector。当一个元素被从链表中移除时(例如在 unlink 方法中),它的 prev 和 next 指针会被显式地设置为 null。这可以打破对象之间的引用链,帮助分代垃圾回收器更早地回收这些可能不再被引用的对象,避免它们从新生代“晋升”到老年代。

核心字段

// ... existing code ...@Nullable E first;@Nullable E last;int modCount;
// ... existing code ...
  • @Nullable E first: 指向双端队列的第一个元素。
  • @Nullable E last: 指向双端队列的最后一个元素。
  • int modCount: 这是实现快速失败(fail-fast)机制的关键。每当队列的结构发生变化(增、删元素),这个计数器就会加一。迭代器在创建时会记录当时的 modCount,并在每次操作前检查它是否发生了变化。如果不一致,就抛出 ConcurrentModificationException,防止在遍历过程中对集合进行修改导致不可预知的行为。

链接操作 (linkFirstlinkLast)

// ... existing code ...void linkFirst(E e) {E f = first;first = e;if (f == null) {last = e;} else {setPrevious(f, e);setNext(e, f);}modCount++;}
// ... existing code ...

这些方法负责将一个元素添加到队列的头部或尾部。逻辑很直接:

  1. 保存当前的头/尾节点。
  2. 将 first/last 指针指向新元素 e
  3. 处理边界情况:如果队列原先是空的 (f == null 或 l == null),那么 first 和 last 都指向新元素。
  4. 如果队列非空,则通过 setPrevious 和 setNext 更新新旧节点之间的双向链接。
  5. modCount++,记录结构性修改。

解链操作 (unlinkFirstunlinkLastunlink)

// ... existing code ...void unlink(E e) {E prev = getPrevious(e);E next = getNext(e);if (prev == null) {first = next;} else {setNext(prev, next);setPrevious(e, null);}if (next == null) {last = prev;} else {setPrevious(next, prev);setNext(e, null);}modCount++;}
// ... existing code ...

unlink(E e) 是最通用的移除方法,它处理了移除任意位置元素的情况:

  1. 获取待移除元素 e 的前驱 prev 和后继 next
  2. 处理前驱链接:如果 prev 是 null,说明 e 是头节点,直接将 first 指向 next。否则,将 prev 的 next 指针跨过 e 指向 next
  3. 处理后继链接:逻辑与前驱类似。如果 next 是 null,说明 e 是尾节点,直接将 last 指向 prev。否则,将 next 的 prev 指针跨过 e 指向 prev
  4. 清理工作:将 e 的 prev 和 next 指针都设为 null,这就是前面提到的“辅助 GC”。
  5. modCount++

size() 方法的特殊性

// ... existing code ...@Overridepublic int size() {@Var int size = 0;for (E e = first; e != null; e = getNext(e)) {size++;}return size;}
// ... existing code ...

与 java.util.LinkedList 不同,这个类没有维护一个实时的 size 字段。获取大小需要遍历整个链表,所以它是一个 O(n) 操作。注释中也明确警告了这一点。

  • 原因:在 Caffeine 的使用场景中,精确的 size() 并不常用。维护一个实时 size 字段在并发环境下需要额外的同步开销(如 AtomicInteger),而 Caffeine 的核心操作(如 getput)是高度并发优化的。为了避免这个开销,设计者选择牺牲不常用的 size() 方法的性能。

抽象方法与子类实现

LinkedDeque 都交给子类实现

AbstractLinkedDeque 将 contains(Object o) 和 remove(Object o) 定义为抽象方法。这是因为它们的实现与具体策略紧密相关。

  • 在 AccessOrderDeque 和 WriteOrderDeque 中,contains 方法被优化了。它不通过遍历链表来判断,而是直接检查元素 e 的 prev 和 next 指针是否为 null。如果一个元素已经在链表中,它的指针必然不全是 null(除非是唯一的元素,此时它等于 first)。这是一个非常高效的 O(1) 判断。
// ... existing code ...// A fast-path containment checkboolean contains(AccessOrder<?> e) {return (e.getPreviousInAccessOrder() != null)|| (e.getNextInAccessOrder() != null)|| (e == first);}
// ... existing code ...

总结

AbstractLinkedDeque 是一个为高性能并发缓存量身定制的双向链表骨架。它通过侵入式设计、无哨兵节点、O(1) 的 contains 检查(由子类实现)以及对 GC 友好的链接清理等手段,最大限度地减少了内存开销和 CPU 指令,是 Caffeine 高性能特性的重要基石。它牺牲了 size() 方法的性能和通用性(元素必须实现特定接口),换来了在核心操作路径上的极致效率。

AccessOrderDeque 

AccessOrderDeque 是一个具体的、final 的类,它继承了 AbstractLinkedDeque

  • 作用:它的核心职责是维护一个基于访问顺序的双向链表。在 LRU 策略中,这个队列遵循一个简单的规则:
    • 当一个元素被访问(get 操作)时,它会被移动到队列的尾部。
    • 当需要淘汰元素时,总是从队列的头部移除元素(因为头部的元素是最久未被访问的)。
    • 新加入的元素会被添加到队列的尾部。

通过这种方式,AccessOrderDeque 始终保持着一个从“最久未访问”到“最近访问”的元素序列。

泛型约束:E extends AccessOrder<E>

这是理解这个类的关键。

// ... existing code ...
final class AccessOrderDeque<E extends AccessOrder<E>> extends AbstractLinkedDeque<E> {
// ... existing code .../*** An element that is linked on the {@link Deque}.*/interface AccessOrder<T extends AccessOrder<T>> {/*** Retrieves the previous element or {@code null} if either the element is unlinked or the* first element on the deque.*/@Nullable T getPreviousInAccessOrder();/** Sets the previous element or {@code null} if there is no link. */void setPreviousInAccessOrder(@Nullable T prev);/*** Retrieves the next element or {@code null} if either the element is unlinked or the last* element on the deque.*/@Nullable T getNextInAccessOrder();/** Sets the next element or {@code null} if there is no link. */void setNextInAccessOrder(@Nullable T next);}
}
  • E extends AccessOrder<E>: 这个泛型约束强制要求所有能被放入 AccessOrderDeque 的元素 E,都必须实现 AccessOrder<E> 接口。
  • AccessOrder<T> 接口: 这个内部接口定义了侵入式链表所需要的 prev 和 next 指针的 get/set 方法。注意方法名 getPreviousInAccessOrder,这个命名非常重要,它明确了这个指针是用于“访问顺序”的。Caffeine 的 Node 类可以同时实现 AccessOrder 和 WriteOrder 接口,通过不同的方法名来区分用于不同目的的链表指针,从而让同一个 Node 对象可以同时存在于多个不同的逻辑队列中。

桥接 AbstractLinkedDeque 和 AccessOrder

AccessOrderDeque 的核心实现部分,就是作为一座“桥梁”,将父类 AbstractLinkedDeque 中泛化的 getPrevious/setNext 等方法,与 AccessOrder 接口中具体化的 getPreviousInAccessOrder/setNextInAccessOrder 方法连接起来。

// ... existing code ...@Overridepublic @Nullable E getPrevious(E e) {return e.getPreviousInAccessOrder();}@Overridepublic void setPrevious(E e, @Nullable E prev) {e.setPreviousInAccessOrder(prev);}@Overridepublic @Nullable E getNext(E e) {return e.getNextInAccessOrder();}@Overridepublic void setNext(E e, @Nullable E next) {e.setNextInAccessOrder(next);}
// ... existing code ...

当 AbstractLinkedDeque 中的 link 或 unlink 方法调用 setNext(e, next) 时,AccessOrderDeque 的实现会确保调用的是元素 e 自己的 setNextInAccessOrder(next) 方法,从而正确地操作了用于访问顺序的指针。

高效的 contains 和 remove 实现

这是侵入式链表设计的巨大优势所在。AbstractLinkedDeque 将 contains 和 remove 声明为抽象方法,AccessOrderDeque 提供了极其高效的具体实现。

// ... existing code ...@Overridepublic boolean contains(Object o) {return (o instanceof AccessOrder<?>) && contains((AccessOrder<?>) o);}// A fast-path containment checkboolean contains(AccessOrder<?> e) {return (e.getPreviousInAccessOrder() != null)|| (e.getNextInAccessOrder() != null)|| (e == first);}@Override@SuppressWarnings("unchecked")public boolean remove(Object o) {return (o instanceof AccessOrder<?>) && remove((E) o);}// A fast-path removalboolean remove(E e) {if (contains(e)) {unlink(e);return true;}return false;}
// ... existing code ...
  • contains(AccessOrder<?> e): 这个方法是 O(1) 复杂度的。它不需要遍历整个链表。它利用了侵入式设计的特点:如果一个元素在链表中,那么它的 prev 或 next 指针至少有一个不为 null(除非链表中只有一个元素,此时它等于 first)。通过直接检查元素自身的指针状态,就可以瞬间判断它是否存在于这个队列中。注释 // A fast-path containment check 也点明了这是一个快速路径。

  • remove(E e): 同样是 O(1) 复杂度。它首先调用 contains(e) 进行快速检查,如果元素存在,就直接调用继承自 AbstractLinkedDeque 的 unlink(e) 方法。unlink 方法也是 O(1) 的,因为它能通过 getPrevious(e) 和 getNext(e) 直接拿到需要操作的节点。

总结

AccessOrderDeque 是一个看似简单但设计精巧的类,它完美地诠释了 Caffeine 的设计哲学:

  1. 专一性: 它只做一件事——维护访问顺序队列。
  2. 桥接模式: 它作为 AbstractLinkedDeque 的具体实现,将通用的链表操作逻辑与具体的 AccessOrder 元素链接能力桥接起来。
  3. 性能至上: 通过实现 contains 和 remove 的 O(1) 快速路径,它为上层缓存的 LRU 策略(如频繁的 reorder 操作)提供了强大的性能保障。
  4. 侵入式设计: 强制元素实现 AccessOrder 接口,虽然增加了耦合,但换来了内存和速度上的巨大优势。

总而言之,AccessOrderDeque 是 AbstractLinkedDeque 和 LinkedDeque 设计思想的一个具体、高效的落地实现,是 Caffeine 高性能淘汰机制不可或缺的一环。

WriteOrderDeque

WriteOrderDeque 这个类。如果已经理解了 AccessOrderDeque,那么理解 WriteOrderDeque 会非常容易,因为它们在设计和实现上几乎是“双胞胎”,但服务于完全不同的缓存策略。

WriteOrderDeque 继承自 AbstractLinkedDeque,它的核心职责是维护一个基于写入/更新顺序的双向链表。

  • 作用:这个队列主要用于实现 expireAfterWrite (写入后固定时间过期) 和 FIFO (先进先出) 淘汰策略。
    • 当一个新元素被创建或一个旧元素的值被更新时,它会被添加到这个队列的尾部
    • 当需要基于写入时间进行淘汰时,缓存会从队列的头部开始检查,因为头部的元素就是最先被写入且之后再未被更新过的元素。

与 AccessOrderDeque 响应“读”操作不同,WriteOrderDeque 响应的是“写”操作。

核心设计:WriteOrder<E> 接口

这是 WriteOrderDeque 与 AccessOrderDeque 最根本的区别所在,也是 Caffeine 设计精妙之处的体现。

// ... existing code ...
final class WriteOrderDeque<E extends WriteOrder<E>> extends AbstractLinkedDeque<E> {
// ... existing code .../*** An element that is linked on the {@link Deque}.*/interface WriteOrder<T extends WriteOrder<T>> {/*** Retrieves the previous element or {@code null} if either the element is unlinked or the* first element on the deque.*/@Nullable T getPreviousInWriteOrder();/** Sets the previous element or {@code null} if there is no link. */void setPreviousInWriteOrder(@Nullable T prev);/*** Retrieves the next element or {@code null} if either the element is unlinked or the last* element on the deque.*/@Nullable T getNextInWriteOrder();/** Sets the next element or {@code null} if there is no link. */void setNextInWriteOrder(@Nullable T next);}
}
  • E extends WriteOrder<E>: 泛型约束要求所有存入该队列的元素 E 必须实现 WriteOrder<E> 接口。
  • WriteOrder<T> 接口: 此接口定义了用于“写入顺序”链表的 prev 和 next 指针的 get/set 方法。
  • 关键点:方法命名:注意方法名是 getPreviousInWriteOrder 和 setNextInWriteOrder。这与 AccessOrderDeque 的 ...InAccessOrder 形成了明确区分。

这个精巧的命名方案,使得同一个缓存条目(Node 对象)可以同时实现 AccessOrder 和 WriteOrder 两个接口,从而拥有两套独立的 prev/next 指针。这意味着同一个 Node 对象可以同时存在于 AccessOrderDeque 和 WriteOrderDeque 两个不同的队列中,而它们的链接状态互不干扰。这是 Caffeine 能够高效地支持复杂组合策略(例如,同时配置 expireAfterAccess 和 expireAfterWrite)的底层基础。

WriteOrderDeque 的代码实现与 AccessOrderDeque 如出一辙,它同样扮演着“桥梁”和“快速路径提供者”的角色。

桥接方法

它将父类 AbstractLinkedDeque 的通用链接操作,精确地导向到元素 E 自身的 ...InWriteOrder 方法上。

// ... existing code ...@Overridepublic @Nullable E getPrevious(E e) {return e.getPreviousInWriteOrder();}@Overridepublic void setPrevious(E e, @Nullable E prev) {e.setPreviousInWriteOrder(prev);}@Overridepublic @Nullable E getNext(E e) {return e.getNextInWriteOrder();}@Overridepublic void setNext(E e, @Nullable E next) {e.setNextInWriteOrder(next);}
// ... existing code ...

高效的 contains 和 remove

它也提供了 O(1) 复杂度的 contains 和 remove 实现,原理和 AccessOrderDeque 完全相同,只是检查的指针变成了写入顺序指针。

// ... existing code ...// A fast-path containment checkboolean contains(WriteOrder<?> e) {return (e.getPreviousInWriteOrder() != null)|| (e.getNextInWriteOrder() != null)|| (e == first);}
// ... existing code ...// A fast-path removalpublic boolean remove(E e) {if (contains(e)) {unlink(e);return true;}return false;}
// ... existing code ...

总结

WriteOrderDeque 是 Caffeine 高性能、可组合策略设计的又一个典范。

  1. 功能专一: 它只负责维护写入顺序,为 expireAfterWrite 和 FIFO 策略服务。
  2. 代码复用: 它与 AccessOrderDeque 共享 AbstractLinkedDeque 的核心链表操作逻辑,避免了代码重复。
  3. 策略隔离: 通过 WriteOrder 接口和独特的命名,它在逻辑上和 AccessOrderDeque 完全隔离,使得一个缓存条目可以被两种不同的顺序策略同时管理。
  4. 性能卓越: 继承了侵入式链表的所有优点,提供了 O(1) 的核心操作,保证了上层缓存策略的执行效率。

简单来说,WriteOrderDeque 和 AccessOrderDeque 是 Caffeine 内部策略队列的两个平行实现,它们结构相同,但目的不同,共同构成了 Caffeine 灵活且高效的淘汰和过期机制的基石。

http://www.dtcms.com/a/366883.html

相关文章:

  • 02-ideal2025 Ultimate版安装教程
  • 代码随想录刷题Day49
  • 随时随地写代码:Jupyter Notebook+cpolar让远程开发像在本地一样流畅
  • 51单片机:中断、定时器与PWM整合手册
  • spring.profiles.active配置的作用
  • 设计模式六大原则2-里氏替换原则
  • 短视频运营为什么需要代理 IP
  • JS函数进阶
  • 【可信数据空间-连接器状态监控】
  • 【面试题】如何构造排序模型训练数据?解决正负样本不均?
  • matlab实现希尔伯特变换(HHT)
  • 批量获取1688商品详情图及API接口调用实操指南
  • 【Kubernetes】知识点4
  • 卫生间异味来源难察觉?这款传感器为你精准探测并预警
  • 从设计到落地:校园图书馆系统的面向对象实现全流程
  • 多个docker compose启动的容器之间通信实现
  • Oracle 数据库如何查询列
  • (论文速读)Navigation World Models: 让机器人像人类一样想象和规划导航路径
  • 子串:最小覆盖子串
  • 深度学习中的学习率优化策略详解
  • UE5 制作游戏框架的部分经验积累(持续更新)
  • Kubernetes知识点(三)
  • AWS中为OpsManage配置IAM权限:完整指南
  • 深入剖析Spring Boot / Spring 应用中可自定义的扩展点
  • 力扣654:最大二叉树
  • AI+Java 守护你的钱袋子!金融领域的智能风控与极速交易
  • .NET 开发者的“Fiddler”:Titanium.Web.Proxy 库的强大魅力
  • 以数据与自动化驱动实验室变革:智能化管理整体规划
  • “乾坤大挪移”:耐达讯自动化RS485转Profinet解锁HMI新乾坤
  • 数据安全章节考试考点及关系梳理