WeakCache弱缓存略析
一、WeakCache类总览
WeakCache是由java.lang.reflect反射包下提供的二级缓存。二级缓存的结构为<Key,Sub-Key,Value>,其中Key和Value均为弱引用,而Sub-Key为强引用。获取缓存值的get()方法时不仅需要Key,还需要多添加一个参数(P类型)。通过这个参数和Key就可以计算出Sub-Key。WeakCache对象的构造方法中会传入两个工厂对象,其中一个为subKeyFactory用于计算Sub-Key的,另外一个为valueFactory用于计算最终的结果的。Key可以为Null但是Sub-key和Value不能为Null。弱引用对象被GC回收掉后不会立马清除缓存,而是会在WeakCache的公开方法方法中手动懒式清除。
final class WeakCache<K, P, V> {
...
}
如上代码所示,WeakCache使用关键字final修饰,不允许被继承。并且会声明三种泛型参数,K为Key的类型,P为参数的类型,V为最终结果的类型。
二、WeakCache属性
WeakCache共有5个私有属性,分别为:
private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
// the keytypeis Object for supporting null key
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map
= new ConcurrentHashMap<>();
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap
= new ConcurrentHashMap<>();
private final BiFunction<K, P, ?> subKeyFactory;
private final BiFunction<K, P, V> valueFactory;
- refQueue: 用于存放GC后被回收的一级Key(使用WeakReference包装)。Reference相关资料
- map:由嵌套ConcurentMap构成的二级缓存,其中第一层Key由K类型参数生成,第二层Key有K类型参数及P类型参数共同计算的结果。注意这里的value的实际类型可能会有多种情况。
- reverseMap:由于两层缓存,因此通过倒排Map加快判断某value结果是否已经存在。
- subKeyFactory:BiFunction类型,根据K类型参数及P类型参数构造二级Key
- valueFactory:BiFunctionle类型,根据K类型参数及P类型参数获取Value.
三、WeakCache构造方法
WeakCache类仅提供了一个构造方法:
public WeakCache(BiFunction<K, P, ?> subKeyFactory,
BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}
构造方法入参需提供两个BiFunction类型的非null函数式实例,其中一个subKeyFactory用于根据K类型参数、P类型参数生成二级Key,另外一个valueFactory用于根据K类型参数和P类型参数生成V类型的具体值,即Value。
四、WeakCache的内部类
WeakCache内部声明了5个私有内部类,其中1个内部接口,1个非静态内部类,3个静态内部类。
4.1 Value接口
/**
* Common type of value suppliers that are holding a referent.
* The {@link #equals} and {@link #hashCode} of implementations is defined
* to compare the referent by identity.
*/
private interface Value<V> extends Supplier<V> {}
这里定义了WeakCache中二级缓存的value的类型(Value<V>),实际上是Supplier类型。为什么不直接使用Supplier<V>呢?在前面WeakCache属性中我们提到reverseMap适用于倒排Map查找缓存值使用,但是reverseMap的类型为ConcurrentMap<Supplier<V>, Boolean>。这问题就来了,在这个map进行hash时是按照Supplier实例进行的,但是Supplier实际上仅是我们对V类型值的延迟封装,真正hash应该按照value值进行,即我们不关心Supplier对value封装的具体形式,只要是supplier1.get()== supplier2.get()也认为是同一个value。
因此需要自定义Value继承Supplier,任何实现Value接口都应该重写hashCode()及equals()方法。下面会说到CacheValue、LookupValue都会实现Value接口,也都重写两个方法。
4.2 Factory内部类
private final class Factory implements Supplier<V> {
...
}
Factory作为私有内部类,实现了Supplier<V>的接口。在WeakCache中的主要作用是完成了对结果Value生成过程、延迟构建的封装。二级缓存的更新也会由Factory完成。
4.2.1 Factory属性
Factory共有4个私有属性,分别为:
private final K key;
private final P parameter;
private final Object subKey;
private final ConcurrentMap<Object, Supplier<V>> valuesMap;
对于二级缓存的Value来说,最终要的四个属性包括:
- key:K类型的一级Key
- parameter:P类型的参数
- subKey:由以上二者生成后的二级Key
- valuesMap:当前一级Key下所有的缓存Map
这四个属性均为Factory构建value的必要参数,因此Factory仅提供了一个构造函数,形参需传入这四个属性。
4.2.1 Factory公开方法
Factory仅提供了一个实现Supplier接口的get方法,主要是用于根据以上四个缓存的属性延迟获取真正的value对象。代码及注释贴下:
public synchronized V get() { // 序列化进入,防止并发
// 按照惯例,加锁内部重新检查[锁等待期间可能情况不一样了]
Supplier<V> supplier = valuesMap.get(subKey);
if (supplier != this) {
// 检查失败,缓存内部二级Key对应的Value已经不是当前的Factory对象了。后续无法继续处理,只能返回null,由调用方法处理这种内部检查不符合预期的情况。
// 可能我们将Factory值替换为了CacheValue值了,返回null可以直接取cacheValue即可
// 也可能是valueFactory根据K\P生成value时出现异常或null,此时的Factory对象会被认为为无效值。结果从二级缓存中移除并且抛异常,下次在进入get时,就可能会为null。
return null;
}
// else still us (supplier == this)
// 校验通过后,根据this进行获取V类型的Value值
V value = null;
try {
// 使用valueFactory根据K和P来生成Value
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
} finally {
if (value == null) { // 生成的Value为null时,必须把当前Factory对象缓存移除掉
valuesMap.remove(subKey, this);
}
}
// 必须保证valueFactory生成的Value不会null
assert value != null;
// 对生成的value使用内部类CacheValue封装,CacheValue是继承了WeakReference,也属于弱引用,后续会单独提到CacheValue。
CacheValue<V> cacheValue = new CacheValue<>(value);
// 将cacheValue放置在倒排Map中,方便后续判断
reverseMap.put(cacheValue, Boolean.TRUE);
// 使用cacheValue替换二级Key对应的缓存值
if (!valuesMap.replace(subKey, this, cacheValue)) {
// 细节,调用底层方法时,返回值各种情况都应该考虑到
throw new AssertionError("Should not reach here");
}
// 返回真实的V类型的value值
return value;
}
JDK的代码都是由很多性能优化及细节值得我们学习的,所以看源码并不能止步于看懂,而是真正理解,理解后知道我们下次碰见类似的情况是否也能这样思考,是否也能注意容易忽略的细节部分。下面根据一些问题我们展开思考:
- 根据Factory#get()方法内部都做了些什么?
主要负责使用valueFactory根据K和P来获取Value的实际逻辑。并且获取到value后会把对应的值重新封装为CacheValue弱引用缓存起来,在下次GC之前获取Value就可以通过缓存了。当然,此外还会把CacheValue放入倒排Map中便于后续判断缓存是否存在指定的value值。 - Factory为什么是内部类而不是静态内部类?
Factory对象get()方法内部需要使用到外部类非静态属性-valueFactory; - WeakCache为什么不直接调用valueFactory生成Value,而要设计Factory内部类?
第一个原因是封装性好。将valueFactory生成Value的逻辑以及将value包装为弱引用,且记录倒排map这些一气呵成的逻辑封装起来。
第二个原因-延迟获取缓存值,节省内存,提升速度。valueFactory是外界提供的,生成value实际大小不明确。WeakCache#map缓存值使用的策略是先使用用Factory对象占坑(Factory对象创建时仅缓存所需要的数据即可,内存占用小),真正需要获取value时才使用Factory#get()方法获取实际的Value。获取真正value之后,会把value封装为弱引用(CacheValue)替换掉Factory对象。这样做,既满足了缓存的需求,又不会浪费内存空间。【在很多底层设计中都有类似的操作,一定要学会】 - Factory#get()方法为什么要使用synchronized关键字?
主要是为了保证缓存map(WeakCache#map)和倒排map(WeakCache#reverseMap)操作的原子性(or,一致性)。单个map都是concurrentMap类型,get\put操作均是线程安全的。但是Factory#get()获取到value之后会不会存在reverseMap有对应的cacheValue,而在valuesMap#replace覆盖不成功呢?在无synchronized时是有可能的,假设存在2个线程同是执行get,其中一个线程执行至reverseMap#put(),另外一个线程执行valueFactory#apply异常,就会存在这种问题。 - 同步代码块应注意哪些问题?
在synchronized同步代码块中一定要进行二次检查,二次检查主要是为了防止等待锁的过程中,外部数据的变化与同步代码逻辑不兼容性。比如在单例模式(DCL)中,也会二次检查对象是否已经创建-已经创建了就不能再创建了,即不必执行同步代码。在Factory#get()方法中检查二级缓存的值是否为当前Factory对象,校验成功后使用Factory对象执行下面同步代码逻辑。需要注意的是,同步代码块正常执行或任何异常都必须保证该检查条件成立。正常执行后会将缓存中value替换为CacheValue,valueFactory获取value出现异常后会将二级缓存清除。
为什么"throw new AssertionError(“Should not reach here”)"不清除缓存?
这种情况是绝对的error,valueFactory获取value出现异常会清除重试
4.3 CacheValue内部类
private static final class CacheValue<V>
extends WeakReference<V> implements Value<V> {
...
}
WeakCache声明了私有静态内部类CacheValue,是用于封装缓存value的类型。CacheValue继承了WeakReference,因此类CacheValue包装的数据均为弱引用,生命周期为下一次GC之前。CacheValue实现了Value接口,但是接口内get()方法实际上是由WeakReference继承实现的。
CacheValue判等逻辑为“==”操作。hashCode()返回内存地址,equals()返回逻辑有两点:① CacheValue对象==;② 均为Value内部类型且Value#get()返回类型V的实际对象==。
疑问:似乎CacheValue不实现Value接口也能行?确实,但是Value接口的含义在于重写hashcode()与equals()方法。
4.4 CacheKey内部类
private static final class CacheKey<K> extends WeakReference<K> {
...
}
CacheKey也是私有静态内部类,同样也继承了WeakReference,因此使用CacheKey是为了包装K类型的Key为弱引用类型,生命周期为下一次GC之前。
CacheKey和CacheValue还有三点主要差异:(不算CacheValue实现了Value接口)
- CacheKey允许Key为null
当Key为null时,使用NULL_KEY空对象代替。 - CacheKey使用了ReferenceQueue,当GC回收后,可以通过引用队列手动清楚掉。(为啥手动清楚?为了清空WeakCache#map、WeakCache#reverseMap)
- CacheKey还提供了expungeFrom方法。手动清空ReferenceQueue时,会清空WeakCache#map、WeakCache#reverseMap。
CacheKey的equals()方法中使用this.getClass判断类对象,其实可以替换为“obj instance CacheKey”。但是一般instance用于判断基类引用的真实类型,这里CacheKey类为final不存在继承、多态。可能这里使用instance有点奇怪吧。
4.5 LookupValue内部类
private static final class LookupValue<V> implements Value<V> {
...
}
LookupValue也是私有静态内部类,实现了Value接口。从LookupValue类名上看,是查找Value的意思。其实这就是为了方便从倒排Map(reverseMap)中查询Value。前面讲过,reverseMap查找Value的hash不能使用Supplier默认方法(继承自Object),而应该实现Value接口重写hashcode()与equals()方法。
为什么不复用CacheValue而是新增LookupValue呢?查找Value是否存在仅是为了查找而不会存储,因此没必要用弱引用包装,LookupValue仅实现Value接口,性能肯定更好。
五、WeakCache的公开方法
WeakCache共提供了3个公开方法:
- get(K key, P parameter):根据K类型Key值即P类型参数来查找缓存的Value值。
- containsValue(V value):判断缓存中是否包含某个Vlaue
- size():返回所有缓存的Value值。
所有的公开方法被访问时都会调用私有方法expungeStaleEntries()去根据WeakCache#refQueue队列poll出已清除的CacheKey来清空WeakCache#map、WeakCache#reverseMap中的无效缓存引用。
这里仅给出get方法的代码解析,其他两个代码简单,不再赘述。
public V get(K key, P parameter) {
Objects.requireNonNull(parameter); // 例行校验P类型参数不能为null
expungeStaleEntries(); // 公开方法均会先清楚已回收的Key及缓存
// 使用CacheKey创建(一级)Key的弱引用,并且使用了引用队列。
// 【之所以返回Object,兼容key为null的情况】
Object cacheKey = CacheKey.valueOf(key, refQueue);
// 根据cacheKey获取二级map-valuesMap。
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
if (valuesMap == null) {
// 注意:这里可以仅使用下面一块代码,提前get增加性能优化。
// cacheKey不存在,这里必须使用putIfAbsent,而不能使用put进行初始化
ConcurrentMap<Object, Supplier<V>> oldValuesMap
= map.putIfAbsent(cacheKey,
valuesMap = new ConcurrentHashMap<>());
if (oldValuesMap != null) { // putIfabsent存在返回已有值,不存在返回null
valuesMap = oldValuesMap;
}
}
// create subKey and retrieve the possible Supplier<V> stored by that
// subKey from valuesMap
// 一级上面处理完了,下面就是处理二级map-valuesMap的过程。
// 首先获取二级Key-subKey。二级Key的生成逻辑由调用方提供的subKeyFactory、K、P决定。
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
// 从二级map中尝试获取subKey的值,这里的值可能是null也可能是Factory也可能是CacheValue
// 注意:supplier即便不为null,也可能get()==null,原因是GC已经回收了
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null; // 声明缓存Value的工厂前身(见Factory类解析)
while (true) {
if (supplier != null) {
// supplier可能是Factory也可能是CacheValue
// 注意:supplier即便不为null,也可能get()==null,原因是GC已经回收了
V value = supplier.get();
if (value != null) {
// 不为null,说明通过Factory或CacheValue获得了value,直接返回
return value;
}
}
/**
* 三种情况:
* ① 二级缓存map中没有subKey对应的值,即supplier==null 【唯第一次循环】
* ② supplier从二级缓存map中获取不为Null,但由于GC回收,get()==null 【不确定】
* ③ supplier instanceof Factory 且 Factory.get()在异常时会返回null。【非第一次循环】
*/
if (factory == null) {
// 初始化Factory对象,包含value创建的所有必要数据 【唯第一次循环】
factory = new Factory(key, parameter, subKey, valuesMap);
}
if (supplier == null) {
// supplier为null,尝试向二级map中添加<subKey, factory>【唯第一次循环】
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
// 成功向二级map添加factory
supplier = factory;
}
// 多线程原因,二级map中已经有了subKey的supplier。
// 同样的,这里虽不为null,也并不代表它不会是已GC回收的弱引用
} else {
/**
* supplier不为null,有两种可能:
* ① supplier从二级缓存map中获取不为Null,但由于GC回收,get()==null 【不确定】
* ② supplier instanceof Factory 且 Factory.get()在异常时会返回null。【非第一次循环】
*/
if (valuesMap.replace(subKey, supplier, factory)) {
// 这里是情况①,不会是②,Factory.get()在异常时会在finally中从二级map中清除
// supplier因为被回收,只能重置为factory
supplier = factory;
} else {
// Factory.get()异常,重试
supplier = valuesMap.get(subKey);
}
}
}
}
上面方法注释的十分详细了,这里就简要说下几个部分:
(1)代码逻辑中设置map数据均使用的是putIfAbsent方法,不能使用put方法。在处理一级Key使用putIfAbsent之前还使用了get方法判断是否存在二级map。这里使用get()方法也是为了优化性能。
(2)在处理二级map获取value的过程,注意三种情况:① supplier == null ② supplier不为null 但已被回收 ③ supplier不为null、未被回收,但内部出错重试。
(3) 从获取缓存value的逻辑来看,Factory的封装性更加重要,反而延迟性(节省内存,避免不必要的初始化)实际上体现的并不明显。由于Factory将value初始化所需要的数据封装起来,赋值给supplier进行兜底(如null或已被回收),转换后统一使用supplier#get获取value值。
六、提问
WeakCache的基本内容和代码都讲解完了,为了更好的认知WeakCache的使用场景,还有几个问题需要我们明确并学习下。
- 为什么WeakCache的key、value设置为弱引用,而sub-Key为强引用?
缓存值value设置弱引用的的目的是控制缓存的有效期,至下一个GC之前。
缓存一级key也设置为弱引用是为了跟踪缓存值回收后记录到队列中,手动清除各map中的引用。
sub-Key不需要弱引用,手动清楚引用时,二级key也会被删除。 - 为什么WeakCache一级key允许为null,而sub-Key、value均不允许为null
value在WeakCache中设计为弱引用,当value为null的时候就会引起歧义,即value是已经被回收了还是本身就是null,无法区分。
sub-Key是由使用方提供的subKeyFactory以及一级Key、P类型参数共同生成的。subKeyFactory返回的null谁也不知道是异常还是正常null值,可能后引起非常难排查的bug。【不信任调用方,那就不支持,干脆直接约定不能为Null】
一级key是确定的,由调用方直接传过来的(不含任何中间逻辑),不会出现sub-Key的歧义问题。
Tip:支持或不支持null,很多时候都是因为歧义性风险的取舍 - 为什么Value实现类型需要重写hashcode()及equals()?
缓存的value值是通过弱引用类型封装的,真实value是其内部get()获取的值。因此必须重写hashcode()及equals()修改为==逻辑(包括两个部分,上面已有不再赘述) - 为什么Factory不需要继承Value?
Factory作为cacheValue的前身,为什么Factory不实现接口Value呢?如果Factory作为延迟构建性,我想Factory应该也会实现接口Value,并重写hashCode与equals(),但其实不然,虽然Factory对象会被暂时性的存储在WeakCache#map中,但是下一步就是会把Factory对象转换为CacheValue,所以Factory仅是构建value过程和数据的封装,并不是真正的缓存值value。
同时可以看到,WeakCache#size()方法统计的个数视为CacheValue的个数,而不包含Factory对象。所以Factory不需要重写hashCode与equals(),Factory也不会通过WeakCache#map中get()获取到。
在大部分的延迟缓存的设计中,是需要把Factory对象作为缓存值看待,真正需要的时候,再将Factory转换为实际可用value。
七、使用场景
WeakCache是由java.lang.reflect反射包下提供的二级弱缓存类,其主要的应用场景即使JDK动态代理。在JDK动态代理中,用于缓存已经生成的代理类的class对象。其中类型K为ClassLoader(类加载器),P类型为Class<?>[](接口列表),V类型为Class<?>(代理类class对象)。
WeakCache需要两个构造参数subKeyFactory、valueFactory,分别用于构建sub-Key和最终值。因此,JDK动态代理的一级Key就是类加载器实例,二级Key由类加载器和接口列表构成(实际上,只与接口列表相关),最终代理类class对象是由valueFactory生成。