Caffeine 双端队列优化揭秘:如何优雅维护访问和写入顺序
AbstractLinkedDeque
AbstractLinkedDeque
是一个设计得非常精巧的类,是 Caffeine 缓存内部实现 LRU (最近最少使用)、LFU (最不经常使用) 等淘汰策略的核心数据结构之一。
- Abstract: 这是一个抽象类。它提供了双向链表(Deque,双端队列)的骨架实现,但将一些特定于不同策略的行为(如
contains
,remove
)留给子类去完成。 - LinkedDeque: 它实现了一个侵入式的双向链表。所谓“侵入式”,是指链表的指针(
previous
,next
)是直接定义在要存储的元素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
方法。这样做的好处是:- 减少内存分配:不需要为每个元素都创建一个额外的
Node
对象。 - 提高缓存局部性:元素和它的链表指针在内存中是连续的,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
,防止在遍历过程中对集合进行修改导致不可预知的行为。
链接操作 (linkFirst
, linkLast
)
// ... 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 ...
这些方法负责将一个元素添加到队列的头部或尾部。逻辑很直接:
- 保存当前的头/尾节点。
- 将
first
/last
指针指向新元素e
。 - 处理边界情况:如果队列原先是空的 (
f == null
或l == null
),那么first
和last
都指向新元素。 - 如果队列非空,则通过
setPrevious
和setNext
更新新旧节点之间的双向链接。 modCount++
,记录结构性修改。
解链操作 (unlinkFirst
, unlinkLast
, unlink
)
// ... 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)
是最通用的移除方法,它处理了移除任意位置元素的情况:
- 获取待移除元素
e
的前驱prev
和后继next
。 - 处理前驱链接:如果
prev
是null
,说明e
是头节点,直接将first
指向next
。否则,将prev
的next
指针跨过e
指向next
。 - 处理后继链接:逻辑与前驱类似。如果
next
是null
,说明e
是尾节点,直接将last
指向prev
。否则,将next
的prev
指针跨过e
指向prev
。 - 清理工作:将
e
的prev
和next
指针都设为null
,这就是前面提到的“辅助 GC”。 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 的核心操作(如get
,put
)是高度并发优化的。为了避免这个开销,设计者选择牺牲不常用的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 的设计哲学:
- 专一性: 它只做一件事——维护访问顺序队列。
- 桥接模式: 它作为
AbstractLinkedDeque
的具体实现,将通用的链表操作逻辑与具体的AccessOrder
元素链接能力桥接起来。 - 性能至上: 通过实现
contains
和remove
的 O(1) 快速路径,它为上层缓存的 LRU 策略(如频繁的reorder
操作)提供了强大的性能保障。 - 侵入式设计: 强制元素实现
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 高性能、可组合策略设计的又一个典范。
- 功能专一: 它只负责维护写入顺序,为
expireAfterWrite
和FIFO
策略服务。 - 代码复用: 它与
AccessOrderDeque
共享AbstractLinkedDeque
的核心链表操作逻辑,避免了代码重复。 - 策略隔离: 通过
WriteOrder
接口和独特的命名,它在逻辑上和AccessOrderDeque
完全隔离,使得一个缓存条目可以被两种不同的顺序策略同时管理。 - 性能卓越: 继承了侵入式链表的所有优点,提供了 O(1) 的核心操作,保证了上层缓存策略的执行效率。
简单来说,WriteOrderDeque
和 AccessOrderDeque
是 Caffeine 内部策略队列的两个平行实现,它们结构相同,但目的不同,共同构成了 Caffeine 灵活且高效的淘汰和过期机制的基石。