长沙建站长沙网站彩票网站的建设

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

长沙建站长沙网站,彩票网站的建设,购物网站怎么建立,电子商务网站建设与规划视频大家好#xff0c;我是 方圆。本文将结合 Guava Cache 的源码来分析它的实现原理#xff0c;并阐述它相比于 Caffeine Cache 在性能上的劣势。为了让大家对 Guava Cache 理解起来更容易#xff0c;我们还是在开篇介绍它的原理#xff1a; Guava Cache 通过分段#xff08;…大家好我是 方圆。本文将结合 Guava Cache 的源码来分析它的实现原理并阐述它相比于 Caffeine Cache 在性能上的劣势。为了让大家对 Guava Cache 理解起来更容易我们还是在开篇介绍它的原理 Guava Cache 通过分段Segment锁ReentrantLock机制、volatile 变量和多种缓存策略实现了性能相对 Caffeine 性能较差的缓存它的数据结构如上图所示。它会将缓存分成多个段Segment去管理单个段内写操作加锁互斥如果要创建大小为 1000 的缓存那么实际上会分配 4 个段每个段的最大容量为 250。读写操作在执行时都会经 segmentFor 方法“路由”到某一个段。数据结构的实现都在 Segment 中它对元素的管理采用的是 AtomicReferenceArray 数组在初始化时是较小的容量并随着元素的增多触发扩容机制。我们称数组中每个索引的位置为“桶”每个桶中保存了元素的引用这些元素是通过单向链表维护的每当有新元素添加时采用的是“头插法”。此外在 Segment 中还维护了三个基于 LRU 算法 的队列处于尾部的元素最“新”分别是 accessQueue、writeQueue 和 recencyQueue它们分别用于记录被访问的元素、被写入的元素和“最近”被访问的元素。accessQueue 的主要作用是在对超过最大容量超过访问后过期时间的元素进行驱逐时优先将最近被访问的越少的元素驱逐头节点开始遍历writeQueue 的主要作用是对写后过期的元素进行驱逐时优先将最近最少被访问的元素驱逐因为越早被添加的元素越早过期当发现某元素未过期时后续队列中的元素是不需要判断的recencyQueue 的作用是记录被访问过的元素它们最终都会被移动到 accessQueue 中并根据访问顺序添加到其尾节点中。 对元素生命周期的管理主要是在 put 方法中完成的put 相关的操作都需要加锁如图中左上方所示这些方法均与缓存元素的管理相关。Guava Cache 为了在不触发写操作而有大量读操作时也能正常触发对缓存元素的管理添加了一个 readCount 变量每次读请求都会使其累加直到该变量超过规定阈值也会触发缓存元素的驱逐postReadCleanUp保证数据的一致性如图中右上方所示。 接下来我们通过创建最大大小为 1000并配置有访问后和写后过期时间的 LoadingCache 来分析 Guava Cache 的实现原理主要关注它的构造方法put 方法和 get 方法 public class TestGuavaCache {Testpublic void test() {LoadingCacheString, 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 LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, 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 EquivalenceObject keyEquivalence;final EquivalenceObject valueEquivalence;final long maxWeight;final WeigherK, V weigher;final long expireAfterAccessNanos;final long expireAfterWriteNanos;final long refreshNanos;final RemovalListenerK, V removalListener;final QueueRemovalNotificationK, 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 SegmentK, 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)) {// 分段位移1segmentShift;// 分段数量为 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为4maxSegmentWeight为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 SegmentK, V[] newSegmentArray(int ssize) {return (SegmentK, V[]) new Segment?, ?[ssize];} }我们接着看下创建 Segment 的方法 LocalCache#createSegment它直接调用了 Segment 的构造方法 class LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, V {// …SegmentK, V createSegment(int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) {return new Segment(this, initialCapacity, maxSegmentWeight, statsCounter);} }Segment 是 LocalCache 内的静态内部类它在缓存中起到了将数据分段管理的作用。它继承了 ReentrantLock主要为了简化锁定的操作 static class SegmentK, V extends ReentrantLock {// … }在该类中有一段 JavaDoc 值得读一下 Segments 内部维护了缓存本身final LocalCacheK, 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 SegmentK, V extends ReentrantLock {// 在某一段Segment中元素的数量volatile int count;// 总的权重值GuardedBy(this)long totalWeight;// 修改次数int modCount;// 继上一次写操作后读操作的数量final AtomicInteger readCount new AtomicInteger();Weak final LocalCacheK, V map;final long maxSegmentWeight;final StatsCounter statsCounter;int threshold;CheckForNull volatile AtomicReferenceArrayReferenceEntryK, V table;final QueueReferenceEntryK, V recencyQueue;GuardedBy(this)final QueueReferenceEntryK, V writeQueue;GuardedBy(this)final QueueReferenceEntryK, V accessQueue;Segment(LocalCacheK, 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();}AtomicReferenceArrayReferenceEntryK, V newEntryArray(int size) {return new AtomicReferenceArray(size);}void initTable(AtomicReferenceArrayReferenceEntryK, 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 数据结构的全貌了如下所示
在接下来的两个小节中我们将深入讨论 put 和 get 方法的实现分析这些数据结构是如何为这些操作提供支持的。 put 在深入 put 方法前我们需要先了解下创建键值对元素的逻辑。在调用 LocalCache 的构造方法时其中 entryFactory 字段我们没具体讲解在这里详细描述下因为它与键值对元素的创建有关。EntryFactory 是一个枚举类它其中定义了如 STRONG_ACCESS_WRITE 和 WEAK_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 {OverrideK, V ReferenceEntryK, V newEntry(SegmentK, V segment, K key, int hash, CheckForNull ReferenceEntryK, V next) {return new StrongAccessWriteEntry(key, hash, next);}// …} }其中 EntryFactory#newEntry 为创建键值对元素的方法创建的键值对类型为 StrongAccessWriteEntry我们看下它的具体实现 class LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, V {static class StrongEntryK, V extends AbstractReferenceEntryK, V {final K key;final int hash;CheckForNull final ReferenceEntryK, V next;volatile ValueReferenceK, V valueReference unset();StrongEntry(K key, int hash, CheckForNull ReferenceEntryK, V next) {this.key key;this.hash hash;this.next next;}// …}static final class StrongAccessWriteEntryK, V extends StrongEntryK, V {volatile long accessTime Long.MAX_VALUE;volatile long writeTime Long.MAX_VALUE;Weak ReferenceEntryK, V nextAccess nullEntry();Weak ReferenceEntryK, V previousAccess nullEntry();Weak ReferenceEntryK, V nextWrite nullEntry();Weak ReferenceEntryK, V previousWrite nullEntry();StrongAccessWriteEntry(K key, int hash, CheckForNull ReferenceEntryK, V next) {super(key, hash, next);}} }StrongAccessWriteEntry 与它的父类 StrongEntry 均为定义在 LocalCache 内的静态内部类构造方法需要指定 Key, hash, next 三个变量这三个变量均定义在 StrongEntry 中注意第三个变量 ReferenceEntryK, V next它被父类中 StrongEntry#next 字段引用并且该字段被 final 修饰这与前文 JavaDoc 中所描述的内容相对应 键值对对象中的 next 字段被 final 修饰所有的添加操作都只能在每个桶的前面进行头插法这也就使得检查变更比较容易并且遍历速度也比较快。 所以实际上在添加新的键值对元素时针对每个桶中元素操作采用的是“头插法”这些元素是通过 next 指针引用的 单向链表。现在了解了元素的类型和创建逻辑我们再来看下 put 方法的实现。 Guava Cache 是不允许添加 null 键和 null 值的如果添加了 null 键或 null 值会抛出 NullPointerException 异常。注意其中的注解 GuardedBy 表示某方法或字段被调用或访问时需要持有哪个同步锁在 Caffeine 中也有类似的应用 class LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, V {// …final SegmentK, V[] segments;final int segmentShift;final int segmentMask;GuardedBy(this)long totalWeight;CheckForNullCanIgnoreReturnValueOverridepublic 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);}SegmentK, V segmentFor(int hash) {// 位移并位与运算segmentMask 为段数组长度减一保证计算结果在有效范围内return segments[(hash segmentShift) segmentMask];} }可以发现在执行 put 操作前会根据键的哈希值来将其路由segmentFor到具体的 Segment 段再调用 Segment 的 put 方法而具体的 put 方法逻辑是在 Segment 中实现的。我们接着看 Segment#put 方法的实现为了保证可读性其中标注了数字的方法会在接下来小节中具体分析 static class SegmentK, V extends ReentrantLock {final AtomicInteger readCount new AtomicInteger();GuardedBy(this)final QueueReferenceEntryK, V accessQueue;final QueueReferenceEntryK, V recencyQueue;GuardedBy(this)final QueueReferenceEntryK, V writeQueue;// guava cache 本身Weakfinal LocalCacheK, V map;// Segment 中保存元素的数组CheckForNullvolatile AtomicReferenceArrayReferenceEntryK, V table;// 计数器final StatsCounter statsCounter;// 该段中的元素数量volatile int count;// 总的权重不配置也表示元素数量每个元素的权重为 1GuardedBy(this)long totalWeight;// capacity * 0.75int threshold;// 更新次数用于确保在批量读取的情况下保证数据一致性如果多次读取时 modCount 不一致则需要重试int modCount;CanIgnoreReturnValueCheckForNullV 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并拿到头节点 firstAtomicReferenceArrayReferenceEntryK, V table this.table;int index hash (table.length() - 1);ReferenceEntryK, V first table.get(index);// 尝试遍历链表寻找被新添加的元素是否已经存在for (ReferenceEntryK, V e first; e ! null; e e.getNext()) {K entryKey e.getKey();// 如果新 put 的元素在缓存中已经存在if (e.getHash() hash entryKey ! null map.keyEquivalence.equivalent(key, entryKey)) {ValueReferenceK, 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 头节点ReferenceEntryK, 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(ReferenceEntryK, V entry, K key, V value, long now) {ValueReferenceK, V previous entry.getValueReference();// 计算元素权重如果没有执行权重计算函数默认为 1int weight map.weigher.weigh(key, value);checkState(weight 0, Weights must be non-negative);// 更新元素的 value 值ValueReferenceK, V valueReference map.valueStrength.referenceValue(this, entry, value, weight);entry.setValueReference(valueReference);// 处理写操作recordWrite(entry, weight, now);// StrongValueReference 该方法为空实现previous.notifyNewValue(value);}GuardedBy(this)void recordWrite(ReferenceEntryK, 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(ReferenceEntryK, V entry, long now) {// 配置了访问后过期时间则更新该元素的访问时间if (map.recordsAccess()) {entry.setAccessTime(now);}// 将该元素添加到访问队列中accessQueue.add(entry);} }上述源码中我们能够发现 Guava Cache 是在 Segment 的级别加锁 的通过这样的方式来减少竞争其中 preWriteCleanup 方法负责处理元素的过期evictEntries 方法负责驱逐保证缓存不超过最大的容量根据 LRU 算法将最近最少访问的元素移除expand 方法负责 Segment 的扩容setValue, recordWrite 和 recordLockedRead 方法负责处理元素的更新并记录访问情况更新 accessQueue 和 writeQueue 队列。它对元素的管理相对简单也比较容易理解接下来我们分步骤看下其中方法的实现。 preWriteCleanup 首先我们重点看下 preWriteCleanup 方法该方法负责处理元素的过期而元素过期的判断也非常简单它会在每个元素中记录它们最新的访问或写入时间将当前时间与这些时间作差后与配置的访问或写入过期时间作比较如果超过了配置的时间则表示元素过期并将它们进行驱逐。除此之外还有两个小细节需要留意队列中维护元素的变动采用的是 LRU 算法并规定元素越靠近尾部表示它越“新”另一点是 readCount 会在写后标记为 0这个变量的作用是保证在没发生写操作的情况下而读次数超过一定阈值也会执行 cleanUp 的方法这个在后文的 get 方法逻辑中还会提到。源码如下所示 static class SegmentK, V extends ReentrantLock {final AtomicInteger readCount new AtomicInteger();GuardedBy(this)final QueueReferenceEntryK, V accessQueue;final QueueReferenceEntryK, V recencyQueue;GuardedBy(this)final QueueReferenceEntryK, V writeQueue;// guava cache 本身Weak final LocalCacheK, V map;// Segment 中保存元素的数组CheckForNull volatile AtomicReferenceArrayReferenceEntryK, V table;// 计数器final StatsCounter statsCounter;// 该段中的元素数量volatile int count;// 总的权重不配置也表示元素数量每个元素的权重为 1GuardedBy(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();ReferenceEntryK, 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() {ReferenceEntryK, V e;// 遍历元素最近被访问队列 recencyQueuewhile ((e recencyQueue.poll()) ! null) {// 如果该元素在访问队列 accessQueue 中if (accessQueue.contains(e)) {// 则将其放到尾节点accessQueue.add(e);}// 源码中对这里的 IF 判断标注了如下内容来解释如此判断的原因// 尽管元素已经在缓存中删除但它仍可能在 recencyQueue 队列中。// 这种情况可能出现在开发者操作元素删除或清空段中所有元素的同时执行读操作}}VisibleForTestingGuardedBy(this)CanIgnoreReturnValueboolean removeEntry(ReferenceEntryK, V entry, int hash, RemovalCause cause) {int newCount this.count - 1;AtomicReferenceArrayReferenceEntryK, V table this.table;// 位与运算找到对应的桶获取头节点int index hash (table.length() - 1);ReferenceEntryK, V first table.get(index);for (ReferenceEntryK, V e first; e ! null; e e.getNext()) {// 找到了对应的元素则操作移除if (e entry) {modCount;// 在链表chain中移除元素注意 e 表示待移除的元素ReferenceEntryK, 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)CheckForNullReferenceEntryK, V removeValueFromChain(ReferenceEntryK, V first, ReferenceEntryK, V entry, CheckForNull K key, int hash, V value, ValueReferenceK, 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)CheckForNullReferenceEntryK, V removeEntryFromChain(ReferenceEntryK, V first, ReferenceEntryK, V entry) {// 初始化计数跟踪数量变化int newCount count;// 被移除元素的 next 元素作为新的头节点ReferenceEntryK, V newFirst entry.getNext();// 遍历从 原头节点 first 到 被移除元素 entry 之间的所有元素for (ReferenceEntryK, V e first; e ! entry; e e.getNext()) {// 创建一个新的节点该节点是节点 e 的副本并把新节点的 next 指针指向 newFirst 节点ReferenceEntryK, 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(ReferenceEntryK, 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) {RemovalNotificationK, V notification RemovalNotification.create(key, value, cause);map.removalNotificationQueue.offer(notification);}} }class LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, V {// 判断元素是否过期它的逻辑非常简单如果配置了对应的过期模式如访问后过期或写后过期// 会根据当前时间与元素的访问时间或写入时间进行比较如果超过配置的过期时间则返回 true否则返回 falseboolean isExpired(ReferenceEntryK, 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 SegmentK, V extends ReentrantLock {static final int MAXIMUM_CAPACITY 1 30;// Segment 中保存元素的数组CheckForNullvolatile AtomicReferenceArrayReferenceEntryK, V table;// 该段中的元素数量volatile int count;// 总的权重不配置也表示元素数量每个元素的权重为 1GuardedBy(this)long totalWeight;// capacity * 0.75int threshold;GuardedBy(this)void expand() {AtomicReferenceArrayReferenceEntryK, V oldTable table;int oldCapacity oldTable.length();// 如果容量已经达到最大值则直接返回if (oldCapacity MAXIMUM_CAPACITY) {return;}int newCount count;// 创建一个原来两倍容量的 AtomicReferenceArrayAtomicReferenceArrayReferenceEntryK, 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) {ReferenceEntryK, V head oldTable.get(oldIndex);if (head ! null) {// 获取头节点的 next 节点ReferenceEntryK, V next head.getNext();// 计算头节点在新数组中的索引int headIndex head.getHash() newMask;// 头节点的 next 节点为空那么证明该桶位置只有一个元素直接将头节点放在新数组的索引处 if (next null) {newTable.set(headIndex, head);} else {// 遍历从 next 开始的节点找到所有能 hash 到同一个桶的一批节点然后将这些节点封装在新数组的对应索引处ReferenceEntryK, V tail head;int tailIndex headIndex;for (ReferenceEntryK, 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 (ReferenceEntryK, V e head; e ! tail; e e.getNext()) {int newIndex e.getHash() newMask;ReferenceEntryK, V newNext newTable.get(newIndex);// 注意这里创建节点是深拷贝并且采用的是头插法ReferenceEntryK, 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 SegmentK, V extends ReentrantLock {Weak final LocalCacheK, V map;final long maxSegmentWeight;GuardedBy(this)long totalWeight;GuardedBy(this)final QueueReferenceEntryK, V accessQueue;GuardedBy(this)void evictEntries(ReferenceEntryK, 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 中从头节点遍历元素依次移除最近没有被访问的元素ReferenceEntryK, V e getNextEvictable();if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {throw new AssertionError();}}}GuardedBy(this)ReferenceEntryK, V getNextEvictable() {for (ReferenceEntryK, V e : accessQueue) {int weight e.getValueReference().getWeight();if (weight 0) {return e;}}throw new AssertionError();} }postWriteCleanup 写后清理操作需要关注的内容并不多它主要处理监听器相关的逻辑因为在我们的例子中并没有创建监听器所以就不在详细分析了。 static class SegmentK, V extends ReentrantLock {Weak final LocalCacheK, V map;void postWriteCleanup() {runUnlockedCleanup();}void runUnlockedCleanup() {// locked cleanup may generate notifications we can send unlockedif (!isHeldByCurrentThread()) {map.processPendingNotifications();}} }class LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, V {final QueueRemovalNotificationK, V removalNotificationQueue;final RemovalListenerK, V removalListener;void processPendingNotifications() {RemovalNotificationK, 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 LocalManualCacheK, V implements CacheK, V, Serializable {final LocalCacheK, V localCache;// … }static class LocalLoadingCacheK, V extends LocalManualCacheK, Vimplements LoadingCacheK, V {Overridepublic V get(K key) throws ExecutionException {return localCache.getOrLoad(key);} } class LocalCacheK, V extends AbstractMapK, V implements ConcurrentMapK, 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 SegmentK, V extends ReentrantLock {static final int DRAIN_THRESHOLD 0x3F;volatile int count;Weak final LocalCacheK, V map;final QueueReferenceEntryK, 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) {ReferenceEntryK, 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 为空ValueReferenceK, 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();}}CheckForNullReferenceEntryK, V getEntry(Object key, int hash) {// 获取到 table 数组中对应桶的头节点for (ReferenceEntryK, 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(ReferenceEntryK, 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(ReferenceEntryK, V entry, long now) {// 如果配置了访问后过期则更新元素访问时间if (map.recordsAccess()) {entry.setAccessTime(now);}// 将元素添加到最近访问队列中recencyQueue.add(entry);}V waitForLoadingValue(ReferenceEntryK, V e, K key, ValueReferenceK, 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 lockedGetOrLoad 是 LoadingCache 的核心逻辑在某个 key 在缓存中不存在且配置了 loader 函数时会被执行本质上这也是触发了一次写操作将不存在的元素通过 loader 方法获取到具体值并加载到缓存中所以在这里也会触发到元素生命周期管理的方法如 preWriteCleanup不过该方法总体上的实现与 put 方法类似并没有特别复杂的地方。 static class SegmentK, V extends ReentrantLock {volatile int count;Weak final LocalCacheK, V map;CheckForNull volatile AtomicReferenceArrayReferenceEntryK, V table;GuardedBy(this)final QueueReferenceEntryK, V writeQueue;GuardedBy(this)final QueueReferenceEntryK, V accessQueue;final StatsCounter statsCounter;int modCount;// 加锁读或加载元素值V lockedGetOrLoad(K key, int hash, CacheLoader? super K, V loader) throws ExecutionException {ReferenceEntryK, V e;ValueReferenceK, V valueReference null;LoadingValueReferenceK, V loadingValueReference null;boolean createNewEntry true;// 加锁lock();try {long now map.ticker.read();// 写前整理操作已经在上文中介绍过preWriteCleanup(now);int newCount this.count - 1;// 找到该元素对应桶的头节点AtomicReferenceArrayReferenceEntryK, V table this.table;int index hash (table.length() - 1);ReferenceEntryK, 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, LoadingValueReferenceK, V loadingValueReference, CacheLoader? super K, V loader) throws ExecutionException {ListenableFutureV loadingFuture loadingValueReference.loadFuture(key, loader);return getAndRecordStats(key, hash, loadingValueReference, loadingFuture);}CanIgnoreReturnValueV getAndRecordStats(K key, int hash, LoadingValueReferenceK, V loadingValueReference, ListenableFutureV 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, LoadingValueReferenceK, 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;}// 找到该元素对应的桶的头节点AtomicReferenceArrayReferenceEntryK, V table this.table;int index hash (table.length() - 1);ReferenceEntryK, V first table.get(index);for (ReferenceEntryK, V e first; e ! null; e e.getNext()) {K entryKey e.getKey();if (e.getHash() hash entryKey ! null map.keyEquivalence.equivalent(key, entryKey)) {ValueReferenceK, 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 元素的操作一致ReferenceEntryK, 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 也并不是一无是处在对性能要求不高的场景下且不想引入额外的依赖CaffeineGuava Cache 也是一个不错的选择因为 Guava 作为常用的 Java 库通常都会在依赖中存在。 巨人的肩膀 Github - Guava