Java源码阅读------WeakCache
描述
根据名字就知道这是一个缓存类,具体点是一个键值对存储的缓存类,至于weak的含义是因为这里的键与值都是弱引用,除此之外,这里所说的缓存是一个二级缓存。第一层是弱引用,第二层是强引用。
实现
二级缓存
private final ConcurrentMap<Object, ConcurrentMap<Object, Supplier<V>>> map = new ConcurrentHashMap<>();
private final ConcurrentMap<Supplier<V>, Boolean> reverseMap = new ConcurrentHashMap<>();
这里的map就是一个二级缓存的实现机制,Object(最左边的)就是key(键),第二个Object为subKey(子键),最后的Supplier< V >接口为值,实际是一个包裹最终是由其中的get方法来获取值。这种设计保证了key为null时也可用。reverseMap用于标注每个Supplier< V >的可用情况,用于缓存的过期处理。
构造方法
这里的K,P,V分别是键、参数、值,传入的参数是两个BiFunction接口,主要使用的是其中的apply方法,定义如下:
R apply(T t, U u);
通过传入的两个参数来获取值,使用这一接口实现了两个简单的工厂,subKeyFactory,valueFactory,这两个工厂分别实现了通过K与P来获取subKey(子键)和通过K,P来获取value值。
public WeakCache(BiFunction<K, P, ?> subKeyFactory, BiFunction<K, P, V> valueFactory) {
this.subKeyFactory = Objects.requireNonNull(subKeyFactory);
this.valueFactory = Objects.requireNonNull(valueFactory);
}
这样我们就可以通过subKeyFactory ,valueFactory 获取对应的子键与值。
CacheValue
静态内部类,实际上就是用于存储一个值的对象。
先看看WeakCache中的Value接口继承自Supplier接口,实际上是为了实现get方法
private interface Value<V> extends Supplier<V> {}
@FunctionalInterface
public interface Supplier<T> {
T get();
}
构造方法
private final int hash;
CacheValue(V value) {
super(value);
this.hash = System.identityHashCode(value); // compare by identity
}
实现了传入的Value的实例保存,同时通过System.identityHashCode(value);函数获取了该Value的HashCode。
hashCode与identityHashCode
在Object类中的hashCode可以获取相应对象的hashCode,而这个identityHashCode也是可以获取对象的hashCode,那么两这有什么不同吗?从源码看两者都是本地方法(native),实际上获取时的结果是与hashCode无异的,但是这里的hashCode指的是原有的Object中的hashCode的方法,如果进行了重写就可能会有不同了,所以为了得到原有的Object中的hashCode的值,identityHashCode会比较方便。
hashCode
重写的hashCode方法,直接返回hash值。
@Override
public int hashCode() {
return hash;
}
equals
重写的equals方法,用于判别传入的obj与弱引用中的value是否相同,这里的get()方法就是返回之前传入的弱引用的value。
@Override
public boolean equals(Object obj) {
V value;
return obj == this || obj instanceof Value &&
(value = get()) != null && value == ((Value<?>) obj).get();
}
LookupValue
静态内部类,为了便于对CacheValue中的值进行判断,建立了LookupValue,也实现了Value接口,是CacheValue运算时的替代,实现方式也很相似。
private final V value;//存储实际的值
LookupValue(V value) {//构造方法
this.value = value;
}
@Override
public V get() {//Value接口中的get方法,返回value的值
return value;
}
@Override
public int hashCode() {
return System.identityHashCode(value); //一样的hashCode计算
}
@Override
public boolean equals(Object obj) {
return obj == this ||
obj instanceof Value &&
this.value == ((Value<?>) obj).get(); // 类似的equals重写
}
CacheKey
静态内部类,CacheKey直接继承了使用引用队列的弱引用,来存储键值。
构造方法
实现的过程与CacheValue中的类似,只不过这里使用引用队列。
private final int hash;
private CacheKey(K key, ReferenceQueue<K> refQueue) {
super(key, refQueue);
this.hash = System.identityHashCode(key);
}
但是这里有一点注意,这里的构造方法是private的也就是说无法从外界直接调用,那么是如何构建出实例对象的呢?
valueOf
这是一个静态方法,传入的是要保存的key与对应的请求队列。
static <K> Object valueOf(K key, ReferenceQueue<K> refQueue) {
return key == null ? NULL_KEY : new CacheKey<>(key, refQueue);
}
这里的NULL_KEY是一个标识用于标注key为空的情况。
private static final Object NULL_KEY = new Object();
hashCode与equals
这里重写了hashCode与equals方法,基本的实现与CacheValue相同,不再赘述。
@Override
public int hashCode() {
return hash;
}
@Override
public boolean equals(Object obj) {
K key;
return obj == this || obj != null &&
obj.getClass() == this.getClass() && (key = this.get()) != null &&
key == ((CacheKey<K>) obj).get();
}
这里的get方法是引用Reference中定义的与之后描述的get方法不同。
expungeFrom
通过这个方法可以将含有这个键值的相关缓存清除。
void expungeFrom(ConcurrentMap<?, ? extends ConcurrentMap<?, ?>> map, ConcurrentMap<?, Boolean> reverseMap) {
ConcurrentMap<?, ?> valuesMap = map.remove(this);//直接从二级缓存中清除,并获取第二级的缓存map
if (valuesMap != null) {//遍历第二级缓存并在reverseMap中清除
for (Object cacheValue : valuesMap.values()) {
reverseMap.remove(cacheValue);
}
}
}
Factory
二级缓存的构建过程是通过这个静态内部类实现的,实现了Supplier接口。
构造方法
private final K key;//键
private final P parameter;//参数
private final Object subKey;//子键
private final ConcurrentMap<Object, Supplier<V>> valuesMap;//二级缓存
Factory(K key, P parameter, Object subKey, ConcurrentMap<Object, Supplier<V>> valuesMap) {
this.key = key;
this.parameter = parameter;
this.subKey = subKey;
this.valuesMap = valuesMap;
}
简单的数据准备工作。
get
这就是构建的具体过程了,实现了Supplier中的get方法,通过同步synchronized来保持线程安全
@Override
public synchronized V get() { // serialize access
// 这里有一个检查,为啥检查先买个关子,到之后WeakCatch的get方法再做细究
Supplier<V> supplier = valuesMap.get(subKey);
if (supplier != this) {
return null;
}
V value = null;
try {
value = Objects.requireNonNull(valueFactory.apply(key, parameter));
//通过key与parameter处理出value
} finally {
if (value == null) { // 失败的处理
valuesMap.remove(subKey, this);//移除相应的subKey对应的缓存
}
}
// 检查value是否完成建立
assert value != null;
// 包裹一下value建立一个cacheValue
CacheValue<V> cacheValue = new CacheValue<>(value);
// 将subKey中的二级缓存中原本的Factory换成包装过的cacheValue,至于为啥一开始二级缓存中会将Factory放进去我们也将其放到WeakCatch的get方法中解释
if (valuesMap.replace(subKey, this, cacheValue)) {
// 加入reverseMap中标记Value可用
reverseMap.put(cacheValue, Boolean.TRUE);
} else {//出错时的异常处理
throw new AssertionError("Should not reach here");
}
//处理成功后将value返回
return value;
}
expungeStaleEntries
由于二级缓存中的Key使用了弱引用,所以在实际使用时gc的不定期处理会导致部分的缓存失效,通过这个函数就可以实现对失效缓存的清除。
private final ReferenceQueue<K> refQueue = new ReferenceQueue<>();
private void expungeStaleEntries() {
CacheKey<K> cacheKey;
while ((cacheKey = (CacheKey<K>)refQueue.poll()) != null) {
cacheKey.expungeFrom(map, reverseMap);
}
}
refQueue是弱引用中的引用队列,在创建CacheKey时传入。(建议了解弱引用后继续)。
当gc处理了部分CacheKey时,refQueue中会有CacheKey的引用,取出来后在调用expungeFrom方法来清除过期的缓存。
size
同样的在获取可用的缓存数量时也使用了expungeStaleEntries来先清除过期的缓存。
public int size() {
expungeStaleEntries();
return reverseMap.size();
}
containsValue
判断缓存中是否包含对应的value。
public boolean containsValue(V value) {
Objects.requireNonNull(value);//对值进行判空
expungeStaleEntries();//清除过期缓存
return reverseMap.containsKey(new LookupValue<>(value));//使用LookupValue代替CacheValue使用,简化操作。
}
get
压轴的高潮来了,这是整个类的灵魂所在,缓存中Value的值是由这个函数来获取的。
public V get(K key, P parameter) {
//参数判空
Objects.requireNonNull(parameter);
//清除过期缓存
expungeStaleEntries();
//根据key生成相应的cacheKey
Object cacheKey = CacheKey.valueOf(key, refQueue);
// 获取二级缓存
ConcurrentMap<Object, Supplier<V>> valuesMap = map.get(cacheKey);
if (valuesMap == null) {//如果没有对应的二级缓存
ConcurrentMap<Object, Supplier<V>> oldValuesMap = map.putIfAbsent(cacheKey, valuesMap = new ConcurrentHashMap<>());//这里的putIfAbsent与put不同,put会直接替代,putIfAbsent是先判断是否含有值,如果有就返回对应值,如果没有就放入新值并返回null
//如果之前含有值就使用之前的。
if (oldValuesMap != null) {
valuesMap = oldValuesMap;
}
}
// 通过subKeyFactory的apply方法创建subKey
Object subKey = Objects.requireNonNull(subKeyFactory.apply(key, parameter));
// 通过subKey获取相应的值
Supplier<V> supplier = valuesMap.get(subKey);
Factory factory = null;
while (true) {
//缓存中有值且不为null就用get方法取出判空返回
if (supplier != null) {
// 根据之前的Factory中的操作,我们知道这里的supplier可能是Factory或CacheValue<V>类型
V value = supplier.get();
if (value != null) {
return value;
}
}
//缓存没有成功取到或是没有缓存时,使用Factory进行加载。
if (factory == null) {
//创建factory
factory = new Factory(key, parameter, subKey, valuesMap);
}
//不含缓存或取值为空的处理
if (supplier == null) {
supplier = valuesMap.putIfAbsent(subKey, factory);
if (supplier == null) {
//缓存加入成功,将最终的返回值设为factory在下一次循环时返回
supplier = factory;
}
} else {//之前含有缓存
if (valuesMap.replace(subKey, supplier, factory)) {//用新的factory进行替换
//成功替换后就将返回值设为factory在下一次循环时返回
supplier = factory;
} else {//替换失败则将其原来的值取出
supplier = valuesMap.get(subKey);
}
}
}
}
说到这里还有一个问题没有解决就是循环轮询的目的是啥,由于在实际使用时可能有多个线程对缓存中的值进行操作,所以使用轮询来不断的进行判断以获取最新的值,这里的get方法可以与factory中的get方法比较来看,就很好理解为啥在factory的get方法中要对取到的supplier进行判断了。
小结
WeakCache是实现Proxy类的关键一环,其中二级缓存的设计思想很有研究的价值,特别是一些内部类的设计与使用,都可以给我们之后的编码带来启发。