我们与库开发者,代码质量的距离,优秀的代码
浅谈 ImmutableMap 的设计
通过阅读jdk中的Map接口,发现好的库代码,从接口就设计的很好,这里的很好是指,能够以接口对接口的理念来设计整个协作过程。
然后学习点,泛型的广泛使用,如何进行高层次代码复用,很多时候依靠好的设计和泛型代码的设计
public static <K extends Comparable<? super K>, V> Comparator<Map.Entry<K,V>> comparingByKey() {
return (Comparator<Map.Entry<K, V>> & Serializable)
(c1, c2) -> c1.getKey().compareTo(c2.getKey());
}
这里可以看到 这里的泛型 K extends Comparable<? super K> 这块是值得学习的,加紧收缩了K 的限制,需要K 实现 Compareable<? super K> 接口, 然后为啥是 ? super K, 因为K父类的Compareable接口,K也是继承了的,不用每次K都要实现,父类实现也算。这里其实就是泛型接口设计的加紧收缩, 通配符 代表K 通常可以加入更多的限制表示更严格的限制,如 extends 某个接口的实现,接口里面也是用泛型实现的接口。
public static <T>
Collector<T, ?, List<T>> toList() {
return new CollectorImpl<>((Supplier<List<T>>) ArrayList::new, List::add,
(left, right) -> { left.addAll(right); return left; },
CH_ID);
}
可以看到这里new方法是构造器函数,然后把它抽象为,Supplier<List> 接口,这种写法比较高度抽象,比较functional 值得学习下。
-
是通过接口里面的默认方法,来实现一些既定逻辑,从而减少实现类的一些实现负担,实现较高度的代码节省和复用。像Map接口里面有一些既定的默认方法,实现类可以直接用,也可以复写。
-
从jdk Collector 源码中可以看到,jdk源码开发者水平真是远超普通程序员,他们通过接口的设计和组合就可以完成类库底层的设计。这等抽象能力,没有丰富的思考和经验恐难实现。
-
结论是,没有读懂类库里面,通过接口的配合完成一整套功能的设计。先留着。
guava cache阅读
- 枚举里面可以放接口,枚举也可以实现接口
enum Strength {
/*
* TODO(kevinb): If we strongly reference the value and aren't loading, we needn't wrap the
* value. This could save ~8 bytes per entry.
*/
STRONG {
@Override
<K, V> ValueReference<K, V> referenceValue(
Segment<K, V> segment, ReferenceEntry<K, V> entry, V value, int weight) {
return (weight == 1)
? new StrongValueReference<K, V>(value)
: new WeightedStrongValueReference<K, V>(value, weight);
}
@Override
Equivalence<Object> defaultEquivalence() {
return Equivalence.equals();
}
},
SOFT {
@Override
<K, V> ValueReference<K, V> referenceValue(
Segment<K, V> segment, ReferenceEntry<K, V> entry, V value, int weight) {
return (weight == 1)
? new SoftValueReference<K, V>(segment.valueReferenceQueue, value, entry)
: new WeightedSoftValueReference<K, V>(
segment.valueReferenceQueue, value, entry, weight);
}
@Override
Equivalence<Object> defaultEquivalence() {
return Equivalence.identity();
}
},
}
- 又回到guava 源码设计的阅读了,本文继续解析思考高水平代码设计
还是从小点开始出发吧,小的设计到大的设计,不然一头扎进整体设计,源码着实看得捉急。
LocalCache 的putValue开始看起
2.1 其中一个设计,经常使用的回调模型,我们可以使用带队列的事件存储模型来进行事件回调事件的缓存和存储。
例子:
/**
* 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);
}
}
}
这个是每次进行 增删查改 cache后,要进行对外的一些回调事件的通知,guava把它存储进队列里面,removalListener 由外部设置,然后这个清理回调的通知,自然是在缓存更改完毕以后,guava的代码里面把它放进了, finally{} 模块,finally 进行 unlock(),和进行事件对外通知回调。 这个可以学习的点是,首先回调事件可以存储进队列, 回调事件通知里面可以有多种类型的设计。就是这个 RemovalNotification 的设计,里面存储了缓存更新的事件和类型。
2.2 第二个原则,也是之前提过的,把集中管理代码分散到各个组件来写。例如真正进行 put 操作, guava 里面的也是放进 Segment里面来处理的。
接下来也是细节处的阅读了
guava cache 如何做,缓存回收,和强弱value 引用的设计
先思考,我们做缓存设计的时候,会进行的思考模式,和库开发者做缓存设计的时候,和他们的区别。
- 我们做强弱引用或者cache大小限制的时候,可能会利用现成数据结构容器来做,例如HashMap,WeakHashMap(弱引用)等等,这样的话,垃圾回收一些弱引用的细节就不能实现很好的监听。
- 而guava cache直接继承 ConcurrentMap 接口方式来进行cache库的设计,没有用现成数据结构容器接口,这需要对类库有相当的把握。下面进行一些简单的分析。
- 了解过ConcurrentHashMap的源码设计的知道,它里面进行线程安全和容器放置是通过 叫Segment的方式来进行,每个Segment继承了RetreenLock接口,Segment类似于一个桶,这个桶由于继承于RetreenLocck接口,有自己的锁机制。放置value时候也会进行,每个桶里面的value的compare 操作。
- guava cache里面也有着自己的Segment 数组,原理和ConcurrentHashMap类似,也是利用了数据 hash ,进行分段锁的操作。然后 它的Segment里面保存的 链表数据结构是
@GwtIncompatible
interface ReferenceEntry<K, V> {
/** Returns the value reference from this entry. */
ValueReference<K, V> getValueReference();
/** Sets the value reference for this entry. */
void setValueReference(ValueReference<K, V> valueReference);
/** Returns the next entry in the chain. */
@Nullable
ReferenceEntry<K, V> getNext();
/** Returns the entry's hash. */
int getHash();
/** Returns the key for this entry. */
@Nullable
K getKey();
/*
// 省略一些内容
可以看到除了简单获取值外,有着比较丰富的接口拿到其他的信息,然后它进行cache的东西是ValueReference<K, V> ,仅仅是在存value的时候用,这个ValueReference 的设计是为了,让Value也能用上 弱引用,能让value自动过期。
例子:
/** References a soft value. */
static class SoftValueReference<K, V> extends SoftReference<V> implements ValueReference<K, V> {
final ReferenceEntry<K, V> entry;
// 略
通过这个ValueReference<K, V> 接口的抽象,value的引用形态可以具现化为多种,根据cache配置来实现不同的配置,如下代码(可以看到通过enum来进行接口抽象形态配置也是可行的)
enum Strength {
/*
* TODO(kevinb): If we strongly reference the value and aren't loading, we needn't wrap the
* value. This could save ~8 bytes per entry.
*/
STRONG {
@Override
<K, V> ValueReference<K, V> referenceValue(
Segment<K, V> segment, ReferenceEntry<K, V> entry, V value, int weight) {
return (weight == 1)
? new StrongValueReference<K, V>(value)
: new WeightedStrongValueReference<K, V>(value, weight);
}
@Override
Equivalence<Object> defaultEquivalence() {
return Equivalence.equals();
}
},
SOFT {
@Override
<K, V> ValueReference<K, V> referenceValue(
Segment<K, V> segment, ReferenceEntry<K, V> entry, V value, int weight) {
return (weight == 1)
? new SoftValueReference<K, V>(segment.valueReferenceQueue, value, entry)
: new WeightedSoftValueReference<K, V>(
segment.valueReferenceQueue, value, entry, weight);
}
@Override
Equivalence<Object> defaultEquivalence() {
return Equivalence.identity();
}
},
WEAK {
@Override
<K, V> ValueReference<K, V> referenceValue(
Segment<K, V> segment, ReferenceEntry<K, V> entry, V value, int weight) {
return (weight == 1)
? new WeakValueReference<K, V>(segment.valueReferenceQueue, value, entry)
: new WeightedWeakValueReference<K, V>(
segment.valueReferenceQueue, value, entry, weight);
}
@Override
Equivalence<Object> defaultEquivalence() {
return Equivalence.identity();
}
};
/** Creates a reference for the given value according to this value strength. */
abstract <K, V> ValueReference<K, V> referenceValue(
Segment<K, V> segment, ReferenceEntry<K, V> entry, V value, int weight);
/**
* Returns the default equivalence strategy used to compare and hash keys or values referenced
* at this strength. This strategy will be used unless the user explicitly specifies an
* alternate strategy.
*/
abstract Equivalence<Object> defaultEquivalence();
}
然后这个 ReferenceEntry ,就是所谓的存的key-value pair,通过接口也能获取到缓存情况里面的一些其他信息。
put方法里面设置entry后,更新回调旧的value值后,记录accessQueue和writeQueue后,然后会判断当前LocalCache容器大小,然后进行删除旧值操作。我们来看下这个操作。
这个方法在
/**
* 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;
}
drainRecencyQueue();
// 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();
}
}
while (totalWeight > maxSegmentWeight) {
ReferenceEntry<K, V> e = getNextEvictable();
if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
throw new AssertionError();
}
}
}
排除掉一些次要逻辑的影响,我们看到 怎么判断Segment容量是否大于预期,直接是通过 每个ValueRefrence 的getWeight 来取每个Value占用的内存大小(是抽象意义上的个数大小)。然后先判断 新值占用的Weight是否大于maxSegmentWeight,如果是,需要进行删除这个entry的判断操作。
然后如果整体容量totalWeight (累加得到的值大于 maxSegmentWeight)的限制,来看看 getNextEvictable 是怎样的方式来取得需要被淘汰的entry进行删除,
// TODO(fry): instead implement this with an eviction head
@GuardedBy("this")
ReferenceEntry<K, V> getNextEvictable() {
for (ReferenceEntry<K, V> e : accessQueue) {
int weight = e.getValueReference().getWeight();
if (weight > 0) {
return e;
}
}
throw new AssertionError();
}
可以看到是从前到后遍历这个accessQueue,最前面的是最早被访问的,猜测是LRU算法的一个实现方式。这种淘汰策略也比较合理。查看这个accessQueue 的插入更新原则。
这里简谈下,这个accessQueue,这里作者也是采用重新设计数据结构的方式,真的是用的炉火纯青,可以说是,我们看下这个AccessQueue的设计。
static final class AccessQueue<K, V> extends AbstractQueue<ReferenceEntry<K, V>> {
final ReferenceEntry<K, V> head =
new AbstractReferenceEntry<K, V>() {
@Override
public long getAccessTime() {
return Long.MAX_VALUE;
}
@Override
public void setAccessTime(long time) {}
ReferenceEntry<K, V> nextAccess = this;
@Override
public ReferenceEntry<K, V> getNextInAccessQueue() {
return nextAccess;
}
@Override
public void setNextInAccessQueue(ReferenceEntry<K, V> next) {
this.nextAccess = next;
}
ReferenceEntry<K, V> previousAccess = this;
@Override
public ReferenceEntry<K, V> getPreviousInAccessQueue() {
return previousAccess;
}
@Override
public void setPreviousInAccessQueue(ReferenceEntry<K, V> previous) {
this.previousAccess = previous;
}
};
// implements Queue
@Override
public boolean offer(ReferenceEntry<K, V> entry) {
// unlink
connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue());
// 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);
}
这里直接看offer函数, queue里面的add函数调的也是offer函数, 这里看到每次插入一个新
entry,先把这个entry的头部和next链接,然后把header 的pre 和这个 entry最尾部链接,然后把这个entry的pre 和 header 头部链接,实际上这里就形成了一个环,每次迭代的时候,看其迭代器的实现
@Override
public Iterator<ReferenceEntry<K, V>> iterator() {
return new AbstractSequentialIterator<ReferenceEntry<K, V>>(peek()) {
@Override
protected ReferenceEntry<K, V> computeNext(ReferenceEntry<K, V> previous) {
ReferenceEntry<K, V> next = previous.getNextInAccessQueue();
return (next == head) ? null : next;
}
};
}
每次迭代的时候,最先迭代到的其实是最早没有被引用的value,是LRU实现的一种,这种通过环形数据链表实现了LRU算法的实现,的确是巧妙,比起我们常用的(proportyQueue记录次数的方式比,到底层实现不少,不愧是类库实现者的实现,就是跟用高层的数据结构常规实现不走同一套,就是要用自己的数据结构来实现这个逻辑)
到这里 put方法基本就讲完了,最后Unlock然后做回调通知。
至于后面其他的方法,下次再说了。(LocalCache这个类就将近五千行,这个作者也是不喜欢多写几个类文件,类文件太长看得累。)