Guava cache中涉及到数据结构

  .Cache类似于Map,它是存储键值对的集合,然而它和Map不同的是它还需要处理evict,expire,dynamic load等逻辑,需要一些额外信息来实现这些操作。在面向对象思想中,经常使用类对一些关联性比较强的数据做封装,同时把操作这 数据相关的操作放到该类中。因而Guava Cache使用ReferenceEntry接口来封装一个键值对,而用ValueReference来封装值值。这里之所以用参考命令,是因为Guava Cache要支持WeakReference Key和SoftReference,WeakReference价值。

ValueReference

  对于ValueReference,因为Guava Cache支持StrongReference的值,SoftReference值以及WeakReference值,因而它对应三个实现类:StrongValueReference,SoftValueReference,WeakValueReference。为了支持动态加载机制,它还有一个LoadingValueReference,在需要动态加载一个键的值时,先把该值封装在LoadingValueReference中,以表达该键对应的值已经在加载了,如果其他线程也要查询该键对应的值,就能得到该引用,并且等待改值加载完成,从而保证该值只被加载一次(可以在逐出以后重新加载)。在该只加载完成后,将LoadingValueReference替换成其他ValueReference类型。对新创建的LoadingValueReference,由于其内部的属性oldValue的初始值是未设置,它isActive为假,isLoading为假,因而此时的LoadingValueReference的isActive为假,但是isLoading为真。每个ValueReference都纪录了重量值,所谓重量从字面上理解是“该值的重量 “,它由Weighter接口计算而得.weight在Guava Cache中由两个用途:1。对称值为0时,在计算因为大小限制而evict是忽略该条目(它可以通过其他机制逐出)。
在这里插入图片描述
  这里ValueReference之所以要有对ReferenceEntry的引用是因为在价值因为了WeakReference,SoftReference的被回收时,需要使用其密钥将对应的项从段的表中移除; copyFor()函数的存在是因为在膨胀(刷新)重新创建节点时,对了WeakReference,SoftReference的需要重新创建实例(个人感觉是为了保持对象状态不会相互影响,但是不确定是否还有其他原因),而对强引用来说,直接使用原来的值即可,这里很好的展示了对彼变化的封装思想; notifiyNewValue只用于LoadingValueReference,它的存在是为了对LoadingValueReference来说能更加及时的得到的CacheLoader加载的值。

ReferenceEntry

  ReferenceEntry是Guava Cache中对一个键值对节点的抽象。和ConcurrentHashMap一样,Guava Cache由多个Segment组成,而每个Segment包含一个ReferenceEntry数组,每个ReferenceEntry数组项都是一条ReferenceEntry链。并且一个ReferenceEntry包含键,哈希,valueReference,接下来的字段。除了在ReferenceEntry数组项中组成的链,在一个段中,所有ReferenceEntry还组成访问链(accessQueue)和写链(writeQueue),这两条都是双向链表,分别通过previousAccess,nextAccess和previousWrite,nextWrite字段链接而成。在对每个节点的更新操作都会将该节点重新链到写链和访问链末尾,并且更新其writeTime和accessTime字段,而没找到一个节点,都会将该节点重新链到访链末尾,并更新其accessTime字段。这两个双向链表的存在都是为了实现采用最近最少使用算法(LRU)的evict操作(expire,size limit引起的evict)。
  Guava Cache中的ReferenceEntry可以是强引用类型的key,也可以WeakReference类型的密钥,为了减少内存使用量,还可以根据是否配置了expireAfterWrite,expireAfterAccess,maximumSize来决定是否需要写链接和访问链确定要创建的具体参考:StrongEntry,StrongWriteEntry,StrongAccessEntry,StrongWriteAccessEntry等创建不同类型的ReferenceEntry由其枚举工厂类EntryFactory来实现,它根据关键的Strongth类型,是否使用accessQueue,是否使用writeQueue来决定不同的EntryFactry实例,并通过它创建相应的ReferenceEntry实例.ReferenceEntry类图如下:
在这里插入图片描述

WriteQueue和AccessQueue

  为了实现最近最少使用算法,Guava Cache在Segment中添加了两条链:写链(writeQueue)和访问链(accessQueue),这两条链都是一个双向链表,通过ReferenceEntry中的previousInWriteQueue,nextInWriteQueue和previousInAccessQueue,nextInAccessQueue链接而成,但是以队列的形式表 达.WriteQueue和AccessQueue都是自定义了报价,加(直接调用报价),删除,民意调查等操作的逻辑,对于报价(添加)操作,如果是新加的节点,则直接加入到该链的结尾,如果是已存在的节点,则将该节点链接的链尾;对除去操作,直接从该链中移除该节点;对轮询操作,将头节点的下一个节点移除,并返回。

static  final  class  WriteQueue <K,V>  extends  AbstractQueue <ReferenceEntry <K,V >> { 
    final  ReferenceEntry <K,V> head =  new  AbstractReferenceEntry <K,V>().... 
    @ Override 
    public  boolean  offer(ReferenceEntry < K,V> entry){ 
      //  unlink
      connectWriteOrder(entry.getPreviousInWriteQueue(),entry.getNextInWriteQueue()); 
      //  添加到tail
      connectWriteOrder(head.getPreviousInWriteQueue(),entry); 
      connectWriteOrder(entry,head); 
      返回 true ; 
    } 
    @Override 
    public  ReferenceEntry <K,V> peek(){
      ReferenceEntry <K,V> next = head.getNextInWriteQueue(); 
      返回  (下一个==头)? null  :next; 
    } 
    @Override 
    public  ReferenceEntry <K,V> poll(){ 
      ReferenceEntry <K,V> next = head.getNextInWriteQueue(); 
      if  (next == head){ 
        return  null ; 
      } 
      删除(下); 
      返回  ; 
    } 
    @Override 
    public  boolean  remove(Object o){ 
      ReferenceEntry <K,V> e =(ReferenceEntry)o; 
      ReferenceEntry <K,V> previous = e.getPreviousInWriteQueue();
      ReferenceEntry <K,V> next = e.getNextInWriteQueue(); 
      connectWriteOrder(上一个,下一个); 
      nullifyWriteOrder(E); 
      return  next!= NullEntry.INSTANCE; 
    } 
    @Override 
    public  boolean  contains(Object o){ 
      ReferenceEntry <K,V> e =(ReferenceEntry)o; 
      return  e.getNextInWriteQueue()!= NullEntry.INSTANCE; 
    } 
.... 
  }

  对于不需要维护WriteQueue和AccessQueue的配置(即没有过期时间或大小限制的evict策略)来说,我们可以使用DISCARDING_QUEUE以节省内存:

static final Queue<?> DISCARDING_QUEUE =
      new AbstractQueue<Object>() {
        @Override
        public boolean offer(Object o) {
          return true;
        }

        @Override
        public Object peek() {
          return null;
        }

        @Override
        public Object poll() {
          return null;
        }

        @Override
        public int size() {
          return 0;
        }

        @Override
        public Iterator<Object> iterator() {
          return ImmutableSet.of().iterator();
        }
      };

Segment中的evict

  在解决了所有数据结构的问题以后,让我们来看看LocalCache中的核心类Segment的实现,首先从evict开始。在Guava Cache的evict时机上,它没有使用另一个后台线程每隔一段时间扫瞄一次表以逐出那些已经过期的条目。而是它在每次操作开始和结束时才做一遍清理工作,这样可以减少开销,但是如果长时间不调用方法的话,会引起有些条目不能及时被逐出出去.evict主要处理四个队列:1。keyReferenceQueue; 2。valueReferenceQueue; 3。writeQueue; 4。accessQueue。前两个队列是因为了WeakReference,SoftReference的被垃圾回收时加入的,清理时只需要遍历整个队列,将对应的项从LocalCache中移除即可,这里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference,要从LocalCache中移除需要有重点,因而ValueReference需要有对ReferenceEntry的引用。这里的移除通过LocalCache而不是分段是因为在移除时因为扩大(刷新)可能导致原来在某个段中的ReferenceEntry后来被移动到另一个段中了。而对后两个队列,只需要检查是否配置了相应的到期时间,然后从头开始查找已经到期的条目,将它们移除即可。有不同的是在移除时,还会注册移除的事件,这些事件将会在接下来的操作调用注册的RemovalListener触发,这些代码比较简单,不详述。
在放的时候,还会清理recencyQueue,即将recencyQueue中的条目添加到accessEntry中,此时可能会发生某个条目实际上已经被移除了,但是又被添加回accessQueue中了,这种情况下,如果没有使用WeakReference,SoftReference,也没有配置expire时间,则会引起一些内存泄漏问题.recencyQueue在获取操作时被添加,但是为什么会有这个Queue的存在一直没有想明白?

Segment 中的put操作

@Nullable
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
        lock();
        try {
            long now = map.ticker.read();
            preWriteCleanup(now);

            int newCount = this.count + 1;
            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 (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 (entryValue == null) {
                        ++modCount;
                        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
                        evictEntries(e);
                        return null;
                    } else if (onlyIfAbsent) {
                        // Mimic
                        // "if (!map.containsKey(key)) ...
                        // else return map.get(key);
                        recordLockedRead(e, now);
                        return entryValue;
                    } else {
                        // clobber existing entry, count remains unchanged
                        ++modCount;
                        enqueueNotification(key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
                        setValue(e, key, value, now);
                        evictEntries(e);
                        return entryValue;
                    }
                }
            }

            // 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();
        }
    }

Segment的CacheLoader的获取操作

  这部分的代码有点不知道怎么说了,大概上的步骤是:1。先查找表中是否已存在没有被回收,也没有过期的条目,如果找到,并在CacheBuilder中配置了refreshAfterWrite,并且当前时间间隔已经操作这个事件,则重新加载值,否则,直接返回原有的值2。如果查找到的ValueReference是LoadingValueReference,则等待该LoadingValueReference加载结束,并返回加载的值; 3。如果没有找到入口,或者找到的条目的值为空,则加锁后,继续表中已存在键对应的条目,如果找到并且对应的entry.isLoading()为真,则表示有另一个线程正在加载,因而等待那个线程加载完成,如果找到一个非空值,返回该值,否则创建一个LoadingValueReference,并调用loadSync加载相应的值,在加载完成后,将新加载的值更新到表中,即大部分情况下替换原来的LoadingValueReference。

V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
        checkNotNull(key);
        checkNotNull(loader);
        try {
            if (count != 0) { // read-volatile
                // don't call getLiveEntry, which would ignore loading values
                ReferenceEntry<K, V> e = getEntry(key, hash);
                if (e != null) {
                    long now = map.ticker.read();
                    V value = getLiveValue(e, now);
                    if (value != null) {
                        recordRead(e, now);
                        statsCounter.recordHits(1);
                        return scheduleRefresh(e, key, hash, value, now, loader);
                    }
                    ValueReference<K, V> valueReference = e.getValueReference();
                    if (valueReference.isLoading()) {
                        return waitForLoadingValue(e, key, valueReference);
                    }
                }
            }

            // at this point e is either null or expired;
            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();
        }
    }

段中的其他操作

其他操作包括不含CacheLoader的get,containsKey,containsValue,replace等操作逻辑重复性很大,而且和ConcurrentHashMap的实现方式也类似,不在详述。

.Cache StatsCounter和CacheStats

为了纪录Cache的使用情况,如命中次数,没有命中次数,evict次数等,Guava Cache中定义了StatsCounter做这些统计信息。
AbstractCache.java

public interface StatsCounter {
	//code segment
}
public static final class SimpleStatsCounter implements StatsCounter {
	// code segment
}

还有CacheStats类
  和ConcurrentHashMap,在知道Segment实现以后,其他的方法基本上都是代理给Segment内部方法,因而在LocalCache类中的其他方法看起来就比较容易理解,不在详述。然而Guava Cache并没有将ConcurrentMap直接提供给用户使用,而是为了区分缓存和地图,它自定义了一个自己的高速缓存接口和LoadingCache接口,我们可以通过CacheBuilder配置不同的参数,然后使用建设()方法返回一个缓存或LoadingCache实例。


参考:http://www.blogjava.net/DLevin/archive/2013/10/20/404847.html https://www.jianshu.com/p/38bd5f1cf2f2 ----未看

猜你喜欢

转载自blog.csdn.net/m0_37941483/article/details/88182300