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

缓存之美:Guava Cache 相比于 Caffeine 差在哪里?

大家好,我是 方圆。本文将结合 Guava Cache 的源码来分析它的实现原理,并阐述它相比于 Caffeine Cache 在性能上的劣势。为了让大家对 Guava Cache 理解起来更容易,我们还是在开篇介绍它的原理:

在这里插入图片描述

Guava Cache 通过分段(Segment)锁(ReentrantLock)机制、volatile 变量和多种缓存策略实现了性能相对 Caffeine 性能较差的缓存,它的数据结构如上图所示。它会将缓存分成多个段(Segment)去管理,单个段内写操作加锁互斥,如果要创建大小为 1000 的缓存,那么实际上会分配 4 个段,每个段的最大容量为 250。读写操作在执行时都会经 segmentFor 方法“路由”到某一个段。数据结构的实现都在 Segment 中,它对元素的管理采用的是 AtomicReferenceArray 数组,在初始化时是较小的容量,并随着元素的增多触发扩容机制。我们称数组中每个索引的位置为“桶”,每个桶中保存了元素的引用,这些元素是通过单向链表维护的,每当有新元素添加时,采用的是“头插法”。此外,在 Segment 中还维护了三个基于 LRU 算法 的队列,处于尾部的元素最“新”,分别是 accessQueuewriteQueuerecencyQueue,它们分别用于记录被访问的元素、被写入的元素和“最近”被访问的元素。accessQueue 的主要作用是在对超过最大容量(超过访问后过期时间)的元素进行驱逐时,优先将最近被访问的越少的元素驱逐(头节点开始遍历);writeQueue 的主要作用是对写后过期的元素进行驱逐时,优先将最近最少被访问的元素驱逐,因为越早被添加的元素越早过期,当发现某元素未过期时,后续队列中的元素是不需要判断的;recencyQueue 的作用是记录被访问过的元素,它们最终都会被移动到 accessQueue 中,并根据访问顺序添加到其尾节点中。

对元素生命周期的管理主要是在 put 方法中完成的,put 相关的操作都需要加锁,如图中左上方所示,这些方法均与缓存元素的管理相关。Guava Cache 为了在不触发写操作而有大量读操作时也能正常触发对缓存元素的管理,添加了一个 readCount 变量,每次读请求都会使其累加,直到该变量超过规定阈值,也会触发缓存元素的驱逐(postReadCleanUp),保证数据的一致性,如图中右上方所示。

接下来我们通过创建最大大小为 1000,并配置有访问后和写后过期时间的 LoadingCache 来分析 Guava Cache 的实现原理,主要关注它的构造方法,put 方法和 get 方法:

public class TestGuavaCache {@Testpublic void test() {LoadingCache<String, String> cache = CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.SECONDS).expireAfterAccess(10, TimeUnit.SECONDS).build(new CacheLoader<>() {@Overridepublic String load(String key) {return String.valueOf(key.hashCode());}});cache.put("key1", "value1");try {System.out.println(cache.get("key"));} catch (ExecutionException e) {throw new RuntimeException(e);}}
}

constructor

首先我们来看一下它的构造方法,它会将创建缓存时指定的参数记录下来,比如访问后过期时间(expireAfterAccessNanos),写后过期时间(expireAfterWriteNanos)等等,除此之外还包括 Segment 分段对象的创建,定义分段的数量和每个分段的大小,并将这些 Segment 对象保存在一个数组中,以创建最大元素数量为 1000 的缓存为例,它会创建 4 个分段,每个分段分配 250 个元素。源码如下所示,均为赋值操作,可关注 Segment 相关逻辑:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {static final int MAX_SEGMENTS = 1 << 16;static final int MAXIMUM_CAPACITY = 1 << 30;final int concurrencyLevel;final Strength keyStrength;final Strength valueStrength;final Equivalence<Object> keyEquivalence;final Equivalence<Object> valueEquivalence;final long maxWeight;final Weigher<K, V> weigher;final long expireAfterAccessNanos;final long expireAfterWriteNanos;final long refreshNanos;final RemovalListener<K, V> removalListener;final Queue<RemovalNotification<K, V>> removalNotificationQueue;final Ticker ticker;final EntryFactory entryFactory;final StatsCounter globalStatsCounter;@CheckForNull final CacheLoader<? super K, V> defaultLoader;final int segmentMask;final int segmentShift;final Segment<K, V>[] segments;LocalCache(CacheBuilder<? super K, ? super V> builder, @CheckForNull CacheLoader<? super K, V> loader) {// 并发级别,不指定默认为 4concurrencyLevel = min(builder.getConcurrencyLevel(), MAX_SEGMENTS);// key 和 value 的引用强度,默认为强引用keyStrength = builder.getKeyStrength();valueStrength = builder.getValueStrength();// 键值比较器,默认为强引用比较器keyEquivalence = builder.getKeyEquivalence();valueEquivalence = builder.getValueEquivalence();// maxWeight 最大权重,指定了为 1000maxWeight = builder.getMaximumWeight();// weigher 没有指定,默认为 1,表示每个元素的权重为 1weigher = builder.getWeigher();// 访问后和写后过期时间,默认为 0,表示不设置过期时间expireAfterAccessNanos = builder.getExpireAfterAccessNanos();expireAfterWriteNanos = builder.getExpireAfterWriteNanos();// 刷新时间,默认为 0,表示不刷新refreshNanos = builder.getRefreshNanos();// 元素驱逐监听器removalListener = builder.getRemovalListener();removalNotificationQueue = (removalListener == NullListener.INSTANCE) ? LocalCache.discardingQueue() : new ConcurrentLinkedQueue<>();// 定义 Ticker 可以模拟时间流动来验证逻辑ticker = builder.getTicker(recordsTime());entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());globalStatsCounter = builder.getStatsCounterSupplier().get();defaultLoader = loader;// 初始化大小,默认为 16int initialCapacity = min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);if (evictsBySize() && !customWeigher()) {initialCapacity = (int) min(initialCapacity, maxWeight);}// 基于大小的驱逐策略是针对每个段而不是全局进行驱逐的,因此段数过多会导致随机的驱逐行为// 计算分段数量和分段位移(shift)的逻辑int segmentShift = 0;int segmentCount = 1;// 保证分段数量不低于并发级别 且 段数*20不超过最大权重,保证每个段的元素数量至少为 10while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20L <= maxWeight)) {// 分段位移+1++segmentShift;// 分段数量为 2的n次幂segmentCount <<= 1;}this.segmentShift = 32 - segmentShift;// 分段掩码值为分段数量-1segmentMask = segmentCount - 1;// 创建分段数组this.segments = newSegmentArray(segmentCount);// 计算每个分段的大小int segmentCapacity = initialCapacity / segmentCount;if (segmentCapacity * segmentCount < initialCapacity) {++segmentCapacity;}int segmentSize = 1;while (segmentSize < segmentCapacity) {segmentSize <<= 1;}// 如果指定了基于大小的驱逐策略,那么要保证所有分段的最大值之和(maxSegmentWeight)要超过指定的最大容量if (evictsBySize()) {long maxSegmentWeight = maxWeight / segmentCount + 1;long remainder = maxWeight % segmentCount;for (int i = 0; i < this.segments.length; ++i) {if (i == remainder) {maxSegmentWeight--;}// 创建 Segment 对象,segmentSize为4,maxSegmentWeight为250this.segments[i] = createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());}}// 如果未指定基于大小的驱逐策略,创建的 Segment 对象的 maxSegmentWeight 为 UNSET_INTelse {for (int i = 0; i < this.segments.length; ++i) {this.segments[i] = createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());}}}final Segment<K, V>[] newSegmentArray(int ssize) {return (Segment<K, V>[]) new Segment<?, ?>[ssize];}
}

我们接着看下创建 Segment 的方法 LocalCache#createSegment,它直接调用了 Segment 的构造方法:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {// ...Segment<K, V> createSegment(int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) {return new Segment<>(this, initialCapacity, maxSegmentWeight, statsCounter);}
}

SegmentLocalCache 内的静态内部类,它在缓存中起到了将数据分段管理的作用。它继承了 ReentrantLock,主要为了简化锁定的操作:

static class Segment<K, V> extends ReentrantLock {// ...
}

在该类中有一段 JavaDoc 值得读一下:

Segments 内部维护了缓存本身(final LocalCache<K, V> map),所以它能一直保持数据一致性,也因此可以在不加锁的情况下进行读操作。被缓存的键值对对象(构成单向链表)中的 next 字段被 final 修饰,所有的添加操作都只能在每个桶的前面进行(头插法),这也就使得检查变更比较容易,并且遍历速度也比较快。当节点需要更改时,会创建新节点来替换它们(深拷贝)。这对于哈希表来说效果很好,因为桶列表往往很短(平均长度小于二)。

读操作可以在不加锁的情况下进行,但依赖于被 volatile 关键字修饰的变量,因为这个关键字能确保“可见性”。对大多数操作来说,可以将记录元素数量的字段 count 来作为确保可见性的变量。它带来了很多便利,在很多读操作中都需要参考这个字段:

  • 未加锁的读操作必须首先读取 count 字段,如果它是 0,则不应读任何元素
  • 加锁的写操作在任何桶发生结构性更改后都需要修改 count 字段值,这些写操作不能在任何情况下导致并发读操作发生读取数据不一致的情况,这样的保证使得 Map 中的读操作更容易。比如,没有操作可以揭示 Map 中添加了新的元素但是 count 字段没有被更新的情况,因此相对于读取没有原子性要求。

作为提示,所有被 volatile 修饰的字段都很关键,它们的读取和写入都会用注释标记。

 * Segments maintain a table of entry lists that are ALWAYS kept in a consistent state, so can* be read without locking. Next fields of nodes are immutable (final). All list additions are* performed at the front of each bin. This makes it easy to check changes, and also fast to* traverse. When nodes would otherwise be changed, new nodes are created to replace them. This* works well for hash tables since the bin lists tend to be short. (The average length is less* than two.)** Read operations can thus proceed without locking, but rely on selected uses of volatiles to* ensure that completed write operations performed by other threads are noticed. For most* purposes, the "count" field, tracking the number of elements, serves as that volatile* variable ensuring visibility. This is convenient because this field needs to be read in many* read operations anyway:** - All (unsynchronized) read operations must first read the "count" field, and should not look* at table entries if it is 0.** - All (synchronized) write operations should write to the "count" field after structurally* changing any bin. The operations must not take any action that could even momentarily cause a* concurrent read operation to see inconsistent data. This is made easier by the nature of the* read operations in Map. For example, no operation can reveal that the table has grown but the* threshold has not yet been updated, so there are no atomicity requirements for this with* respect to reads.** As a guide, all critical volatile reads and writes to the count field are marked in code* comments.

通过它的 JavaDoc 我们可以了解到它通过写操作对数据一致性的保证和被 volatile 修饰的字段来实现无锁的读操作,不过其中键值对中被 final 修饰的 next 字段究竟是怎么回事就需要在后文中去探究了。下面我们根据它的构造方法看一下该类中比较重要的字段信息:

static class Segment<K, V> extends ReentrantLock {// 在某一段(Segment)中元素的数量volatile int count;// 总的权重值@GuardedBy("this")long totalWeight;// 修改次数int modCount;// 继上一次写操作后读操作的数量final AtomicInteger readCount = new AtomicInteger();@Weak final LocalCache<K, V> map;final long maxSegmentWeight;final StatsCounter statsCounter;int threshold;@CheckForNull volatile AtomicReferenceArray<ReferenceEntry<K, V>> table;final Queue<ReferenceEntry<K, V>> recencyQueue;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> writeQueue;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> accessQueue;Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) {// LocalCache 对象的引用this.map = map;// 最大分段权重,我们的例子中它的值是 250this.maxSegmentWeight = maxSegmentWeight;this.statsCounter = checkNotNull(statsCounter);// 根据初始化容量创建支持原子操作的 AtomicReferenceArray 对象initTable(newEntryArray(initialCapacity));// 管理弱引用和虚引用的 Key,Value 队列keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<>() : null;valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<>() : null;// 用于记录“最近”元素被访问的顺序recencyQueue = map.usesAccessQueue() ? new ConcurrentLinkedQueue<>() : LocalCache.discardingQueue();// 用于记录元素的写入顺序,元素被写入时会被添加到尾部writeQueue = map.usesWriteQueue() ? new WriteQueue<>() : LocalCache.discardingQueue();// 记录元素的访问顺序,元素被访问后会被添加到尾部accessQueue = map.usesAccessQueue() ? new AccessQueue<>() : LocalCache.discardingQueue();}AtomicReferenceArray<ReferenceEntry<K, V>> newEntryArray(int size) {return new AtomicReferenceArray<>(size);}void initTable(AtomicReferenceArray<ReferenceEntry<K, V>> newTable) {// 默认负载因子为 0.75this.threshold = newTable.length() * 3 / 4;if (!map.customWeigher() && this.threshold == maxSegmentWeight) {// 在执行驱逐操作前防止不必要的扩张操作,将阈值+1this.threshold++;}this.table = newTable;}// ...
}

根据上述代码和注释信息,每个 Segment 的数据结构由 AtomicReferenceArray(本质上是 Object[] 数组)和三个基于LRU算法的队列组成,AtomicReferenceArray 初始化时为一个较小容量(4)的数组,在缓存的操作过程中会根据元素添加的情况触发扩容,在这里我们已经能看到 Guava Cache 数据结构的全貌了,如下所示:
在这里插入图片描述

在接下来的两个小节中,我们将深入讨论 putget 方法的实现,分析这些数据结构是如何为这些操作提供支持的。

put

在深入 put 方法前,我们需要先了解下创建键值对元素的逻辑。在调用 LocalCache 的构造方法时,其中 entryFactory 字段我们没具体讲解,在这里详细描述下,因为它与键值对元素的创建有关。EntryFactory 是一个枚举类,它其中定义了如 STRONG_ACCESS_WRITEWEAK_ACCESS_WRITE 等一系列枚举,并根据创建缓存时指定的 Key 引用类型和元素管理策略来决定具体是哪个枚举,如其中的 EntryFactory#getFactory 方法所示:

enum EntryFactory {STRONG {// ...},STRONG_ACCESS {// ...},STRONG_WRITE {// ...  },STRONG_ACCESS_WRITE {// ...},// ...WEAK_ACCESS_WRITE {// ...};static final int ACCESS_MASK = 1;static final int WRITE_MASK = 2;static final int WEAK_MASK = 4;static final EntryFactory[] factories = {STRONG,STRONG_ACCESS,STRONG_WRITE,STRONG_ACCESS_WRITE,WEAK,WEAK_ACCESS,WEAK_WRITE,WEAK_ACCESS_WRITE,};static EntryFactory getFactory(Strength keyStrength, boolean usesAccessQueue, boolean usesWriteQueue) {int flags = ((keyStrength == Strength.WEAK) ? WEAK_MASK : 0)| (usesAccessQueue ? ACCESS_MASK : 0)| (usesWriteQueue ? WRITE_MASK : 0);return factories[flags];}
}

当不指定 Key 的引用类型时为强引用,结合指定的访问后和写后过期策略,会匹配到 STRONG_ACCESS_WRITE 枚举,根据它的命名也能理解它表示 Key 为强引用且配置了访问后和写后过期策略。下面主要关注下它的逻辑:

enum EntryFactory {STRONG_ACCESS_WRITE {@Override<K, V> ReferenceEntry<K, V> newEntry(Segment<K, V> segment, K key, int hash, @CheckForNull ReferenceEntry<K, V> next) {return new StrongAccessWriteEntry<>(key, hash, next);}// ...}
}

其中 EntryFactory#newEntry 为创建键值对元素的方法,创建的键值对类型为 StrongAccessWriteEntry,我们看下它的具体实现:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {static class StrongEntry<K, V> extends AbstractReferenceEntry<K, V> {final K key;final int hash;@CheckForNull final ReferenceEntry<K, V> next;volatile ValueReference<K, V> valueReference = unset();StrongEntry(K key, int hash, @CheckForNull ReferenceEntry<K, V> next) {this.key = key;this.hash = hash;this.next = next;}// ...}static final class StrongAccessWriteEntry<K, V> extends StrongEntry<K, V> {volatile long accessTime = Long.MAX_VALUE;volatile long writeTime = Long.MAX_VALUE;@Weak ReferenceEntry<K, V> nextAccess = nullEntry();@Weak ReferenceEntry<K, V> previousAccess = nullEntry();@Weak ReferenceEntry<K, V> nextWrite = nullEntry();@Weak ReferenceEntry<K, V> previousWrite = nullEntry();StrongAccessWriteEntry(K key, int hash, @CheckForNull ReferenceEntry<K, V> next) {super(key, hash, next);}}
}

StrongAccessWriteEntry 与它的父类 StrongEntry 均为定义在 LocalCache 内的静态内部类,构造方法需要指定 Key, hash, next 三个变量,这三个变量均定义在 StrongEntry 中,注意第三个变量 ReferenceEntry<K, V> next,它被父类中 StrongEntry#next 字段引用,并且该字段被 final 修饰,这与前文 JavaDoc 中所描述的内容相对应:

键值对对象中的 next 字段被 final 修饰,所有的添加操作都只能在每个桶的前面进行(头插法),这也就使得检查变更比较容易,并且遍历速度也比较快。

所以实际上在添加新的键值对元素时,针对每个桶中元素操作采用的是“头插法”,这些元素是通过 next 指针引用的 单向链表。现在了解了元素的类型和创建逻辑,我们再来看下 put 方法的实现。

Guava Cache 是不允许添加 null 键和 null 值的,如果添加了 null 键或 null 值,会抛出 NullPointerException 异常。注意其中的注解 @GuardedBy 表示某方法或字段被调用或访问时需要持有哪个同步锁,在 Caffeine 中也有类似的应用:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {// ...final Segment<K, V>[] segments;final int segmentShift;final int segmentMask;@GuardedBy("this")long totalWeight;@CheckForNull@CanIgnoreReturnValue@Overridepublic V put(K key, V value) {checkNotNull(key);checkNotNull(value);// 计算 hash 值,过程中调用了 rehash 扰动函数使 hash 更均匀int hash = hash(key);// 根据 hash 值路由到具体的 Segment 段,再调用 Segment 的 put 方法return segmentFor(hash).put(key, hash, value, false);}Segment<K, V> segmentFor(int hash) {// 位移并位与运算,segmentMask 为段数组长度减一,保证计算结果在有效范围内return segments[(hash >>> segmentShift) & segmentMask];}
}

可以发现在执行 put 操作前,会根据键的哈希值来将其路由(segmentFor)到具体的 Segment 段,再调用 Segmentput 方法,而具体的 put 方法逻辑是在 Segment 中实现的。我们接着看 Segment#put 方法的实现(为了保证可读性,其中标注了数字的方法会在接下来小节中具体分析):

static class Segment<K, V> extends ReentrantLock {final AtomicInteger readCount = new AtomicInteger();@GuardedBy("this")final Queue<ReferenceEntry<K, V>> accessQueue;final Queue<ReferenceEntry<K, V>> recencyQueue;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> writeQueue;// guava cache 本身@Weakfinal LocalCache<K, V> map;// Segment 中保存元素的数组@CheckForNullvolatile AtomicReferenceArray<ReferenceEntry<K, V>> table;// 计数器final StatsCounter statsCounter;// 该段中的元素数量volatile int count;// 总的权重,不配置也表示元素数量,每个元素的权重为 1@GuardedBy("this")long totalWeight;// capacity * 0.75int threshold;// 更新次数,用于确保在批量读取的情况下,保证数据一致性,如果多次读取时 modCount 不一致则需要重试int modCount;@CanIgnoreReturnValue@CheckForNullV put(K key, int hash, V value, boolean onlyIfAbsent) {// 先加锁 ReentrantLock#lock 实现lock();try {long now = map.ticker.read();// 1. 写前置的清理操作,包括处理过期元素等preWriteCleanup(now);int newCount = this.count + 1;// 如果超过阈值,则扩容if (newCount > this.threshold) {// 2. 扩容操作expand();newCount = this.count + 1;}// 根据元素的 hash 值找到对应的桶索引 index,并拿到头节点 firstAtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;int index = hash & (table.length() - 1);ReferenceEntry<K, V> first = table.get(index);// 尝试遍历链表寻找被新添加的元素是否已经存在for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {K entryKey = e.getKey();// 如果新 put 的元素在缓存中已经存在if (e.getHash() == hash && entryKey != null && map.keyEquivalence.equivalent(key, entryKey)) {ValueReference<K, V> valueReference = e.getValueReference();V entryValue = valueReference.get();// 通过元素的 value 是否为 null 的情况来判断if (entryValue == null) {++modCount;// StrongValueReference 类型默认 active 为 true 表示 value 值是有效的if (valueReference.isActive()) {// 但是此时元素的 value 值为空,说明该 value 值已经被垃圾回收了,所以需要将该元素 value 被垃圾回收的通知加入到通知队列中// 并在总权重中将该元素的权重减去enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);// 更新为新的 valuesetValue(e, key, value, now);// 元素总数不变更newCount = this.count;} else {// 赋值新的 Value 并需要将元素数 +1setValue(e, key, value, now);newCount = this.count + 1;}// 变更 count 值(可见性)this.count = newCount;// 3. 驱逐元素evictEntries(e);return null;} else if (onlyIfAbsent) {// 如果 onlyIfAbsent 为 true,那么已存在元素的 value 是不会被覆盖的,只需要记录读操作即可recordLockedRead(e, now);return entryValue;} else {// 这种情况是 value 不为空,需要将旧值替换成新值// 变更修改次数++modCount;// 同样是先将该元素的权重减去,并将元素 value 被替换的通知加入到通知队列中enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);setValue(e, key, value, now);// 3. 驱逐元素evictEntries(e);return entryValue;}}}// put 的元素是一个新元素++modCount;// 创建一个新元素,并指定 next 节点为 first 头节点ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);setValue(newEntry, key, value, now);// 将该元素封装到数组中table.set(index, newEntry);// 更新 count 值newCount = this.count + 1;this.count = newCount;// 3. 驱逐元素evictEntries(newEntry);return null;} finally {// 执行完操作,解锁unlock();// 4. 写后操作,主要是处理通知,在后文中介绍postWriteCleanup();}}@GuardedBy("this")void setValue(ReferenceEntry<K, V> entry, K key, V value, long now) {ValueReference<K, V> previous = entry.getValueReference();// 计算元素权重,如果没有执行权重计算函数默认为 1int weight = map.weigher.weigh(key, value);checkState(weight >= 0, "Weights must be non-negative");// 更新元素的 value 值ValueReference<K, V> valueReference = map.valueStrength.referenceValue(this, entry, value, weight);entry.setValueReference(valueReference);// 处理写操作recordWrite(entry, weight, now);// StrongValueReference 该方法为空实现previous.notifyNewValue(value);}@GuardedBy("this")void recordWrite(ReferenceEntry<K, V> entry, int weight, long now) {// 将元素的最近被访问队列 recencyQueue 清空,并使用尾插法将它们都放到访问队列 accessQueue 的尾节点drainRecencyQueue();// 变更总权重totalWeight += weight;// 如果配置了访问后或写后过期则更新元素的访问后或写后时间if (map.recordsAccess()) {entry.setAccessTime(now);}if (map.recordsWrite()) {entry.setWriteTime(now);}// 添加到访问队列和写队列中accessQueue.add(entry);writeQueue.add(entry);}@GuardedBy("this")void recordLockedRead(ReferenceEntry<K, V> entry, long now) {// 配置了访问后过期时间则更新该元素的访问时间if (map.recordsAccess()) {entry.setAccessTime(now);}// 将该元素添加到访问队列中accessQueue.add(entry);}
}

上述源码中,我们能够发现 Guava Cache 是在 Segment 的级别加锁 的,通过这样的方式来减少竞争,其中 preWriteCleanup 方法负责处理元素的过期;evictEntries 方法负责驱逐保证缓存不超过最大的容量,根据 LRU 算法将最近最少访问的元素移除;expand 方法负责 Segment 的扩容;setValue, recordWriterecordLockedRead 方法负责处理元素的更新并记录访问情况(更新 accessQueuewriteQueue 队列)。它对元素的管理相对简单也比较容易理解,接下来我们分步骤看下其中方法的实现。

preWriteCleanup

首先我们重点看下 preWriteCleanup 方法,该方法负责处理元素的过期,而元素过期的判断也非常简单,它会在每个元素中记录它们最新的访问或写入时间,将当前时间与这些时间作差后与配置的访问或写入过期时间作比较,如果超过了配置的时间则表示元素过期,并将它们进行驱逐。除此之外还有两个小细节需要留意,队列中维护元素的变动采用的是 LRU 算法,并规定元素越靠近尾部表示它越“新”,另一点是 readCount 会在写后标记为 0,这个变量的作用是保证在没发生写操作的情况下,而读次数超过一定阈值也会执行 cleanUp 的方法,这个在后文的 get 方法逻辑中还会提到。源码如下所示:

static class Segment<K, V> extends ReentrantLock {final AtomicInteger readCount = new AtomicInteger();@GuardedBy("this")final Queue<ReferenceEntry<K, V>> accessQueue;final Queue<ReferenceEntry<K, V>> recencyQueue;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> writeQueue;// guava cache 本身@Weak final LocalCache<K, V> map;// Segment 中保存元素的数组@CheckForNull volatile AtomicReferenceArray<ReferenceEntry<K, V>> table;// 计数器final StatsCounter statsCounter;// 该段中的元素数量volatile int count;// 总的权重,不配置也表示元素数量,每个元素的权重为 1@GuardedBy("this")long totalWeight;@GuardedBy("this")void preWriteCleanup(long now) {// 执行加锁的清理操作,包括处理引用队列和过期元素runLockedCleanup(now);}void runLockedCleanup(long now) {// 仍然是 ReentrantLock#tryLock 实现if (tryLock()) {try {// 处理引用队列(本文不对指定 Key 或 Value 为 weekReference 的情况进行分析)drainReferenceQueues();// 处理元素过期expireEntries(now);// 写后读次数清零readCount.set(0);} finally {unlock();}}}@GuardedBy("this")void expireEntries(long now) {// 处理最近访问队列 recencyQueue 和访问队列 accessQueuedrainRecencyQueue();ReferenceEntry<K, V> e;// 从头节点开始遍历写队列 writeQueue,将过期的元素移除while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {throw new AssertionError();}}while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {throw new AssertionError();}}}// 将元素的最近被访问队列 recencyQueue 清空,并使用尾插法将它们都放到访问队列 accessQueue 的尾节点@GuardedBy("this")void drainRecencyQueue() {ReferenceEntry<K, V> e;// 遍历元素最近被访问队列 recencyQueuewhile ((e = recencyQueue.poll()) != null) {// 如果该元素在访问队列 accessQueue 中if (accessQueue.contains(e)) {// 则将其放到尾节点accessQueue.add(e);}// 源码中对这里的 IF 判断,标注了如下内容来解释如此判断的原因:// 尽管元素已经在缓存中删除,但它仍可能在 recencyQueue 队列中。// 这种情况可能出现在开发者操作元素删除或清空段中所有元素的同时执行读操作}}@VisibleForTesting@GuardedBy("this")@CanIgnoreReturnValueboolean removeEntry(ReferenceEntry<K, V> entry, int hash, RemovalCause cause) {int newCount = this.count - 1;AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;// 位与运算找到对应的桶,获取头节点int index = hash & (table.length() - 1);ReferenceEntry<K, V> first = table.get(index);for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {// 找到了对应的元素则操作移除if (e == entry) {++modCount;// 在链表chain中移除元素,注意 e 表示待移除的元素ReferenceEntry<K, V> newFirst = removeValueFromChain(first, e, e.getKey(), hash, e.getValueReference().get(), e.getValueReference(), cause);// 注意这里又重新计算了 newCount,防止在方法执行前的 newCount 快照值发生变更newCount = this.count - 1;// 桶的位置更新为新的链表头节点table.set(index, newFirst);// count 被 volatile 修饰,可见性写操作this.count = newCount;return true;}}return false;}/*** 将元素从队列中移除* * @param first 队列的头节点* @param entry 待移除元素* @param key   待移除元素的 key* @param hash  待移除元素的 hash 值* @param value 待移除元素的 value 值* @param valueReference 带移除元素的 value 引用对象* @param cause 被移除的原因* @return 返回链表头节点*/@GuardedBy("this")@CheckForNullReferenceEntry<K, V> removeValueFromChain(ReferenceEntry<K, V> first, ReferenceEntry<K, V> entry, @CheckForNull K key, int hash, V value, ValueReference<K, V> valueReference, RemovalCause cause) {// 入队元素被移除的通知等操作enqueueNotification(key, hash, value, valueReference.getWeight(), cause);// 在写队列和访问队列中移除元素writeQueue.remove(entry);accessQueue.remove(entry);if (valueReference.isLoading()) {valueReference.notifyNewValue(null);return first;} else {// 将元素在链表中移除return removeEntryFromChain(first, entry);}}@GuardedBy("this")@CheckForNullReferenceEntry<K, V> removeEntryFromChain(ReferenceEntry<K, V> first, ReferenceEntry<K, V> entry) {// 初始化计数跟踪数量变化int newCount = count;// 被移除元素的 next 元素,作为新的头节点ReferenceEntry<K, V> newFirst = entry.getNext();// 遍历从 原头节点 first 到 被移除元素 entry 之间的所有元素for (ReferenceEntry<K, V> e = first; e != entry; e = e.getNext()) {// 创建一个新的节点,该节点是节点 e 的副本,并把新节点的 next 指针指向 newFirst 节点ReferenceEntry<K, V> next = copyEntry(e, newFirst);// 如果 next 不为空,变更 newFirst 的引用,指向刚刚创建的节点// 如果原链表为 a -> b -> c -> d 移除 c 后链表为 b -> a -> dif (next != null) {newFirst = next;} else {// 如果 next 为空,表示发生了垃圾回收,将该被回收的元素的移除removeCollectedEntry(e);// 计数减一newCount--;}}// 更新计数this.count = newCount;// 返回新的头节点return newFirst;}@GuardedBy("this")void removeCollectedEntry(ReferenceEntry<K, V> entry) {// 入队元素被移除的通知等操作enqueueNotification(entry.getKey(), entry.getHash(), entry.getValueReference().get(),entry.getValueReference().getWeight(), RemovalCause.COLLECTED);writeQueue.remove(entry);accessQueue.remove(entry);}@GuardedBy("this")void enqueueNotification(@CheckForNull K key, int hash, @CheckForNull V value, int weight, RemovalCause cause) {// 将当前元素的权重在总权重中减去totalWeight -= weight;// 如果元素被驱逐,则在计数器中记录if (cause.wasEvicted()) {statsCounter.recordEviction();}// 如果配置了驱逐通知,则将该元素被驱逐的原因等信息放入队列if (map.removalNotificationQueue != DISCARDING_QUEUE) {RemovalNotification<K, V> notification = RemovalNotification.create(key, value, cause);map.removalNotificationQueue.offer(notification);}}
}class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {// 判断元素是否过期,它的逻辑非常简单,如果配置了对应的过期模式,如访问后过期或写后过期// 会根据当前时间与元素的访问时间或写入时间进行比较,如果超过配置的过期时间,则返回 true,否则返回 falseboolean isExpired(ReferenceEntry<K, V> entry, long now) {checkNotNull(entry);if (expiresAfterAccess() && (now - entry.getAccessTime() >= expireAfterAccessNanos)) {return true;}if (expiresAfterWrite() && (now - entry.getWriteTime() >= expireAfterWriteNanos)) {return true;}return false;}
}
expand

接下来是扩容方法,因为在初始化时,Segment 对象的 table 数组并没有被创建为最大的分段的大小,而是采取了较小的值 4 作为初始大小,所以随着元素的增加,会触发数组的扩容以满足元素的存储,它的负载因子默认为 0.75,每次扩容的大小为原有长度的 2 倍。其中对原有数组中元素的迁移蛮有意思,它会将能够哈希到同一个桶的元素直接赋值到新的数组,而不能被哈希到同一个桶的元素则创建它们的深拷贝,一个个放入新的数组中,如下:

static class Segment<K, V> extends ReentrantLock {static final int MAXIMUM_CAPACITY = 1 << 30;// Segment 中保存元素的数组@CheckForNullvolatile AtomicReferenceArray<ReferenceEntry<K, V>> table;// 该段中的元素数量volatile int count;// 总的权重,不配置也表示元素数量,每个元素的权重为 1@GuardedBy("this")long totalWeight;// capacity * 0.75int threshold;@GuardedBy("this")void expand() {AtomicReferenceArray<ReferenceEntry<K, V>> oldTable = table;int oldCapacity = oldTable.length();// 如果容量已经达到最大值,则直接返回if (oldCapacity >= MAXIMUM_CAPACITY) {return;}int newCount = count;// 创建一个原来两倍容量的 AtomicReferenceArrayAtomicReferenceArray<ReferenceEntry<K, V>> newTable = newEntryArray(oldCapacity << 1);// 阈值计算,负载因子为固定的 0.75threshold = newTable.length() * 3 / 4;// 掩码值为容量大小 -1,因为容量是 2 的幂次方,所以掩码值的二进制表示中,从最低位开始,所有位都是 1,位与运算能计算出元素对应的索引int newMask = newTable.length() - 1;// 遍历扩容前的 AtomicReferenceArray tablefor (int oldIndex = 0; oldIndex < oldCapacity; ++oldIndex) {ReferenceEntry<K, V> head = oldTable.get(oldIndex);if (head != null) {// 获取头节点的 next 节点ReferenceEntry<K, V> next = head.getNext();// 计算头节点在新数组中的索引int headIndex = head.getHash() & newMask;// 头节点的 next 节点为空,那么证明该桶位置只有一个元素,直接将头节点放在新数组的索引处 if (next == null) {newTable.set(headIndex, head);} else {// 遍历从 next 开始的节点,找到所有能 hash 到同一个桶的一批节点,然后将这些节点封装在新数组的对应索引处ReferenceEntry<K, V> tail = head;int tailIndex = headIndex;for (ReferenceEntry<K, V> e = next; e != null; e = e.getNext()) {int newIndex = e.getHash() & newMask;if (newIndex != tailIndex) {tailIndex = newIndex;tail = e;}}newTable.set(tailIndex, tail);// 将这些剩余的不能 hash 到同一个桶的节点,依次遍历,将它们放入新数组中for (ReferenceEntry<K, V> e = head; e != tail; e = e.getNext()) {int newIndex = e.getHash() & newMask;ReferenceEntry<K, V> newNext = newTable.get(newIndex);// 注意这里创建节点是深拷贝,并且采用的是头插法ReferenceEntry<K, V> newFirst = copyEntry(e, newNext);if (newFirst != null) {newTable.set(newIndex, newFirst);} else {// 如果 next 为空,表示发生了垃圾回收,将该被回收的元素的移除removeCollectedEntry(e);newCount--;}}}}}// 操作完成后更新 table 和 counttable = newTable;this.count = newCount;}}
evictEntries

元素的驱逐方法 evictEntries 主要负责维护缓存不超过最大容量限制,实现原理也非常简单,它会根据 accessQueue 队列,将最近最少访问的元素优先移除:

static class Segment<K, V> extends ReentrantLock {@Weak final LocalCache<K, V> map;final long maxSegmentWeight;@GuardedBy("this")long totalWeight;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> accessQueue;@GuardedBy("this")void evictEntries(ReferenceEntry<K, V> newest) {if (!map.evictsBySize()) {return;}// 这个方法我们在前文中介绍过// 将元素的最近被访问队列 recencyQueue 清空,并使用尾插法将它们都放到访问队列 accessQueue 的尾节点drainRecencyQueue();// 如果新加入的元素权重超过了最大权重,那么直接将该元素驱逐if (newest.getValueReference().getWeight() > maxSegmentWeight) {if (!removeEntry(newest, newest.getHash(), RemovalCause.SIZE)) {throw new AssertionError();}}// 如果当前权重超过最大权重,那么不断地驱逐元素直到满足条件while (totalWeight > maxSegmentWeight) {// 在 accessQueue 中从头节点遍历元素,依次移除最近没有被访问的元素ReferenceEntry<K, V> e = getNextEvictable();if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {throw new AssertionError();}}}@GuardedBy("this")ReferenceEntry<K, V> getNextEvictable() {for (ReferenceEntry<K, V> e : accessQueue) {int weight = e.getValueReference().getWeight();if (weight > 0) {return e;}}throw new AssertionError();}
}
postWriteCleanup

写后清理操作需要关注的内容并不多,它主要处理监听器相关的逻辑,因为在我们的例子中并没有创建监听器所以就不在详细分析了。

static class Segment<K, V> extends ReentrantLock {@Weak final LocalCache<K, V> map;void postWriteCleanup() {runUnlockedCleanup();}void runUnlockedCleanup() {// locked cleanup may generate notifications we can send unlockedif (!isHeldByCurrentThread()) {map.processPendingNotifications();}}
}class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {final Queue<RemovalNotification<K, V>> removalNotificationQueue;final RemovalListener<K, V> removalListener;void processPendingNotifications() {RemovalNotification<K, V> notification;while ((notification = removalNotificationQueue.poll()) != null) {try {removalListener.onRemoval(notification);} catch (Throwable e) {logger.log(Level.WARNING, "Exception thrown by removal listener", e);}}}
}

get

接下来我们要深入 get 方法的分析了,同样地它也会被路由(segmentFor)到具体的 Segment

static class LocalManualCache<K, V> implements Cache<K, V>, Serializable {final LocalCache<K, V> localCache;// ...
}static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>implements LoadingCache<K, V> {@Overridepublic V get(K key) throws ExecutionException {return localCache.getOrLoad(key);}
}
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {@CheckForNull final CacheLoader<? super K, V> defaultLoader;V getOrLoad(K key) throws ExecutionException {return get(key, defaultLoader);}@CanIgnoreReturnValueV get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {int hash = hash(checkNotNull(key));return segmentFor(hash).get(key, hash, loader);}
}

它的核心逻辑也在 Segment 中,注意读操作是不加锁的,它相比与 put 方法要简单,其中要注意的是 recordRead 方法,它会将被访问的元素添加到 recencyQueue 最近访问队列中,用于记录最近被访问元素的顺序,后续执行维护(cleanUp)相关的逻辑时,会将该队列中的元素全部移动到 accessQueue 队列中,用于根据元素的访问顺序来判断元素是否被驱逐。除此之外,因为元素的驱逐大多都是在 put 方法中完成的,为了在不发生写操作的情况下也能正常管理元素的生命中期,在 get 方法中也有相关的实现,比如 postReadCleanup 方法,它通过 readCount 的计数和 DRAIN_THRESHOLD 的阈值来判断是否需要驱逐元素,当计数超过阈值时则调用 cleanUp 方法进行驱逐,当然这并不会强制执行(因为它执行的是 ReentrantLock#tryLock 方法),尝试获取锁来执行,即使没有获取到锁,那么证明有写操作在执行,元素的驱逐操作也不需要再多关心了。源码如下所示:

static class Segment<K, V> extends ReentrantLock {static final int DRAIN_THRESHOLD = 0x3F;volatile int count;@Weak final LocalCache<K, V> map;final Queue<ReferenceEntry<K, V>> recencyQueue;final StatsCounter statsCounter;final AtomicInteger readCount = new AtomicInteger();@CanIgnoreReturnValueV get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {checkNotNull(key);checkNotNull(loader);try {if (count != 0) {ReferenceEntry<K, V> e = getEntry(key, hash);// 获取到对应元素if (e != null) {long now = map.ticker.read();// 获取到对应的 valueV value = getLiveValue(e, now);// value 不为空if (value != null) {// 记录读操作,会将元素添加到最近访问队列 recencyQueue 的尾节点recordRead(e, now);// 命中计数 +1statsCounter.recordHits(1);// 如果配置了元素刷新则执行相关刷新逻辑return scheduleRefresh(e, key, hash, value, now, loader);}// value 为空ValueReference<K, V> valueReference = e.getValueReference();// 如果 value 正在 loading 则等待if (valueReference.isLoading()) {return waitForLoadingValue(e, key, valueReference);}}}// 如果元素数量为 0,则执行该方法return lockedGetOrLoad(key, hash, loader);} catch (ExecutionException ee) {Throwable cause = ee.getCause();if (cause instanceof Error) {throw new ExecutionError((Error) cause);} else if (cause instanceof RuntimeException) {throw new UncheckedExecutionException(cause);}throw ee;} finally {postReadCleanup();}}@CheckForNullReferenceEntry<K, V> getEntry(Object key, int hash) {// 获取到 table 数组中对应桶的头节点for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {// 元素 hash 值不相等继续遍历寻找if (e.getHash() != hash) {continue;}// 找到对应的元素后,如果 key 为空,则处理引用队列K entryKey = e.getKey();if (entryKey == null) {tryDrainReferenceQueues();continue;}// 如果 key 值相等,则返回该元素if (map.keyEquivalence.equivalent(key, entryKey)) {return e;}}// 否则返回 nullreturn null;}V getLiveValue(ReferenceEntry<K, V> entry, long now) {// key 和 value 为空的情况,处理引用队列if (entry.getKey() == null) {tryDrainReferenceQueues();return null;}V value = entry.getValueReference().get();if (value == null) {tryDrainReferenceQueues();return null;}// 如果元素过期了if (map.isExpired(entry, now)) {// 尝试加锁执行元素过期驱逐操作tryExpireEntries(now);return null;}return value;}void recordRead(ReferenceEntry<K, V> entry, long now) {// 如果配置了访问后过期,则更新元素访问时间if (map.recordsAccess()) {entry.setAccessTime(now);}// 将元素添加到最近访问队列中recencyQueue.add(entry);}V waitForLoadingValue(ReferenceEntry<K, V> e, K key, ValueReference<K, V> valueReference) throws ExecutionException {if (!valueReference.isLoading()) {throw new AssertionError();}checkState(!Thread.holdsLock(e), "Recursive load of: %s", key);try {// 等待元素加载完成V value = valueReference.waitForValue();if (value == null) {throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");}// 执行记录读操作long now = map.ticker.read();recordRead(e, now);return value;} finally {// 未命中统计数+1statsCounter.recordMisses(1);}}// 通常在写入过程中进行清理。如果在足够数量的读取后没有观察到清理,请尝试从读取线程进行清理。void postReadCleanup() {// readCount 自增,如果达到阈值则执行清理操作,清理操作与写入操作中的逻辑一致if ((readCount.incrementAndGet() & DRAIN_THRESHOLD) == 0) {cleanUp();}}void cleanUp() {long now = map.ticker.read();runLockedCleanup(now);runUnlockedCleanup();}
}
lockedGetOrLoad

lockedGetOrLoadLoadingCache 的核心逻辑,在某个 key 在缓存中不存在且配置了 loader 函数时会被执行,本质上这也是触发了一次写操作,将不存在的元素通过 loader 方法获取到具体值并加载到缓存中,所以在这里也会触发到元素生命周期管理的方法,如 preWriteCleanup,不过该方法总体上的实现与 put 方法类似,并没有特别复杂的地方。

static class Segment<K, V> extends ReentrantLock {volatile int count;@Weak final LocalCache<K, V> map;@CheckForNull volatile AtomicReferenceArray<ReferenceEntry<K, V>> table;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> writeQueue;@GuardedBy("this")final Queue<ReferenceEntry<K, V>> accessQueue;final StatsCounter statsCounter;int modCount;// 加锁读或加载元素值V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {ReferenceEntry<K, V> e;ValueReference<K, V> valueReference = null;LoadingValueReference<K, V> loadingValueReference = null;boolean createNewEntry = true;// 加锁lock();try {long now = map.ticker.read();// 写前整理操作,已经在上文中介绍过preWriteCleanup(now);int newCount = this.count - 1;// 找到该元素对应桶的头节点AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;int index = hash & (table.length() - 1);ReferenceEntry<K, V> first = table.get(index);// 遍历寻找该元素for (e = first; e != null; e = e.getNext()) {K entryKey = e.getKey();// 如果找到了与该元素 key 值相等的元素if (e.getHash() == hash && entryKey != null && map.keyEquivalence.equivalent(key, entryKey)) {valueReference = e.getValueReference();// 判断元素是否在加载中if (valueReference.isLoading()) {// 如果在加载中则不创建新的元素createNewEntry = false;} else {// 否则执行元素的加载V value = valueReference.get();if (value == null) {// 如果获取到的值为空,说明元素已经被回收了,则将该事件通知放到队列中enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);} else if (map.isExpired(e, now)) {// 如果元素过期了,则将该事件通知放到队列中enqueueNotification(entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);} else {// 否则,获取到了 value 值,执行记录读操作recordLockedRead(e, now);// 命中统计数+1statsCounter.recordHits(1);return value;}// value 无效的情况都需要将元素在写队列和访问队列中移除writeQueue.remove(e);accessQueue.remove(e);// 可见性写操作this.count = newCount;}break;}}// 没有找到该元素,则需要创建新元素if (createNewEntry) {// value 的类型为 LoadingValueReferenceloadingValueReference = new LoadingValueReference<>();if (e == null) {e = newEntry(key, hash, first);e.setValueReference(loadingValueReference);table.set(index, e);} else {e.setValueReference(loadingValueReference);}}} finally {// 解锁和写后通知操作unlock();postWriteCleanup();}if (createNewEntry) {try {// 同步执行元素的加载synchronized (e) {return loadSync(key, hash, loadingValueReference, loader);}} finally {// 元素未命中统计数+1statsCounter.recordMisses(1);}} else {// 元素已经存在,等在加载return waitForLoadingValue(e, key, valueReference);}}V loadSync(K key, int hash, LoadingValueReference<K, V> loadingValueReference, CacheLoader<? super K, V> loader) throws ExecutionException {ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);return getAndRecordStats(key, hash, loadingValueReference, loadingFuture);}@CanIgnoreReturnValueV getAndRecordStats(K key, int hash, LoadingValueReference<K, V> loadingValueReference, ListenableFuture<V> newValue) throws ExecutionException {V value = null;try {// 阻塞执行 loader 函数(不允许中断)value = getUninterruptibly(newValue);if (value == null) {throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");}// 标记元素加载完成statsCounter.recordLoadSuccess(loadingValueReference.elapsedNanos());// 保存加载的元素storeLoadedValue(key, hash, loadingValueReference, value);return value;} finally {if (value == null) {statsCounter.recordLoadException(loadingValueReference.elapsedNanos());removeLoadingValue(key, hash, loadingValueReference);}}}// 逻辑与 put 方法类似@CanIgnoreReturnValueboolean storeLoadedValue(K key, int hash, LoadingValueReference<K, V> oldValueReference, V newValue) {// 加锁lock();try {long now = map.ticker.read();preWriteCleanup(now);int newCount = this.count + 1;if (newCount > this.threshold) {expand();newCount = this.count + 1;}// 找到该元素对应的桶的头节点AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;int index = hash & (table.length() - 1);ReferenceEntry<K, V> first = table.get(index);for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {K entryKey = e.getKey();if (e.getHash() == hash && entryKey != null && map.keyEquivalence.equivalent(key, entryKey)) {ValueReference<K, V> valueReference = e.getValueReference();V entryValue = valueReference.get();// 如果元素已经存在,则替换旧的 LoadingValueReferenceif (oldValueReference == valueReference || (entryValue == null && valueReference != UNSET)) {++modCount;if (oldValueReference.isActive()) {RemovalCause cause = (entryValue == null) ? RemovalCause.COLLECTED : RemovalCause.REPLACED;enqueueNotification(key, hash, entryValue, oldValueReference.getWeight(), cause);newCount--;}setValue(e, key, newValue, now);this.count = newCount;evictEntries(e);return true;}enqueueNotification(key, hash, newValue, 0, RemovalCause.REPLACED);return false;}}++modCount;// 没有找到该 key 对应的元素则创建新的元素,此处逻辑与 put 元素的操作一致ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);setValue(newEntry, key, newValue, now);table.set(index, newEntry);this.count = newCount;evictEntries(newEntry);return true;} finally {unlock();postWriteCleanup();}}
}

与 Caffeine 的对比

Guava Cache 的源码要比 Caffeine 简单得多,但是 Caffeine 的实现更加优雅,可读性也更高(详细参阅万文详解 Caffeine 实现原理)。

Guava Cache 采用的是分段锁的思想,这种思想在 JDK 1.7 的 ConcurrentHashMap 也有实现,但由于性能相对较差,在 JDK 1.8 及之后被弃用,取而代之的是使用 CAS 操作、少量 synchronized 关键字同步操作、及合适的 自旋重试volatile 关键字的方案,而在 Caffeine 中,底层实现采用的便是 ConcurrentHashMap,它是在 ConcurrentHashMap 之上添加了缓存相关的管理功能,如缓存过期、缓存淘汰等(相比于 Guava Cache 功能也更丰富),在这一点上 Caffeine 就已经占尽了优势,能够高效支持更大规模的并发访问,而且还能随着 JDK 升级过程中对 ConcurrentHashMap 的优化而持续享受优化后带来的并发效率的提升。

在 Guava Cache 中针对缓存的驱逐采用了 LRU 算法,实际上这种驱逐策略并不精准,在 Caffeine 中提供了基于 TinyLFU 算法 的驱逐策略,这种算法在对缓存驱逐的准确性上更高,能更好的提供缓存命中率和保证缓存的性能。

除此之外,Caffeine 提供了更复杂的缓存过期管理机制:TimeWheel,这一点在 Guava Cache 中是没有的(本地缓存 Caffeine 中的时间轮(TimeWheel)是什么?)。另外,在对内存性能的优化上,Caffeine 针对常访问的字段避免了缓存伪共享问题(高性能缓存设计:如何解决缓存伪共享问题),这些在 Guava Cache 中也是没有任何体现的。

总而言之,Caffeine 是更加现代化的本地缓存,可以说它全面优于 Guava Cache,在技术选型上,可以优先考虑。不过,Guava Cache 也并不是一无是处,在对性能要求不高的场景下且不想引入额外的依赖(Caffeine),Guava Cache 也是一个不错的选择,因为 Guava 作为常用的 Java 库,通常都会在依赖中存在。


巨人的肩膀

  • Github - Guava

相关文章:

缓存之美:Guava Cache 相比于 Caffeine 差在哪里?

大家好&#xff0c;我是 方圆。本文将结合 Guava Cache 的源码来分析它的实现原理&#xff0c;并阐述它相比于 Caffeine Cache 在性能上的劣势。为了让大家对 Guava Cache 理解起来更容易&#xff0c;我们还是在开篇介绍它的原理&#xff1a; Guava Cache 通过分段&#xff08;…...

Go string 字符串底层逻辑

在 Go 语言中&#xff0c;string 类型的底层结构是一个结构体&#xff0c;包含两个字段&#xff1a;一个指向字节数组的指针和该字节数组的长度。以下是其在 Go 源码中的大致定义&#xff1a;type stringStruct struct {str unsafe.Pointerlen int } str&#xff1a;这是一个指…...

高效集成聚水潭采购退货数据到MySQL的最佳实践

聚水潭数据集成到MySQL&#xff1a;采购退货单的高效对接方案 在企业的数据管理和分析过程中&#xff0c;数据的准确性和实时性至关重要。本文将分享一个具体的系统对接集成案例&#xff1a;如何通过轻易云数据集成平台&#xff0c;将聚水潭中的采购退货单数据高效地集成到MyS…...

STM32步进电机S型与T型加减速算法

目录 一、基本原理 二、常见类型 三、算法详解 四、应用场合 五、代码实现 1、main...

centos操作系统上传和下载百度网盘内容

探序基因 整理 进入百度网盘官网百度网盘 客户端下载 下载linux的rpm格式的安装包 在linux命令行中输入&#xff1a;rpm -ivh baidunetdisk_4.17.7_x86_64.rpm 出现报错&#xff1a; 错误&#xff1a;依赖检测失败&#xff1a; libXScrnSaver 被 baidunetdisk-4.17.7-1.x8…...

深入 Python 网络爬虫开发:从入门到实战

一、为什么需要爬虫&#xff1f; 在数据驱动的时代&#xff0c;网络爬虫是获取公开数据的重要工具。它可以帮助我们&#xff1a; 监控电商价格变化抓取学术文献构建数据分析样本自动化信息收集 二、基础环境搭建 1. 核心库安装 pip install requests beautifulsoup4 lxml …...

网络爬虫【简介】

我叫补三补四&#xff0c;很高兴见到大家&#xff0c;欢迎一起学习交流和进步 今天来讲一讲爬虫 一、网络爬虫的定义 网络爬虫&#xff08;Web Crawler&#xff09;&#xff0c;又称为网络蜘蛛、网络机器人等&#xff0c;是一种按照一定规则自动抓取互联网信息的程序或脚本。它…...

Linux:Ubuntu server 24.02 上搭建 ollama + dify

一、安装Ubuntu 具体的安装过程可以参见此链接&#xff1a;链接&#xff1a;Ubuntu Server 20.04详细安装教程&#xff0c;这里主要记录一下过程中遇到的问题。 安装时subnet如何填写 在Ubuntu中subnet填写255.255.255.0是错误的&#xff0c;其格式为 xx.xx.xx.xx/yy &#…...

【生日蛋糕——DFS剪枝优化】

题目 分析 代码 #include <bits/stdc.h> using namespace std;const int N 24; const int inf 0x3f3f3f3f;int mins[N], minv[N]; int R[N], H[N]; int n, m, ans inf;void dfs(int u, int v, int s) {if(v minv[u] > n) return;if(s mins[u] > ans) return;…...

RabbitMq C++客户端的使用

1.RabbitMq介绍 RabbitMQ 是一款开源的消息队列中间件&#xff0c;基于 AMQP&#xff08;高级消息队列协议&#xff09;实现&#xff0c;支持多种编程语言和平台。以下是其核心特点和介绍&#xff1a; 核心特点 多语言支持 提供 Java、Python、C#、Go、JavaScript 等语言的客…...

入门基础项目-前端Vue_02

文章目录 1. 用户信息1.1 整体设计1.2 完整代码 User.vue1.2.1 数据加载1.2.2 表格 el-table1.2.2.1 多选1.2.2.2 自定义列的内容 Slot1.2.2.3 图片 el-image1.2.2.4 分页 el-pagination 1.2.3 编辑1.2.3.1 弹出框 el-dialog1.2.3.2 上传 el-upload 1.2.4 新增1.2.5 删除1.2.6 …...

C#中SerialPort 的使用

最近在学习C#的SerialPort &#xff0c;关于SerialPort 的使用&#xff0c;做如下总结&#xff1a; 1.可以通过函数System.IO.Ports.SerialPort.GetPortNames() 将获得系统所有的串口名称。C#代码如下&#xff1a; string[] sPorts SerialPort.GetPortNames(); foreach(stri…...

使用py-ffmpeg批量合成视频的脚本

我有一个小米摄像头&#xff0c;用它录出来的视频全部都是3s一段3s一段的。其中有几个小时的视频我需要保存&#xff0c;当初直接把摄像头的卡文件导出来重命名掉了&#xff0c;那时候没有注意&#xff0c;之后想剪辑/发送给别人的时候发现疯了&#xff1a; 1.剪辑的话&#x…...

mac安装navicat及使用

0.删除旧的 sudo rm -Rf /Applications/Navicat\ Premium.app sudo rm -Rf /private/var/db/BootCaches/CB6F12B3-2C14-461E-B5A7-A8621B7FF130/app.com.prect.NavicatPremium.playlist sudo rm -Rf ~/Library/Caches/com.apple.helpd/SDMHelpData/Other/English/HelpSDMIndexF…...

织梦dedecmsV5.7提示信息提示框美化(带安装教程和效果展示)

一、效果展示 1、安装前效果 2、安装后效果 二、安装说明 1、安装测试版本&#xff1a;DedeCMS-V5.7.117-UTF8&#xff1b; 2、必须在修改代码之前请做好文件备份&#xff0c;以免误操无法恢复&#xff1b; 3、为了兼容其他版本&#xff0c;请在安装时&#xff0c;最好将替…...

【知识迁移的底层逻辑:从符号到语义的升维】

大语言模型&#xff08;LLMs&#xff09;能够通过有限语料库实现广泛知识迁移并回答多样化问题&#xff0c;其核心机制在于抽象模式学习、上下文推理能力及知识组合泛化&#xff0c;而非简单的数据记忆。以下是具体实现路径与技术原理&#xff1a; 一、知识迁移的底层逻辑&…...

Windows根据文件名批量在文件夹里查找文件并复制出来,用WPF实现的详细步骤

项目前言 在日常工作和生活中&#xff0c;我们常常会遇到需要从大量文件中根据文件名批量查找特定文件并复制到指定位置的情况。手动一个个查找和复制文件不仅效率低下&#xff0c;还容易出错。使用 Windows Presentation Foundation (WPF) 可以创建一个用户友好的图形界面应用…...

Certbot实现SSL免费证书自动续签(CentOS 7版 + Docker部署的nginx)

前置安装&#xff0c;可参考Certbot实现SSL免费证书自动续签&#xff08;CentOS 7 nginx/apache&#xff09; 如果是通过 Docker 运行 Nginx&#xff0c; certbot 无法直接检测到本地的 Nginx 配置。解决方案是 使用 standalone 模式 或 挂载 Webroot 方式获取 SSL 证书&…...

一周学会Flask3 Python Web开发-SQLAlchemy查询所有数据操作-班级模块

锋哥原创的Flask3 Python Web开发 Flask3视频教程&#xff1a; 2025版 Flask3 Python web开发 视频教程(无废话版) 玩命更新中~_哔哩哔哩_bilibili 我们来新建一个的蓝图模块-班级模块&#xff0c;后面可以和学生模块&#xff0c;实现一对多的数据库操作。 blueprint下新建g…...

工程实践:如何使用SU17无人机来实现室内巡检任务

阿木实验室最近发布了科研开发者版本的无人机SU17&#xff0c;该无人机上集成了四目视觉&#xff0c;三维激光雷达&#xff0c;云台吊舱&#xff0c;高算力的机载计算机&#xff0c;是一个非常合适的平台用于室内外巡检场景。同时阿木实验室维护了多个和无人机相关的开源项目。…...

14.使用各种读写包操作 Excel 文件:辅助模块

一 各种读写包 这些是 pandas 在底层使用的各种读写包。无须安装 pandas&#xff0c;直接使用这些读写包就能够读写 Excel 工作簿。可以尽可能地使用 pandas 来解决这类问题&#xff0c;只在 pandas 没有提供你所需要的功能时才用到读写包。 表中没有 xlwings &#xff0c;因为…...

深入理解 Maven BOM 及其继承特性

深入理解 Maven BOM 及其继承特性 一、什么是 Maven BOM&#xff1f; Maven BOM&#xff08;Bill Of Materials&#xff0c;物料清单&#xff09;是一种特殊的 Maven 项目&#xff0c;用于集中管理依赖项的版本信息。BOM 项目本身并不包含实际的代码或资源&#xff0c;而仅仅…...

责任链模式如何减少模块之间的耦合

责任链模式如何减少模块之间的耦合 在复杂的软件系统中&#xff0c;模块之间的耦合是一个常见的问题。高耦合的代码不仅增加了维护成本&#xff0c;还会导致系统的扩展性和灵活性受限。当我们需要为不同的请求设计灵活的处理逻辑时&#xff0c;传统的硬编码方式会将请求的发送…...

Java面试:集合框架体系

一、ArrayList 1.数组&#xff08;Array&#xff09; 是一种用连续的内存空间存储相同数据类型数据的线性数据结构 数组如何获取其他元素的地址值&#xff1f; 寻址公式&#xff1a;a[i] baseAddress i * dataTypeSize baseAddress&#xff1a;数组的首地址dataTypeSize&am…...

【八股文】ArrayList和LinkedList的区别

先讲讲两者是如何实现的 ArrayList public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable {transient Object[] elementData; private int size; } 通过源码可以看出&#xff0c;ArrayLis…...

sentinel限流算法

限流算法&#xff1a;固定窗口算法、滑动时间窗口、令牌桶和漏桶这四种常见限流算法的原理&#xff1a; 限流算法原理 固定窗口&#xff1a; 固定窗口算法将时间划分为固定大小的窗口&#xff0c;并在每个窗口内限制请求的数量。在每个窗口开始时&#xff0c;计数器重置&#…...

Spring生态下的中台架构设计:如何构建可扩展业务系统?

一、中台战略的架构觉醒 在数字化转型的浪潮中,企业面临的核心矛盾日益凸显:前端业务的快速迭代需求与后端系统刚性架构之间的矛盾。中台架构的提出,本质上是对传统单体架构和过度微服务化的辩证扬弃。Spring生态以其模块化设计理念,恰好为中台建设提供了绝佳的技术土壤。…...

Project回调函数qsort②进阶应用

#define _CRT_SECURE_NO_WARNINGS #include<stdio.h> #include<string.h>//库函数strcmp头文件 //使用qsort排序结构体 struct Stu { char name[20]; int age; }; //按照年龄排序 int cmp_stu_by_age(const void* e1,const void* e2) { return ((struc…...

【推荐项目】052-用水监控管理系统

052-用水监控管理系统 介绍 用水监控管理系统 springboot java vuejs jdk1.8 当然&#xff0c;以下是一个简洁的用水监控管理系统的功能模块划分&#xff0c;基于Spring Boot&#xff08;JDK 1.8&#xff09;后端和Vue.js前端&#xff1a; 用水监控管理系统功能模块 后端&…...

Hive SQL 精进系列:PERCENTILE_APPROX 搞定分位数

目录 一、引言二、percentile_approx 函数基础2.1 基本语法参数解释返回值简单示例 三、应用场景3.1 数据分析与报告3.2 数据清洗与异常值检测3.3 性能监控与优化 四、使用注意事项4.1 数据类型要求4.2 精度与性能平衡4.3 空值处理 五、总结 一、引言 百分位数作为一种常用的统…...

使用Hbuilder发布小程序显示发布失败?

接受了一个新uniapp项目 但是在Hbuilder中发行报错 小程序发行失败 试了几次还是不行 写代码的人也走了&#xff0c;头疼。 不用Hbuilder小程序的主包体积又太大哎 开发工具无法上传~ 后来想看一下 这个发布失败到底有没有生成打包好的文件 如果生成了可以试一下 直接导入到微信…...

甲骨文找回二次验证的方法(超简单)

因为更换手机丢失了二次验证。 然后给客服沟通&#xff0c;获得了找到二次验证的办法&#xff0c;希望对你有用。 1、登录到账号登陆界面&#xff0c;查看地址栏当中自己的IDCE地址&#xff08;yourIDCS_Stripe_here&#xff09;部分&#xff0c;并复制。 https://idcs-yourID…...

Tcp网络通信的基本流程梳理

先来一张经典的流程图 接下介绍一下大概流程&#xff0c;各个函数的参数大家自己去了解加深一下印象 服务端流程 1.创建套接字&#xff1a;使用 socket 函数创建一个套接字&#xff0c;这个套接字后续会被用于监听客户端的连接请求。 需要注意的是&#xff0c;服务端一般有俩…...

C++相关基础概念之入门讲解(上)

1. 命名空间 C中的命名空间&#xff08;namespace&#xff09;是用来避免命名冲突问题的一种机制。通过将类、函数、变量等封装在命名空间中&#xff0c;可以避免不同部分的代码中出现相同名称的冲突。在C中&#xff0c;可以使用namespace关键字来定义命名空间。 然后我们在调…...

Redis能否替代MySQL作为主数据库?深入解析两者的持久化差异与适用边界——基于AOF持久化与关系型数据库的对比

一、Redis的持久化机制与可靠性分析 ​AOF持久化原理与策略 Redis的AOF&#xff08;Append Only File&#xff09;通过记录所有写操作命令实现持久化&#xff0c;支持三种策略&#xff1a; ​**always模式**&#xff1a;每条命令执行后立即同步到磁盘&#xff0c;理论上数据丢失…...

Hive函数大全:从核心内置函数到自定义UDF实战指南(附详细案例与总结)

目录 背景‌一、Hive函数分类与核心函数表‌1. 内置函数分类‌2. 用户自定义函数(UDF)分类二、常用函数详解与实战案例‌1. 数学函数‌2. 字符串函数‌3. 窗口函数‌4. 自定义UDF实战‌三、总结与优化建议‌1. 核心总结2. 性能优化建议‌3. 常问问题背景‌ Hive作为Hadoop生…...

如何修改 Ubuntu 软件源(镜像源)

如何修改 Ubuntu 软件源&#xff08;镜像源&#xff09; 前言 在使用 Ubuntu 时&#xff0c;默认的软件源可能速度较慢&#xff0c;影响软件安装和系统更新的效率。我们可以通过修改 sources.list 文件或使用图形界面更换更快的镜像源&#xff0c;提升软件包管理的速度。 本…...

在Spring Boot项目中接入DeepSeek深度求索,感觉笨笨的呢

文章目录 引言1. 什么是DeepSeek&#xff1f;2. 准备工作2.1 注册DeepSeek账号 3.实战演示3.1 application增加DS配置3.2 编写service3.3 编写controller3.4 编写前端界面chat.html3.5 测试 总结 引言 在当今快速发展的数据驱动时代&#xff0c;企业越来越重视数据的价值。为了…...

AP AR

混淆矩阵 真实值正例真实值负例预测值正例TPFP预测值负例FNTN &#xff08;根据阈值预测&#xff09; P精确度计算&#xff1a;TP/(TPFP) R召回率计算&#xff1a;TP/(TPFN) AP 综合考虑P R 根据不同的阈值计算出不同的PR组合&#xff0c; 画出PR曲线&#xff0c;计算曲线…...

Vue 中的 MVVM、MVC 和 MVP 模式深度解析

文章目录 1. 模式概览与核心概念1.1 模式定义1.2 架构对比图 2. MVC 模式详解2.1 MVC 流程图2.2 Vue 中的 MVC 实现 3. MVP 模式详解3.1 MVP 流程图3.2 Vue 中的 MVP 实现 4. MVVM 模式详解4.1 MVVM 流程图4.2 Vue 中的 MVVM 实现 5. 模式对比分析5.1 职责对比5.2 通信方式对比…...

WVP前后端部署

使用默认的构建&#xff0c;能够直接访问18080&#xff0c;我以为二者是一起的。实际上这不影响前后端分离。 前端服务器 构建war之后&#xff0c;部署到另外一台机器上&#xff0c;比如使用apache2。 后端服务器 修改src/main/resources/static/static/js/config.js&#…...

VSTO(C#)Excel开发9:处理格式和字体

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github&#xff1a;codetoys&#xff0c;所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的&#xff0c;可以在任何平台上使用。 源码指引&#xff1a;github源…...

蓝耘MaaS平台:阿里QWQ应用拓展与调参实践

摘要&#xff1a;本文深入探讨了蓝耘MaaS平台与阿里QWQ模型的结合&#xff0c;从平台架构、模型特点到应用拓展和调参实践进行了全面分析。蓝耘平台凭借其强大的算力支持、弹性资源调度和全栈服务&#xff0c;为QWQ模型的高效部署提供了理想环境。通过细化语义描述、调整推理参…...

【统计学相关笔记】2. 多元正态的Cochran定理

fisher 引理 如何说明一个线性变换和二次型独立&#xff1a; 二次型矩阵和线性变换阵乘积0即可。...

Vuex 基础概念与环境搭建

Vuex 是实现数据集中式状态管理的插件。所有组件共享 Vuex 中的数据&#xff0c;当任意组件修改数据时&#xff0c;其他组件会同步更新。与全局事件总线的区别在于&#xff1a; 全局事件总线&#xff1a;数据传递但未真正共享Vuex&#xff1a;数据存储在中央仓库&#xff0c;实…...

使用 BookMarkHub 插件进行书签同步

前言: 通过 BookMarkHub 插件&#xff0c;你可以方便地将书签同步到 GitHub Gist&#xff0c;实现跨设备管理书签。以下是详细的步骤&#xff1a; 使用 BookMarkHub 插件进行书签同步 1. 安装 BookMarkHub 插件2. 获取 GitHub Token3. 获取 Gist ID4. 配置 BookMarkHub 插件5.完…...

用Lua脚本实现Redis原子操作

1. 环境准备 依赖&#xff1a;在pom.xml中添加Spring Data Redis&#xff1a; <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId> </dependency>配置RedisTemplate&#…...

【算法】位运算

文章目录 1. 常见位运算总结&#xff08;图片包含五道算法题&#xff09;2. leetcode 面试题 01.01 判断字符是否唯一2.1 题目2.2 思路2.3 代码 3.leetcode 268. 丢失的数字3.1 题目3.2 思路3.3 代码 4. leetcode 371.两整数之和4.1 题目4.2 思路4.3 代码 5.leetcode 137.只出现…...

HTML块级元素和内联元素(简单易懂)

在HTML中&#xff0c;元素可以分为块级元素&#xff08;Block-level elements&#xff09;和内联元素&#xff08;Inline elements&#xff09;。这两类元素在页面布局和样式应用上有不同的特点和用途。 一、块级元素&#xff08;Block-level elements&#xff09; 1. 定义 …...

【论文精读】DifFace: Blind Face Restoration with Diffused Error Contraction

文章目录 0.前言1.当前问题2.怎么解决问题3.具体做法(Method)3.1 受什么的启发&#xff1f;(Motivation)3.2具体的模型设计(Design)3.3 整体算法 4.实验效果4.1 Synthetic(CelebA-Test)4.2 Real World &#xff08;LFW, WebPhoto, and WIDER&#xff09; 0.前言 这篇文章是被 …...