目录
建议对Guava Cache完全不了解的同学,先看一下Guava Cache核心参数及其原理的讲解,先了解一些关键的节点,再看下面的原理分析,可能更轻松一些:Guava Cache:核心参数深度剖析和相关源码分析
一、基础信息
1、版本
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
2、构造方式
3、核心参数
核心参数详解参考:Guava Cache:核心参数深度剖析和相关源码分析
1、容量
- initialCapacity:初始容量;
- maximumSize:最大容量;
- maximumWeight:最大权重,每条缓存的默认权重为1,可以增加单挑缓存的权重;
- weither:权重器,用于衡量不同缓存条目的权重。
2、超时时长
- expireAfterAccess:超时计算方式:在访问请求之后重置超时计时器;
- expireAfterWrite:超时计算方式:在写请求之后重置超时计时器。
3、刷新
- refreshAfterWrite:写操作后多久刷新缓存内容,刷新使用下面的加载器;
- build(CacheLoader loader):构造LoadingCache实例,入参是用于刷新的加载器;
- removalListener:移除监听器,接收条目被移除的通知,可以过滤需要的缓存条目,进行相应处理。
4、引用强度
- weakValues:指定所有的value都是弱引用;
- weakKeys:指定所有的key都是弱引用;
- softValues:指定所有的key都是软引用。
5、其他
- concurrencyLevel:并发级别,级别越高支持的最大并发数越大;
- recordStats:启用缓存统计,即缓存操作期间的性能相关的统计;
- ticker:指定纳秒精度的时间源,默认使用System.nanoTime()。
二、基本原理
1、数据结构
Guava Cache的数据结构,和JDK 1.7版本的ConcurrentHashMap非常相似:
- 分段segment:最外层是分段segment,用于控制最大的写并发数量;
- 分段内的数组table:每个分段内维护一个原子引用数组table,根据元素的hash值确定在数组中的位置;
- 数组内的链表:数组的任一元素,存放的都是一个链表,用于解决哈希碰撞的情况;
- 和JDK 1.7的ConcurrentHashMap的一个重要区别在于,Guava Cache的数组中始终存放的都是链表,不会变成红黑树。
1.1 分段segment的数量
分段segment是缓存工具Cache的最外层结构。一个缓存可能会有多个segment,所有segment的内容之和,表示整个缓存。
segmentCount的值主要取决于建造者类CacheBuilder的参数并发级别concurrencyLevel,另外还会受到CacheBuilder参数最大加权值maximumWeight。
详情参考: Guava Cache:核心参数深度剖析和相关源码分析 # 并发级别concurrencyLevel
分段数量segmentCount的取值规则简述:
- segmentCount是2的整数倍;
- segmentCount在允许的取值范围内取最大值;
- concurrencyLevel的约束:1/2 * segmentCount满足:小于concurrencyLevel ;
- maxWeight的约束:如果maxWeight < 0(不限制缓存最大容量),则对segmentCount无影响;如果设置了有效的maxWeight,则 1/2 * segmentCount 小于等于1/20 * maxWeight。
分段数量segmentCount的计算代码(取自LocalCache.LocalLoadingCache的构造器):
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
// 这时的segmentShift还是表示segmentCount是2的多少次幂
++segmentShift;
// segmentCount是满足while条件的最大值的2倍
segmentCount <<= 1;
}
// 最终的segmentShift用于取hash的高位的相应位数,用来计算寻找一个元素在哪个segment中
this.segmentShift = 32 - segmentShift;
// 掩码,取hash的低位的相应位数的值,即为在segment中的角标
segmentMask = segmentCount - 1;
// 是否限制容量
boolean evictsBySize() {
return maxWeight >= 0;
}
1.2 分段segment的定位
如何找到一条缓存唯一对应的segment,是缓存使用过程中首先要考虑的。无论是缓存更新还是缓存查询,必须首先先根据缓存的key,匹配到一个segment,才能进行后续的操作。
1、相关成员:
Guava Cache是通过高位哈希的原理给缓存分配segment的。通过1.1分段segment的数量 可知,缓存工具Cache中会维护一些跟segment相关的参数,包括:
- segmentCount:segment的数量;
- segmentShift:计算segment使用的位数。指的是段的数量是1通过左移多少位计算得到的,再让32减去这个数字。例如segmentCount = 4的情况,segmentShift就是 32 - 2 = 30,表示计算segment的时候,使用hash值的30~32位;
- segmentMask:计算segment使用的掩码。segmentMask所有位的值都是1,它位数等于32 - segmentShift的值。例如segmentCount = 4的情况,segmentMask就是111。
2、分段segment的定位方式:
- Cache中维护着一个segment的数组segments,数组的大小是segmentCount;
- Cache计算缓存key的hash值;
- Cache让hash右移segmentShift位,得到hash从第segmentShift位开始的高位的值(例如当segmentCount = 4时,segmentShift=30,此时会右移30位,得到从30~32位的值);
- Cache将得到的hash高位的值,和掩码segmentMask相与(例如当segmentCount = 4时,segmentMask = 111,此时相与得到的结果就是hash的30~32位的值本身);
- 上一步得到的相与结果,就是Cache中维护的segment数组segments中的角标。
注意事项:
- 之所以有第四步和segmentMask相与的操作,是为了保证计算得到的segments的角标不会数组越界,因为segmentMask实际上就是segments中最大的角标;
- 这里计算segment,之所以使用hash的高位,是因为在segment中的数组中定位元素,使用的hash的低位。两者分开,可以让缓存在segments中的分布和缓存在segment的table中的分布互相正交,减少哈希碰撞导致的性能较差的情况。
3、代码:
/**
* Returns the segment that should be used for a key with the given hash.
*
* @param hash the hash code for the key
* @return the segment
*/
Segment<K, V> segmentFor(int hash) {
// TODO(fry): Lazily create segments?
return segments[(hash >>> segmentShift) & segmentMask];
}
1.3 原子数组table
segment中维护了一个原子引用数组AtomicReferenceArray的实例table。table类似于一个数组,数组中的不同元素的hash值是不同的。
segment通过hash的低位,定位缓存在table中的位置。用key的hash值,和table的长度-1,进行与运算,得到的数值就是key在数组table中的位置。
/** Returns first entry of bin for given hash. */
ReferenceEntry<K, V> getFirst(int hash) {
// read this volatile field only once
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
return table.get(hash & (table.length() - 1));
}
1.4 链表节点ReferenceEntry
table中维护的是一个链表,链表的每个节点的类型,是ReferenceEntry。
ReferenceEntry是个接口,有多个实现类。Guava Cache会构造时设置的引用强度,选择相应的实现类:
2、并发控制
并发控制是通过参数并发级别concurrencyLevel设置的,还会受参数最大加权值maximumWeight的影响,最终通过分段segment的数量来起作用。分段数量的计算规则参考上述1.1分段segment的数量。
缓存的写操作(包括显式的写操作,以及读操作触发的缓存失效、缓存加载等)是需要加锁的,而加锁的基本单元是segment。类Segment是Java的重入锁ReentrantLock的子类。在写操作前后会分别调用ReentrantLock的lock()和unlock()方法,进行加锁和解锁操作。
3、缓存淘汰
Guava Cache定义了枚举类RemovalCause,用来标识缓存被移除的原因。这里通过分析RemovalCause的枚举值,来分析不同的缓存淘汰原因。
枚举类RemovalCause要求枚举值分别实现wasEvicted()方法,表示缓存是否是被淘汰的,而不是被用户显式清除的。
/**
* Returns {@code true} if there was an automatic removal due to eviction (the cause is neither
* {@link #EXPLICIT} nor {@link #REPLACED}).
*/
abstract boolean wasEvicted();
3.1 用户显式清除的两种场景
如果用户通过invalite或invalidateAll等方法主动失效了相应缓存,或者通过put方法使用新值替换了旧的缓存值,这两种情况表示缓存是被用户显式清除掉的,而不是被缓存内部清除的。
枚举值:
/**
* The entry was manually removed by the user. This can result from the user invoking {@link
* Cache#invalidate}, {@link Cache#invalidateAll(Iterable)}, {@link Cache#invalidateAll()}, {@link
* Map#remove}, {@link ConcurrentMap#remove}, or {@link Iterator#remove}.
*/
// 用户主动失效掉了缓存
EXPLICIT {
@Override
boolean wasEvicted() {
return false;
}
},
/**
* The entry itself was not actually removed, but its value was replaced by the user. This can
* result from the user invoking {@link Cache#put}, {@link LoadingCache#refresh}, {@link Map#put},
* {@link Map#putAll}, {@link ConcurrentMap#replace(Object, Object)}, or {@link
* ConcurrentMap#replace(Object, Object, Object)}.
*/
// 用户主动使用新值替换了缓存
REPLACED {
@Override
boolean wasEvicted() {
return false;
}
},
3.2 被垃圾回收淘汰
Guava Cache支持将配置的引用强度设置成软引用和弱引用,以避免缓存过多导致内存溢出等问题。
相关的建造者类的方法包括:softValues()、weakValues()、weakKeys()。
详情参考:Guava Cache:核心参数深度剖析和相关源码分析 # 软引用和弱引用
枚举值:
/**
* The entry was removed automatically because its key or value was garbage-collected. This can
* occur when using {@link CacheBuilder#weakKeys}, {@link CacheBuilder#weakValues}, or {@link
* CacheBuilder#softValues}.
*/
COLLECTED {
@Override
boolean wasEvicted() {
return true;
}
},
3.3 超时淘汰
Guava Cache支持两种超时机制:访问后超时 和写后超时。
- 访问后超时会设置一个访问时间,每次读取缓存内容或者设置缓存的值,都会刷新访问的时间;如果下一次访问的时候,发现访问时长超时,会直接让缓存失效。访问超时通过方法expireAfterAccess进行设置;
- 写后超时会设置一个写时间,每次设置缓存的值,都会刷新写时间;如果下一次访问的时候,发现访问时长超时,会直接让缓存失效。访问超时通过方法expireAfterWrite进行设置。
详情参考:Guava Cache:核心参数深度剖析和相关源码分析 # 超时
枚举值:
/**
* The entry's expiration timestamp has passed. This can occur when using {@link
* CacheBuilder#expireAfterWrite} or {@link CacheBuilder#expireAfterAccess}.
*/
EXPIRED {
@Override
boolean wasEvicted() {
return true;
}
},
3.4 容量超限淘汰(LRU算法)
如果缓存设置了最大容量(maximumSize,或者maximumWeight),则在添加缓存的时候,会去判断当前容量是否已经超限。如果缓存容量超限,则会通过LRU算法,淘汰掉最久没有访问的缓存。
枚举值:
/**
* The entry was evicted due to size constraints. This can occur when using {@link
* CacheBuilder#maximumSize} or {@link CacheBuilder#maximumWeight}.
*/
SIZE {
@Override
boolean wasEvicted() {
return true;
}
};
三、核心源码:缓存淘汰
1、基本原理
缓存淘汰主要包括三方面:
垃圾回收回收软引用、弱引用的缓存:这是通过JVM进行的,基本不需要程序主动进行回收,所以这里不进行讨论;- 缓存超时被淘汰:主要分为访问时间超时expireAfterAccess和写时间超时expireAfterWrite两种;
- 容量超限淘汰:这种是通过LRU算法,淘汰掉最久没有访问/写入最晚的缓存。
在容量超限时,Guava Cache通过LRU算法进行缓存淘汰。
GuavaCache并没有独立的线程来管理缓存,以避免和应用程序发生资源的争夺。主要靠在写操作时来做一部分清理工作,如果清理写操作太少,也可能在读操作中触发清理操作清理部分缓存。
另外,用户可以自己决定如何清理缓存。如果写操作非常少,又需要有更快的读取速率,或者是又想长期维持较大的内存开销,可以自定义维护线程,定期清理失效缓存。
2、相关实现
LocalCache的Segment中实现了一些队列,用来协助完成缓存的淘汰:
- final Queue<ReferenceEntry<K, V>> recencyQueue:记录节点被访问的顺序,会在写操作执行或者DRAIN_THRESHOLD被触发的时候全部出队;
- final @Nullable ReferenceQueue<V> valueReferenceQueue:值引用队列,记录被垃圾回收、且需要被内部清理的值;
- final @Nullable ReferenceQueue<V> keyReferenceQueue:值键引用队列,记录被垃圾回收、且需要被内部清理的节点;
- @GuardedBy("this") final Queue<ReferenceEntry<K, V>> writeQueue:缓存中的全部节点,按写顺序排序,最近加入的元素会被加到队列的尾部;
- @GuardedBy("this") final Queue<ReferenceEntry<K, V>> accessQueue:缓存中的全部节点,按访问顺序排序,最近访问的元素会被加到队列的尾部,写操作也是访问的一种。
3、写方法缓存回收
主要步骤:
- 执行写入操作;
- 检查是否totalWeight > maxSegmentWeight,如果是,则执行移除操作
- 从当前segment的第一个节点开始删除,循环进行直到totalWeight <= maxSegmentWeight
循环逻辑:
while (totalWeight > maxSegmentWeight) {
// 获取下一个可回收的节点
ReferenceEntry<K, V> e = getNextEvictable();
if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
删除逻辑:
@VisibleForTesting
@GuardedBy("this")
boolean 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;
// 删除头节点,并更新头节点的指向
ReferenceEntry<K, V> newFirst =
removeValueFromChain(
first,
e,
e.getKey(),
hash,
e.getValueReference().get(),
e.getValueReference(),
cause);
newCount = this.count - 1;
table.set(index, newFirst);
this.count = newCount; // write-volatile
return true;
}
}
return false;
}
具体的删除逻辑:
@GuardedBy("this")
@Nullable
ReferenceEntry<K, V> removeValueFromChain(
ReferenceEntry<K, V> first,
ReferenceEntry<K, V> entry,
@Nullable 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);
}
}
4、访问后缓存回收
核心过程:
- 读操作完成后进行清理工作
- 如果读操作的累计次数readCount达到了DRAIN_THRESHOLD指示的次数,即0x3F次,则执行清理(这是为了避免长期没有写操作,导致缓存长时间没有被清理);
- 清理逻辑:根据选择的超时方式(expireAfterAccess或expireAfterWrite),分别从writeQueue或accessQueue删除元素。
注意事项:
- 读操作的累计次数readCount,会在每次清空缓存、写操作、读之后的清理工作进行后,被清零。
源码:
void cleanUp() {
long now = map.ticker.read();
runLockedCleanup(now);
runUnlockedCleanup();
}
void runLockedCleanup(long now) {
if (tryLock()) {
try {
// 清空keyReferenceQueue和valueReferenceQueue
drainReferenceQueues();
// 清理超时节点
expireEntries(now); // calls drainRecencyQueue
readCount.set(0);
} finally {
unlock();
}
}
}
@GuardedBy("this")
void expireEntries(long now) {
drainRecencyQueue();
ReferenceEntry<K, V> e;
// 判断节点是否超时,和设置的超时方式有关expireAfterWrite或expireAfterAccess
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();
}
}
}
// 如果没有持有锁,可以给缓存移除监听器发送通知消息
void runUnlockedCleanup() {
// locked cleanup may generate notifications we can send unlocked
if (!isHeldByCurrentThread()) {
map.processPendingNotifications();
}
}
}
5、缓存刷新
1、基本原理:
Guava Cache提供了一种缓存刷新的机制。在缓存超时前,可以设置一个刷新时间,在缓存写入后多久进行刷新。刷新时间在构造缓存时使用refreshAfterWrite方法进行设置。
缓存刷新的注意事项:
- 缓存刷新是异步实现的,但是第一条触发缓存刷新的线程,会阻塞等待异步任务完成;
- 如果缓存刷新任务获取新的缓存失败,则触发缓存刷新的线程,会返回缓存中现有的旧值;
- 第一条请求之后的其他线程,如果发现该缓存正在被刷新,它不会阻塞等待刷新任务的完成,而是会直接返回缓存中现有的旧值。
2、核心代码:
1)第一条线程发现超过refreshNanos,执行刷新;后面的线程访问到正在刷新的缓存时,直接返回旧值:
V scheduleRefresh(
ReferenceEntry<K, V> entry,
K key,
int hash,
V oldValue,
long now,
CacheLoader<? super K, V> loader) {
// 判断: 是否需要刷新 && 并不是正在刷新
if (map.refreshes()
&& (now - entry.getWriteTime() > map.refreshNanos)
&& !entry.getValueReference().isLoading()) {
V newValue = refresh(key, hash, loader, true);
if (newValue != null) {
return newValue;
}
}
// 不需要刷新,或者有其他线程这个字刷新,就返回现在的旧值
return oldValue;
2)第一条触发refresh逻辑的线程,阻塞等待异步执行结果:
@Nullable
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
// 插入一个LoadingValueReference,这样后面的线程访问到的时候,可以知道这个缓存正在被刷新
final LoadingValueReference<K, V> loadingValueReference =
insertLoadingValueReference(key, hash, checkTime);
if (loadingValueReference == null) {
return null;
}
// 异步执行,生成future占位符
ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
// 服务线程阻塞等待异步任务执行完成
if (result.isDone()) {
try {
return Uninterruptibles.getUninterruptibly(result);
} catch (Throwable t) {
// don't let refresh exceptions propagate; error was already logged
}
}
return null;
3)异步刷新缓存的具体逻辑:
ListenableFuture<V> loadAsync(
final K key,
final int hash,
final LoadingValueReference<K, V> loadingValueReference,
CacheLoader<? super K, V> loader) {
final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
loadingFuture.addListener(
new Runnable() {
@Override
public void run() {
try {
// 异步任务执行完成时,回调getAndRecordStats方法去设置缓存的值,并记录统计结果
getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
} catch (Throwable t) {
logger.log(Level.WARNING, "Exception thrown during refresh", t);
loadingValueReference.setException(t);
}
}
},
directExecutor());
return loadingFuture;
四、核心源码:主要流程
1、构造缓存
由于Cache有多种实现类,以及多个复杂的参数,所以Cache的实例是使用建造者模式,通过CacheBuilder进行构造的。CacheBuilder中会对各个参数的内容、使用场景等进行组合和校验,并最终调用相应Cache接口的实现类构造实例。
CacheBuilder的方法详解参考:Guava Cache:核心参数深度剖析和相关源码分析
1.1 Cache的实现类
Cache有多个子接口和实现类:
这些实现类分为两种:Cache接口的直接实现类,以及Cache的子接口LoadingCache的实现类。LoadingCache在继承Cache接口的接触上,还要求实现类可以实现缓存的自动刷新。对用户来说,两个最直接的感知:
- 构造Cache的直接实现类时,使用CacheBuilder的build()无参方法;而构造LoadingCache的实现类,需要使用CacheBuilder的build(CacheLoader loader)方法指定默认的缓存加载器;
- 从Cache的直接实现类查询缓存时,使用get(K key, Callable<? extends V> loader),loader用来在缓存失效或不存在时加载缓存;从LoadingCache的实现类获取缓存时,可以使用get(K key)方法,因为LoadingCache的实现类有默认的loader。
当然,LoadingCache还提供了其他一些方法,包括:
- V getUnchecked(K key):get方法会抛ExecutionException,而getUnchecked会将ExecutionException转换成UncheckedExecutionException并抛出;
- ImmuableMap<K, V> getAll(Iterale<? extends K> keys):获取一批缓存;
- V apply(K key):从Function中继承来的接口,内部是通过get方法或者getUnchecked方法实现的;
- void refresh(K key):刷新key对应的缓存值,可能同步也可能异步。LocalLoadingCache的实现,是异步刷新,但是会同步等待异步结果。
1.2 LoadingCache的构造
一般来说,推荐使用的是LoadingCache子接口的实现类,好处是在get方法获取缓存的时候,不需要再次指定CacheLoader,
LoadingCache最重要的一个实现类,就是LocalLoadingCache。使用CacheBuilder的build(CacheLoader loader)方法,构造出来的就是LocalLoadingCache的实例。
LoadingCache的详细构造方法,参考:Guava Cache:核心参数深度剖析和相关源码分析
源码分析:
1)CacheBuilder.build(CacheLoader loader)创建LocalLoadingCache的实例:
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) {
checkWeightWithWeigher();
return new LocalCache.LocalLoadingCache<>(this, loader);
}
2)LocalLoadingCache是LocalManuelCache的子类,最终调用了LocalManuelCache的构造器:
// LocalLoadingCache的构造器,这是CacheBuilder中直接调用的
LocalLoadingCache(
CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
// 可以看出,Cache的实例实际上是通过LocalCache的实例,对缓存的内部逻辑进行管理的
super(new LocalCache<K, V>(builder, checkNotNull(loader)));
}
// 这是父类LocalManualCache的构造器,上面LocalLoadingCache的构造器中super的调用目标
private LocalManualCache(LocalCache<K, V> localCache) {
this.localCache = localCache;
}
从上面的源码可以看出,Cache至少一个对外提供接口的封装,实际上缓存内部的各种控制逻辑,都是通过LocalCache来实现的。
1.3 LocalCache的构造
LocalCache和Cache没有任何继承方面的关系,LocalCache一般用作Cache实现类的成员变量。Cache是对外提供相关缓存功能的客户端,而客户端内部的逻辑控制,是通过LocalCache来完成的。
LocalCache实际上是一个哈希结构:
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>
LocalCache构造器中的主要逻辑,主要是将CacheBuilder的参数,转换成自身的参数。
一般参数的转化逻辑比较简单,就是“取值+边界校验”,这里不再详细解析,对特定某个参数感兴趣可以参考另一篇文章:Guava Cache:核心参数深度剖析和相关源码分析。
构造器中还有些较为复杂的逻辑,包括计算段数量的逻辑,以及计算段的最大权重值的逻辑。这里只展示源码和注释,逻辑概述可以参考上面一段中的文章。
1、计算段数量:
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
// 这时的segmentShift还是表示segmentCount是2的多少次幂
++segmentShift;
// segmentCount是满足while条件的最大值的2倍
segmentCount <<= 1;
}
// 最终的segmentShift用于取hash的高位的相应位数,用来计算寻找一个元素在哪个segment中
this.segmentShift = 32 - segmentShift;
// 掩码,取hash的低位的相应位数的值,即为在segment中的角标
segmentMask = segmentCount - 1;
// 是否限制容量
boolean evictsBySize() {
return maxWeight >= 0;
}
2、计算段的最大权重值
// 构造器中maxWeight对段生效的代码
// segmentCapacity = initialCapacity 除以 segmentCount 向上取整
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
}
// segmentSize = 不小于segmentCapacity的 最小的 2的整数幂
// segmentSize用作段的初始容量
int segmentSize = 1;
while (segmentSize < segmentCapacity) {
segmentSize <<= 1;
}
// 是否限制容量
if (evictsBySize()) {
// Ensure sum of segment max weights = overall max weights
// 段容量基础值 = 总容量 除以 段数 向上取整
long maxSegmentWeight = maxWeight / segmentCount + 1;
long remainder = maxWeight % segmentCount;
for (int i = 0; i < this.segments.length; ++i) {
// 从第余数段开始,段容量减1,以保证各段容量之和等于总容量
if (i == remainder) {
maxSegmentWeight--;
}
this.segments[i] =
createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
}
} else {
// 如果未设置总的最大容量,则每个分段都不设置最大容量
for (int i = 0; i < this.segments.length; ++i) {
this.segments[i] =
createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
}
}
// 是否会根据容量进行淘汰
boolean evictsBySize() {
return maxWeight >= 0;
}
3、构造器完整源码:
/**
* Creates a new, empty map with the specified strategy, initial capacity and concurrency level.
*/
LocalCache(
CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);
keyStrength = builder.getKeyStrength();
valueStrength = builder.getValueStrength();
keyEquivalence = builder.getKeyEquivalence();
valueEquivalence = builder.getValueEquivalence();
maxWeight = builder.getMaximumWeight();
weigher = builder.getWeigher();
expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
refreshNanos = builder.getRefreshNanos();
removalListener = builder.getRemovalListener();
removalNotificationQueue =
(removalListener == NullListener.INSTANCE)
? LocalCache.<RemovalNotification<K, V>>discardingQueue()
: new ConcurrentLinkedQueue<RemovalNotification<K, V>>();
ticker = builder.getTicker(recordsTime());
entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
globalStatsCounter = builder.getStatsCounterSupplier().get();
defaultLoader = loader;
int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
if (evictsBySize() && !customWeigher()) {
initialCapacity = (int) Math.min(initialCapacity, maxWeight);
}
// Find the lowest power-of-two segmentCount that exceeds concurrencyLevel, unless
// maximumSize/Weight is specified in which case ensure that each segment gets at least 10
// entries. The special casing for size-based eviction is only necessary because that eviction
// happens per segment instead of globally, so too many segments compared to the maximum size
// will result in random eviction behavior.
int segmentShift = 0;
int segmentCount = 1;
while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
++segmentShift;
segmentCount <<= 1;
}
this.segmentShift = 32 - segmentShift;
segmentMask = segmentCount - 1;
this.segments = newSegmentArray(segmentCount);
int segmentCapacity = initialCapacity / segmentCount;
if (segmentCapacity * segmentCount < initialCapacity) {
++segmentCapacity;
}
int segmentSize = 1;
while (segmentSize < segmentCapacity) {
segmentSize <<= 1;
}
if (evictsBySize()) {
// Ensure sum of segment max weights = overall max weights
long maxSegmentWeight = maxWeight / segmentCount + 1;
long remainder = maxWeight % segmentCount;
for (int i = 0; i < this.segments.length; ++i) {
if (i == remainder) {
maxSegmentWeight--;
}
this.segments[i] =
createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
}
} else {
for (int i = 0; i < this.segments.length; ++i) {
this.segments[i] =
createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
}
}
}
2、查询缓存
2.1 主要方式
Cache通过get方法查询缓存:
- get(K key):
- get(K key, Callable loader):
2.2 基本流程
这里分析LocalLoadingCache的get(K key)的逻辑。两方法的底层原理其实是一样的,get(K key)可以看做是调用了get(key, defaultLoader)。defaultLoader是在构造实例时通过build方法传入的。
1、调用成员变量localCache的getOrLoad方法
@Override
public V get(K key) throws ExecutionException {
return localCache.getOrLoad(key);
}
2、localCache的getOrLoad方法去调用它的重载方法。
V getOrLoad(K key) throws ExecutionException {
return get(key, defaultLoader);
}
这也是Cache的get(K key, Callable loader)实际上调用的方法,使用Callable loader构造一个CacheLoader的实例,再调用localCache的get方法:
@Override
public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException {
checkNotNull(valueLoader);
return localCache.get(
key,
new CacheLoader<Object, V>() {
@Override
public V load(Object key) throws Exception {
return valueLoader.call();
}
});
}
3、localCache的get方法:
localCache的get方法,主要作用是:
- 检查key是否有效(不是null);
- 根据key计算hash;
- 根据hash的高位使用hash算法寻找相应的segment;
- 调用segment的get方法,查询或者加载缓存。
源码:
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
int hash = hash(checkNotNull(key));
return segmentFor(hash).get(key, hash, loader);
}
segmentFor方法用来根据hash的高位从segments数组中取出相应的segment实例。详细解析参考上面第二章1.2的分析。
/**
* Returns the segment that should be used for a key with the given hash.
*
* @param hash the hash code for the key
* @return the segment
*/
Segment<K, V> segmentFor(int hash) {
// TODO(fry): Lazily create segments?
return segments[(hash >>> segmentShift) & segmentMask];
}
4、segment的get方法:
segment的get方法获取缓存的真正核心逻辑。它的主要流程包括:
1)预操作:检查参数key和loader是否有效(不是null);
2)主流程:
- 判断segment是否为空,如果为空则去加载缓存;
- 根据hash和key获取键值对,如果键值对为空,则去加载缓存;
- 根据取到的键值对,获取存活的value,如果value不存在、或者已经被清理掉、或者已经超时,则去加载或者等待加载结果;
- 此时说明存在有效的缓存,统计缓存的访问,并返回结果或者刷新再返回结果。
对于最后异步,如果未达到refreshAfterWrite设置的超时时间,则直接返回结果;如果达到了,则第一个线程会阻塞等待后台异步刷新,后面的线程不会等待,而是直接返回现有结果。
3)后操作:特定情况下清理超时节点。如果在上一次写操作或者清理操作以后,已经经历过多次(readCount == 0x3F,即63次)读操作而未清理超时节点,就会执行清理工作,清理超时节点。清理完成后,会重置上述次数readCount。
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
checkNotNull(key);
checkNotNull(loader);
try {
// count指的是当前segment中元素的数量。count == 0 表示当前segment为空
if (count != 0) { // read-volatile
// don't call getLiveEntry, which would ignore loading values
// getEntry会校验key,所以key为弱引用被回收的场景,取到的e是null
ReferenceEntry<K, V> e = getEntry(key, hash);
if (e != null) {
// ticker.read()计算纳秒时间戳,用于判断是否超时、是否需要刷新
long now = map.ticker.read();
// getLiveValue取到的是存活的有效的value:
// 1)如果超时会取到null;
// 2)如果value是软引用或者弱引用,被GC回收了,也会取到null;
// 3)如果value没有超时且没有被回收,但是超过了refreshAfterWrite设置的时间,也会取到有效值。
V value = getLiveValue(e, now);
if (value != null) {
// 记录该缓存被访问了。此时expireAfterAccess相关的时间会被刷新
recordRead(e, now);
// 记录缓存击中
statsCounter.recordHits(1);
// 用来判断是直接返回现有value,还是等待刷新
return scheduleRefresh(e, key, hash, value, now, loader);
}
ValueReference<K, V> valueReference = e.getValueReference();
// 只有key存在,但是value不存在(被回收)、或缓存超时的情况会到达这里
// 如果已经有线程在加载缓存了,后面的线程不会重复加载,而是等待加载的结果
if (valueReference.isLoading()) {
return waitForLoadingValue(e, key, valueReference);
}
}
}
// at this point e is either null or expired;
// 走到这里的场景:
// 1)segment为空
// 2)key或value不存在(没有缓存,或者弱引用、软引用被回收),
// 3)缓存超时(expireAfterAccess或expireAfterWrite触发的)
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 {
// 读后的清理工作。如果在上一次写操作或者清理操作以后,已经有多次(0x3F次)读操作,就会执行清理工作,清理超时节点
postReadCleanup();
}
}
2.3 步骤详解:查询和刷新
1、根据hash和key获取键值对:getEntry
@Nullable
ReferenceEntry<K, V> getEntry(Object key, int hash) {
// getFirst用来根据hash获取table中相应位置的链表的头元素
for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {
// hash不相等的,key肯定不相等。hash判等是int判等,比直接用key判等要快得多
if (e.getHash() != hash) {
continue;
}
K entryKey = e.getKey();
// entryKey == null的情况,是key为软引用或者弱引用,已经被GC回收了。直接清理掉
if (entryKey == null) {
tryDrainReferenceQueues();
continue;
}
if (map.keyEquivalence.equivalent(key, entryKey)) {
return e;
}
}
return null;
}
其中getFirst的代码:
// 获取table中hash相应位置的链表的头元素
/** Returns first entry of bin for given hash. */
ReferenceEntry<K, V> getFirst(int hash) {
// read this volatile field only once
AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
return table.get(hash & (table.length() - 1));
}
2、清理被回收的缓存:tryDrainReferenceQueues
这个操作只会在查询缓存结果、判断缓存是否存在的场景才会触发。
/** Cleanup collected entries when the lock is available. */
void tryDrainReferenceQueues() {
// tryLock是继承自ReentrantLock的方法。尝试获取锁,成功获取到锁则返回true
if (tryLock()) {
try {
// 清理存储非强引用的缓存的队列。真正的清理被回收配置的逻辑
drainReferenceQueues();
} finally {
unlock();
}
}
}
drainReferenceQueues用来清空key的非强引用的队列和value的非强引用的队列。队列里存储的是ReferenceEntry。
/**
* Drain the key and value reference queues, cleaning up internal entries containing garbage
* collected keys or values.
*/
@GuardedBy("this")
void drainReferenceQueues() {
if (map.usesKeyReferences()) {
drainKeyReferenceQueue();
}
if (map.usesValueReferences()) {
drainValueReferenceQueue();
}
}
清理key的非强引用队列的具体逻辑:
@GuardedBy("this")
void drainKeyReferenceQueue() {
Reference<? extends K> ref;
int i = 0;
while ((ref = keyReferenceQueue.poll()) != null) {
@SuppressWarnings("unchecked")
ReferenceEntry<K, V> entry = (ReferenceEntry<K, V>) ref;
// reclaimKey方法用来清理一个key被垃圾回收的缓存的键值对ReferenceEntry
map.reclaimKey(entry);
if (++i == DRAIN_MAX) {
break;
}
}
}
清理value的非强引用队列的具体逻辑:
@GuardedBy("this")
void drainValueReferenceQueue() {
Reference<? extends V> ref;
int i = 0;
while ((ref = valueReferenceQueue.poll()) != null) {
@SuppressWarnings("unchecked")
ValueReference<K, V> valueReference = (ValueReference<K, V>) ref;
map.reclaimValue(valueReference);
if (++i == DRAIN_MAX) {
break;
}
}
}
3、获取存活的value: getLiveValue
/**
* Gets the value from an entry. Returns null if the entry is invalid, partially-collected,
* loading, or expired.
*/
V getLiveValue(ReferenceEntry<K, V> entry, long now) {
// 软引用或者弱引用的key被清理掉了
if (entry.getKey() == null) {
// 清理非强引用的队列
tryDrainReferenceQueues();
return null;
}
// 软引用或者弱引用的value被清理掉了
V value = entry.getValueReference().get();
if (value == null) {
// 清理非强引用的队列
tryDrainReferenceQueues();
return null;
}
if (map.isExpired(entry, now)) {
// 如果获取锁成功,就清理超时的缓存
tryExpireEntries(now);
return null;
}
return value;
}
4、获取结果或刷新缓存:scheduleRefresh
scheduleRefresh的主要逻辑:
- 如果没有开启刷新功能,或者没有达到刷新的时间,则直接返回现有的value;
- 如果达到了刷新时间,并且value没有在刷新,则调用refresh,刷新value并阻塞获取刷新结果;
- 如果刷新失败,则返回现有的value的值;
- 如果达到了刷新时间,但是value已经在刷新,则直接返回现有的value。
V scheduleRefresh(
ReferenceEntry<K, V> entry,
K key,
int hash,
V oldValue,
long now,
CacheLoader<? super K, V> loader) {
// refreshes判断是否开启了自动刷新,即是否配置了refreshAfterWrite
if (map.refreshes()
// 达到了刷新时间
&& (now - entry.getWriteTime() > map.refreshNanos)
// 已经有线程触发了该条缓存的刷新
&& !entry.getValueReference().isLoading()) {
// 调用refresh刷新缓存
V newValue = refresh(key, hash, loader, true);
// 如果刷新失败,即newValue == null,则返回现有值
if (newValue != null) {
return newValue;
}
}
return oldValue;
}
刷新缓存的逻辑refresh:
/**
* Refreshes the value associated with {@code key}, unless another thread is already doing so.
* Returns the newly refreshed value associated with {@code key} if it was refreshed inline, or
* {@code null} if another thread is performing the refresh or if an error occurs during
* refresh.
*/
@Nullable
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
// 用LoadingValueReference替换现在的ValueReference。LoadingValueReference的特点是isLoading方法返回true
final LoadingValueReference<K, V> loadingValueReference =
insertLoadingValueReference(key, hash, checkTime);
if (loadingValueReference == null) {
return null;
}
// Future说明是异步在刷新缓存
ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
// Future的isDone方法用来判断这个Future是不是完整的,即是否能够返回结果(不代表已经指向完成)
if (result.isDone()) {
try {
// 阻塞,不断的轮询调用Future的get方法获取future的结果,直到线程被打断
return Uninterruptibles.getUninterruptibly(result);
} catch (Throwable t) {
// don't let refresh exceptions propagate; error was already logged
}
}
return null;
}
5、等待其他线程刷新:waitForLoadingValue
V waitForLoadingValue(ReferenceEntry<K, V> e, K key, ValueReference<K, V> valueReference)
throws ExecutionException {
// double check机制。在调用这个方法前,实际上会先验证valueReference.isLoading() == true
if (!valueReference.isLoading()) {
throw new AssertionError();
}
// 还是在检查。如果当前线程持有锁,就会抛异常
checkState(!Thread.holdsLock(e), "Recursive load of: %s", key);
// don't consider expiration as we're concurrent with loading
try {
// 阻塞的去获取锁。底层就是getUninterruptibly(futureValue)
V value = valueReference.waitForValue();
if (value == null) {
throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
}
// re-read ticker now that loading has completed
long now = map.ticker.read();
recordRead(e, now);
return value;
} finally {
// 这种情况要记录缓存没有击中
statsCounter.recordMisses(1);
}
}
防止读上面的检查代码时搞不清楚,贴上Preconditions.checkState的代码:
// 防止读上面的检查代码时搞不清楚,贴上Preconditions.checkState的代码
public static void checkState(
boolean b, @Nullable String errorMessageTemplate, @Nullable Object p1) {
if (!b) {
throw new IllegalStateException(lenientFormat(errorMessageTemplate, p1));
}
}
6、加锁获取缓存:lockedGetOrLoad(key, hash, loader)
会走到这一步的场景:
- segment为空;
- key或value不存在(没有缓存,或者弱引用、软引用被回收);
- 缓存超时(expireAfterAccess或expireAfterWrite触发的)
lockedGetOrLoad的逻辑相当于:根据key,使用loader获取到value并写入到缓存中,然后再返回。这个跟写很接近了,源码也比较长,重新拉一节来解析。
2.4 步骤详解:加锁并加载
会走到这一步的场景:
- segment为空;
- key或value不存在(没有缓存,或者弱引用、软引用被回收);
- 缓存超时(expireAfterAccess或expireAfterWrite触发的)
这一步的主要逻辑:
- 加锁;
- 清理无效节点(被GC回收的非强引用节点、超时节点);
- 根据key查询缓存,并验证value是否存在、是否有效;
- 如果需要加载缓存,则创建LoadingValueReference节点并加载缓存;
- 释放锁,并执行清理操作(发布缓存清理事件)
- 等待缓存加载
源码:
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 {
// 重新计算现在的时间,是为了应对加锁前有其他线程插入了key的缓存的场景
// re-read ticker once inside the lock
long now = map.ticker.read();
// 写前清理的主要内容:清理被回收的弱引用;清理超时节点;readCount重置为0
preWriteCleanup(now);
// newCount用来给this.count赋值。应用场景:
// 如果读到了超时的缓存,或者被GC回收的缓存,就清理掉读到的缓存,同时让count-1。清理之后,会再去加载缓存
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循环用来遍历,是否存在key相应的缓存
for (e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
// if判断是否存在缓存
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
valueReference = e.getValueReference();
// if判断读到的缓存引用,是否是正在加载缓存
if (valueReference.isLoading()) {
// 如果已经有其他线程在加载缓存,则本线程不再触发加载操作,而是等待加载结果
createNewEntry = false;
} else {
V value = valueReference.get();
if (value == null) {
// 此时value已经被GC回收了,enqueueNotification表示发布缓存回收事件,原因是COLLECTED
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
} else if (map.isExpired(e, now)) {
// preWriteCleanup中已经清理超时节点了,但是这里还是重复检查是否超时。
// 双重检查机制是加锁场景的一种常用手段,避免加锁前的检查和加锁之间其他触发了影响操作目标的操作
// This is a duplicate check, as preWriteCleanup already purged expired
// entries, but let's accommodate an incorrect expiration queue.
enqueueNotification(
entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
} else {
// 如果读到的value存在且没有超时,说明value是有效值
// 记录一次加锁读
recordLockedRead(e, now);
// 记录一次缓存命中
statsCounter.recordHits(1);
// 返回有效值
// we were concurrent with loading; don't consider refresh
return value;
}
// 此时说明读到的value无效,应该移除相应的缓存,并让缓存数量减1
// immediately reuse invalid entries
writeQueue.remove(e);
accessQueue.remove(e);
this.count = newCount; // write-volatile
}
break;
}
}
// createNewEntry用来判断是否需要新建一个LoadingValueReference,用来加载缓存
// 如果上面的for循环中读到了LoadingValueReference类型的节点,就不用再加载缓存了
if (createNewEntry) {
loadingValueReference = new LoadingValueReference<>();
if (e == null) {
e = newEntry(key, hash, first);
e.setValueReference(loadingValueReference);
table.set(index, e);
} else {
e.setValueReference(loadingValueReference);
}
}
} finally {
// 针对key的写操作处理完成,释放锁
unlock();
// 写后清理,主要逻辑是调用无锁清理方法,发布缓存清理事件(缓存清理事件通过enqueueNotification加入队列)
postWriteCleanup();
}
if (createNewEntry) {
try {
// Synchronizes on the entry to allow failing fast when a recursive load is
// detected. This may be circumvented when an entry is copied, but will fail fast most
// of the time.
// 如果当前线程触发了缓存加载的操作,就针对e加锁等待加载完成
synchronized (e) {
return loadSync(key, hash, loadingValueReference, loader);
}
} finally {
statsCounter.recordMisses(1);
}
} else {
// 如果当前线程不是触发缓存加载的操作的线程,就调用waitForLoadingValue方法同步等待缓存加载完成
// The entry already exists. Wait for loading.
return waitForLoadingValue(e, key, valueReference);
}
}
1、写前加锁清理preWriteCleanup:
preWriteClean本质上是调用了加锁场景的清理方法runLockedCleanUp方法:
@GuardedBy("this")
void preWriteCleanup(long now) {
runLockedCleanup(now);
}
runLockedCleanUp方法要清理的内容,包括:被GC回收的非强引用节点、超时节点,同时还会将readCount重置为0。readCount会在每次读操作后累加,并在达到一定门限(63)时执行缓存清理工作。
readCount重置的场景:
- 写操作,preWriteClean调用runLockedCleanUp;
- 读操作,postReadCleanup,readCount达到一定门限(63)时会调用runLockedCleanUp;
- clear()方法被调用,清除全部缓存的时候,会重置readCount;
void runLockedCleanup(long now) {
if (tryLock()) {
try {
drainReferenceQueues();
expireEntries(now); // calls drainRecencyQueue
readCount.set(0);
} finally {
unlock();
}
}
}
2、写后无锁清理postWriteCleanup:
postWriteCleanup主要是用来发布缓存清理通知。
postWriteCleanup调用了无锁清理方法runUnlockedCleanup:
void postWriteCleanup() {
runUnlockedCleanup();
}
runUnlockedCleanup方法是在当前线程无锁的情况下,才会去发布缓存清理通知。之所以要判断当前线程没有持锁,是因为持锁的情况下是可能产生新的通知的。
void runUnlockedCleanup() {
// locked cleanup may generate notifications we can send unlocked
if (!isHeldByCurrentThread()) {
map.processPendingNotifications();
}
}
}
发布通知的具体逻辑。实际上就是从通知队列中取出所以的缓存清理通知,并分别传入缓存移除器的onRemoval方法。
/**
* Notifies listeners that an entry has been automatically removed due to expiration, eviction, or
* eligibility for garbage collection. This should be called every time expireEntries or
* evictEntry is called (once the lock is released).
*/
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);
}
}
}
3、添加/更新缓存
3.1 主要方式
用户主动添加缓存的方式包括:
- put(K key, V value):存入一条缓存;
- putAll(Map<? extends K, ? extends V> m):存储一批缓存。
3.2 源码分析
putAll是通过对Map迭代分别调用put方法实现的。所以,这里分析put的逻辑。
1、调用成员变量localCache的put方法
@Override
public void put(K key, V value) {
localCache.put(key, value);
}
2、localCache的put方法:
localCache的put方法,主要作用是:
- 检查key是否有效(不是null);
- 根据key计算hash;
- 根据hash的高位使用hash算法寻找相应的segment;
- 调用segment的put方法,查询或者加载缓存。
@Override
public V put(K key, V value) {
checkNotNull(key);
checkNotNull(value);
int hash = hash(key);
return segmentFor(hash).put(key, hash, value, false);
}
segmentFor方法用来根据hash的高位从segments数组中取出相应的segment实例。详细解析参考上面第二章1.2的分析。
/**
* Returns the segment that should be used for a key with the given hash.
*
* @param hash the hash code for the key
* @return the segment
*/
Segment<K, V> segmentFor(int hash) {
// TODO(fry): Lazily create segments?
return segments[(hash >>> segmentShift) & segmentMask];
}
3、segment的put方法:
segment的get方法获取缓存的真正核心逻辑。它的主要流程包括:
- 加锁;
- 清理无效节点(被GC回收的非强引用节点、超时节点);
- 判断缓存数量是否达到了扩容门限,如果达到了则进行扩容;
- 根据key查询缓存,并验证value是否存在、是否有效
- 创建缓存节点并保存;
- 判断是否需要由于容量超限驱逐节点;
- 释放锁,并执行清理操作(发布缓存清理事件)
源码如下:
@Nullable
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();
try {
long now = map.ticker.read();
// 写前清理,主要逻辑是清理被GC的节点和超时节点,同时将readCount置0。详情参见上一节
preWriteCleanup(now);
int newCount = this.count + 1;
// 门限是当前长度的0.75倍:this.threshold = newTable.length() * 3 / 4; // 0.75
if (newCount > this.threshold) { // ensure capacity
// 扩容。详情参见下一节
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);
// 在表中寻找节点是否已经存在
// Look for an existing entry.
for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
K entryKey = e.getKey();
// if用来判断节点存在
if (e.getHash() == hash
&& entryKey != null
&& map.keyEquivalence.equivalent(key, entryKey)) {
// We found an existing entry.
ValueReference<K, V> valueReference = e.getValueReference();
V entryValue = valueReference.get();
// if用来判断value是否存在。key存在而value不存在,说明value不是强引用,被GC回收了
if (entryValue == null) {
// modCount用来记录修改table大小的次数。
// modCount主要用在批量读取中,判断批量读取的缓存是不是来自于同一个内存镜像。如果不是,可能需要重试。
// 比如,在isEmpty方法中会校验segment的modCount
++modCount;
// valueReference是否包含激活的value:如果包含有效的value,或者已经被驱逐的value,或者是已经被GC回收的value,都会返回true
// 目前所有的实现类都会return true
// 结合上面的判断,说明这个Entry曾经存入过缓存,但是value已经不存在,说明被GC清理了
if (valueReference.isActive()) {
// 发布缓存清理事件
enqueueNotification(
key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
setValue(e, key, value, now);
newCount = this.count; // count remains unchanged
} else {
// 目前没有代码会进入这里
setValue(e, key, value, now);
newCount = this.count + 1;
}
this.count = newCount; // write-volatile
// 驱逐缓存。会判断是否容量超限,如果超限则按LRU清除一部分缓存保证容量不超限
evictEntries(e);
// put传入的value被存入的情况,会返回null
return null;
} else if (onlyIfAbsent) {
// 此时value存在,且put方法要求onlyIfAbsent才会写入缓存。当前版本Cache没有提供putIfAbsent方法,所以这里走不到
// Mimic
// "if (!map.containsKey(key)) ...
// else return map.get(key);
recordLockedRead(e, now);
// 如果缓存已经存在,则返回旧值
return entryValue;
} else {
// 没有修改table的大小,依然改了modCount。。。这里有点迷,感觉以后的版本可能会扩展modCount的用途
// clobber existing entry, count remains unchanged
++modCount;
enqueueNotification(
key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
setValue(e, key, value, now);
// 驱逐缓存。会判断是否容量超限,如果超限则按LRU清除一部分缓存保证容量不超限
evictEntries(e);
// 如果缓存已经存在,则返回旧值
return entryValue;
}
}
}
// 创建新节点,所以需要修改modCount
// Create a new entry.
++modCount;
ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
setValue(newEntry, key, value, now);
table.set(index, newEntry);
newCount = this.count + 1;
this.count = newCount; // write-volatile
// 驱逐缓存
evictEntries(newEntry);
return null;
} finally {
// 释放锁和写后清理
unlock();
postWriteCleanup();
}
}
3.3 扩容
1、需要重新计算的变量:
- 扩容门限threshold: newTable.length() * 3 / 4
- Hash计算的掩码newMask: ewTable.length() - 1
2、扩容的基本逻辑:
- 长度是否超限(MAXIMUM_CAPACITY = 1 << 30);
- 生成新的数组newTable,长度是现有数组的2倍;
- 重新计算数组的相应变量,包括threshold和newMask;
- 旧数组中的元素迁移到新数组(下面详细讲解);
- 新表替换旧表
3、数组迁移的详细逻辑:
- 遍历旧数组,找到数组每个位置的头节点;
- 如果头节点不为空,则从头节点开始,遍历链表;
- 对每个节点判断是否key、value被GC回收,如果回收则清理节点
- 对每个没有被GC的节点,使用新的掩码newMask和key的哈希,得到一个新的值,这个新的值就是在新数组中的位置;
- 如果上一步计算得到的位置不再是现在的位置,则放入新位置
4、源码:
/** Expands the table if possible. */
@GuardedBy("this")
void expand() {
AtomicReferenceArray<ReferenceEntry<K, V>> oldTable = table;
int oldCapacity = oldTable.length();
if (oldCapacity >= MAXIMUM_CAPACITY) {
return;
}
/*
* Reclassify nodes in each list to new Map. Because we are using power-of-two expansion, the
* elements from each bin must either stay at same index, or move with a power of two offset.
* We eliminate unnecessary node creation by catching cases where old nodes can be reused
* because their next fields won't change. Statistically, at the default threshold, only about
* one-sixth of them need cloning when a table doubles. The nodes they replace will be garbage
* collectable as soon as they are no longer referenced by any reader thread that may be in
* the midst of traversing table right now.
*/
int newCount = count;
AtomicReferenceArray<ReferenceEntry<K, V>> newTable = newEntryArray(oldCapacity << 1);
threshold = newTable.length() * 3 / 4;
int newMask = newTable.length() - 1;
for (int oldIndex = 0; oldIndex < oldCapacity; ++oldIndex) {
// We need to guarantee that any existing reads of old Map can
// proceed. So we cannot yet null out each bin.
ReferenceEntry<K, V> head = oldTable.get(oldIndex);
if (head != null) {
ReferenceEntry<K, V> next = head.getNext();
int headIndex = head.getHash() & newMask;
// Single node on list
if (next == null) {
newTable.set(headIndex, head);
} else {
// Reuse the consecutive sequence of nodes with the same target
// index from the end of the list. tail points to the first
// entry in the reusable list.
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) {
// The index changed. We'll need to copy the previous entry.
tailIndex = newIndex;
tail = e;
}
}
newTable.set(tailIndex, tail);
// Clone nodes leading up to the tail.
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 {
removeCollectedEntry(e);
newCount--;
}
}
}
}
}
table = newTable;
this.count = newCount;
}
3.4 驱逐缓存
驱逐缓存的方法evictEntries主要在新加入缓存元素后触发,用来从访问队列中按LRU算法驱逐缓存,直到segment的当前的权重值不大于最大权重值。需要注意的是,由于容量限制进行LRU算法时,是按找访问时间进行LRU的,而不是按照写入时间进行的。
主要逻辑:
- 先将最近访问的元素按顺序从recencyQueue存入到accessQueue;
- 判断新加入的元素是否过大,超过了segment的容量。如果超过了容量,则直接丢弃;
- 如果最大权重仍然超限,则不断从accessQueue中获取元素并进行驱逐,直到不再超限。
源码如下:
/**
* Performs eviction if the segment is over capacity. Avoids flushing the entire cache if the
* newest entry exceeds the maximum weight all on its own.
*
* @param newest the most recently added entry
*/
@GuardedBy("this")
void evictEntries(ReferenceEntry<K, V> newest) {
if (!map.evictsBySize()) {
return;
}
// 把recencyQueue的内容全部按FIFO顺序放入到accessQueue,并把recencyQueue清空
drainRecencyQueue();
// 如果新加入的节点权重太高,超过了segment的最大容量,则直接驱逐
// If the newest entry by itself is too heavy for the segment, don't bother evicting
// anything else, just that
if (newest.getValueReference().getWeight() > maxSegmentWeight) {
if (!removeEntry(newest, newest.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
// 如果目前的全部权重totalWeight仍然大于最大权重,则不断从accessQueue中取出元素并驱逐
while (totalWeight > maxSegmentWeight) {
ReferenceEntry<K, V> e = getNextEvictable();
if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
}
1、排空recencyQueue队列drainRecencyQueue:
drainRecencyQueue方法涉及到两个队列:
- recencyQueue:每次读操作都会将读操作访问的元素添加到recencyQueue的队尾;每次写操作都会将recencyQueue的元素都放入accessQueue,并清空recencyQueue
- accessQueue:accessQueue用于存放访问元素的顺序。accessQueue的类型是Guava自定义的AccessQueue,每次在写入一个元素时,会把这个元素写入前在accessQueue的位置替换成队尾,而不是直接放到队尾,避免了一个元素在队列的多个位置重复存在。
/**
* Drains the recency queue, updating eviction metadata that the entries therein were read in
* the specified relative order. This currently amounts to adding them to relevant eviction
* lists (accounting for the fact that they could have been removed from the map since being
* added to the recency queue).
*/
@GuardedBy("this")
void drainRecencyQueue() {
ReferenceEntry<K, V> e;
while ((e = recencyQueue.poll()) != null) {
// An entry may be in the recency queue despite it being removed from
// the map . This can occur when the entry was concurrently read while a
// writer is removing it from the segment or after a clear has removed
// all of the segment's entries.
// 每次写入元素的时候,都会将元素写入到accessQueue,所以如果recencyQueue的元素在accessQueue不存在,表示这个元素在真实的缓存中也不存在
// 这种情况出现的场景是:有其他线程并行的移除了这个元素
if (accessQueue.contains(e)) {
// accessQueue的add方法实际上是调用了offer方法,可以看下下面的源码
accessQueue.add(e);
}
}
}
2、AccessQueue实现的offer方法:
@Override
public boolean offer(ReferenceEntry<K, V> entry) {
// unlink
// connectAccessOrder用来拼接入参中的两个节点
// 这个方法是用来将元素从现有顺序中移除的。可以看下下面的方法源码
connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue());
// 这两行代码,实际上就是将entry放到head和head.previous之间
// 清理的时候,是从head.next开始清理的。相当于,head.next是实际的队首元素,head.previous是实际的队尾元素
// add to tail
connectAccessOrder(head.getPreviousInAccessQueue(), entry);
connectAccessOrder(entry, head);
return true;
}
// 用来拼接入参中的两个节点
// Guarded By Segment.this
static <K, V> void connectAccessOrder(ReferenceEntry<K, V> previous, ReferenceEntry<K, V> next) {
previous.setNextInAccessQueue(next);
next.setPreviousInAccessQueue(previous);
}
3、移除节点removeEntry
这个方法是从table中找到entry节点,然后调用removeValueFromChain方法删除节点。
@VisibleForTesting
@GuardedBy("this")
boolean 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;
ReferenceEntry<K, V> newFirst =
removeValueFromChain(
first,
e,
e.getKey(),
hash,
e.getValueReference().get(),
e.getValueReference(),
cause);
newCount = this.count - 1;
table.set(index, newFirst);
this.count = newCount; // write-volatile
return true;
}
}
return false;
}
从链表移除value:
removeValueFromChain方法封装了移除一个value的底层细节,包括:
- 入队缓存删除通知;
- 从writeQueue队列移除。这个队列记录的是写entry的顺序;
- 从accessQueue移除。上面有介绍,这个队列记录的是访问accessQueue的顺序;
- 如果节点正在加载,则节点类型是LoadingValueReference,此时就失效掉旧值,即把旧值oldValue设置为UNSET;
- 如果节点不在加载,就删除掉整个entry。
@GuardedBy("this")
@Nullable
ReferenceEntry<K, V> removeValueFromChain(
ReferenceEntry<K, V> first,
ReferenceEntry<K, V> entry,
@Nullable 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);
}
}
从链表移除节点entry:
对于entry所在的链表,entry后面的节点先拼在链表的头节点后面,然后使用头插法从头节点开始一个节点一个节点往链表的最前面插,知道碰到entry为止。
示例:原链表:1->2->3->4->5->6,其中4是需要删除的节点,则使用下面的逻辑移除4以后,顺序变成:3->2->1->5->6
@GuardedBy("this")
@Nullable
ReferenceEntry<K, V> removeEntryFromChain(
ReferenceEntry<K, V> first, ReferenceEntry<K, V> entry) {
int newCount = count;
ReferenceEntry<K, V> newFirst = entry.getNext();
for (ReferenceEntry<K, V> e = first; e != entry; e = e.getNext()) {
// 这里需要重点关注一下:复制e的值复制生成新节点next,同时将next的下一个节点设置为newFirst
ReferenceEntry<K, V> next = copyEntry(e, newFirst);
if (next != null) {
newFirst = next;
} else {
removeCollectedEntry(e);
newCount--;
}
}
this.count = newCount;
return newFirst;
}