Java Cache系列之Guava Cache实现详解

Guava做为Google开源出来的工具库,Google本身对Guava的描述:The Guava project contains several of Google's core libraries that we rely on in our Java-based projects: collections, caching, primitives support, concurrency libraries, common annotations, string processing, I/O, and so forth.做为Google的core libraries,直接提供Cache实现,足以证实Cache应用的普遍程度。 然而做为工具库中的一部分,咱们天然不能期待Guava对Cache有比较完善的实现。于是Guava中的Cache只能用于一些把Cache做为一种辅助设计的项目或者在项目的前期为了实现简单而引入。

在Guava CacheBuilder的注释中给定Guava Cache如下的需求:
  1. automatic loading of entries into the cache
  2. least-recently-used eviction when a maximum size is exceeded
  3. time-based expiration of entries, measured since last access or last write
  4. keys automatically wrapped in WeakReference
  5. values automatically wrapped in WeakReference or SoftReference soft
  6. notification of evicted (or otherwise removed) entries
  7. accumulation of cache access statistics
对于这样的需求,若是要咱们本身来实现,咱们应该怎么设计?对于我来讲,对于其核心实现我会作以下的设计:
  1. 定义一个CacheConfig类用于纪录全部的配置,如CacheLoader,maximum size、expire time、key reference level、value reference level、eviction listener等。
  2. 定义一个Cache接口,该接口相似Map(或ConcurrentMap),可是为了和Map区别开来,于是从新定义一个Cache接口。
  3. 定义一个实现Cache接口的类CacheImpl,它接收CacheConfig做为参数的构造函数,并将CacheConfig实例保存在字段中。
  4. 在实现上模仿ConcurrentHashMap的实现方式,有一个Segment数组,其长度由配置的concurrencyLevel值决定。为了实现最近最少使用算法(LRU),添加AccessQueue和WriteQueue字段,这两个Queue内部采用双链表,每次新建立一个Entry,就将这个Entry加入到这两个Queue的末尾,而每读取一个Entry就将其添加到AccessQueue的末尾,没更新一个Entry将该Entry添加到WriteQueue末尾。为了实现key和value上的WeakReference、SoftReference,添加ReferenceQueue<K>类型的keyReferenceQueue和valueReferenceQueue字段。
  5. 在每次调用方法以前都遍历AccessQueue和WriteQueue,若是发现有Entry已经expire,就将该Entry从这两个Queue上和Cache中移除。而后遍历keyReferenceQueue和valueReference,若是发现有项存在,一样将它们移除。在移除时若是有EvictionListener注册着,则调用该listener。
  6. 对Segment实现,它时一个CacheEntry数组,CacheEntry是一个链节点,它包含hash、key、vlaue、next。CacheEntry根据是否须要包装在WeakReference中建立WeakEntry或StrongEntry,而对value根据是否须要包装在WeakReference、SoftReference中建立WeakValueReference、SoftValueReference、StrongValueReference。在get操做中对于须要使用CacheLoader加载的值先添加一个具备LoadingValueReference值的Entry,这样能够保证同一个Key只加载依次。在加载成功后将LoadingValueReference根据配置替换成其余Weak、Soft、Strong ValueReference。
  7. 对于cache access statistics,只须要有一个类在须要的地方作一些统计计数便可。
  8. 最后我必须得认可以上的设计有不少是对Guava Cache的参考,我有点后悔没有在看源码以前考虑这个问题,等看过之后思路就被它的实现给羁绊了。。。。

Guava Cache的数据结构
由于新进一家公司,要熟悉新公司项目以及项目用到的第三方库的代码,于是几个月来看了许多代码。而后愈来愈发现要理解一个项目的最快方法是先搞清楚该项目的底层数据结构,而后再去看构建于这些数据结构以上的逻辑就会容易许多。记得在仍是学生的时候,有在一本书上看到过一个大牛说的一句话:程序=数据结构+算法;当时对这句话并非和理解,如今是很赞同这句话,我对算法接触的很少,于是我更倾向于将这里的算法理解长控制数据流动的逻辑。于是咱们先来熟悉一下Guava Cache的数据结构。

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

ValueReference
对于ValueReference,由于Guava Cache支持强引用的Value、SoftReference Value以及WeakReference Value,于是它对应三个实现类:StrongValueReference、SoftValueReference、WeakValueReference。为了支持动态加载机制,它还有一个LoadingValueReference,在须要动态加载一个key的值时,先把该值封装在LoadingValueReference中,以表达该key对应的值已经在加载了,若是其余线程也要查询该key对应的值,就能获得该引用,而且等待改值加载完成,从而保证该值只被加载一次(能够在evict之后从新加载)。在该只加载完成后,将LoadingValueReference替换成其余ValueReference类型。对新建立的LoadingValueReference,因为其内部oldValue的初始值是UNSET,它isActive为false,isLoading为false,于是此时的LoadingValueReference的isActive为false,可是isLoading为true。每一个ValueReference都纪录了weight值,所谓weight从字面上理解是“该值的重量”,它由Weighter接口计算而得。weight在Guava Cache中由两个用途:1. 对weight值为0时,在计算由于size limit而evict是忽略该Entry(它能够经过其余机制evict);2. 若是设置了maximumWeight值,则当Cache中weight和超过了该值时,就会引发evict操做。可是目前还不知道这个设计的用途。最后,Guava Cache还定义了Stength枚举类型做为ValueReference的factory类,它有三个枚举值:Strong、Soft、Weak,这三个枚举值分别建立各自的ValueReference,而且根据传入的weight值是否为1而决定是否要建立Weight版本的ValueReference。如下是ValueReference的类图:  java

这里ValueReference之因此要有对ReferenceEntry的引用是由于在Value由于WeakReference、SoftReference被回收时,须要使用其key将对应的项从Segment的table中移除;copyFor()函数的存在是由于在expand(rehash)从新建立节点时,对WeakReference、SoftReference须要从新建立实例(我的感受是为了保持对象状态不会相互影响,可是不肯定是否还有其余缘由),而对强引用来讲,直接使用原来的值便可,这里很好的展现了对彼变化的封装思想;notifiyNewValue只用于LoadingValueReference,它的存在是为了对LoadingValueReference来讲能更加及时的获得CacheLoader加载的值。

ReferenceEntry
ReferenceEntry是Guava Cache中对一个键值对节点的抽象。和ConcurrentHashMap同样,Guava Cache由多个Segment组成,而每一个Segment包含一个ReferenceEntry数组,每一个ReferenceEntry数组项都是一条ReferenceEntry链。而且一个ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry数组项中组成的链,在一个Segment中,全部ReferenceEntry还组成access链(accessQueue)和write链(writeQueue),这两条都是双向链表,分别经过previousAccess、nextAccess和previousWrite、nextWrite字段连接而成。在对每一个节点的更新操做都会将该节点从新链到write链和access链末尾,而且更新其writeTime和accessTime字段,而没找到一个节点,都会将该节点从新链到access链末尾,并更新其accessTime字段。这两个双向链表的存在都是为了实现采用最近最少使用算法(LRU)的evict操做(expire、size limit引发的evict)。

Guava Cache中的ReferenceEntry能够是强引用类型的key,也能够WeakReference类型的key,为了减小内存使用量,还能够根据是否配置了expireAfterWrite、expireAfterAccess、maximumSize来决定是否须要write链和access链肯定要建立的具体Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。建立不一样类型的ReferenceEntry由其枚举工厂类EntryFactory来实现,它根据key的Strongth类型、是否使用accessQueue、是否使用writeQueue来决定不一样的EntryFactry实例,并经过它建立相应的ReferenceEntry实例。ReferenceEntry类图以下: 
算法

WriteQueue和AccessQueue 
为了实现最近最少使用算法,Guava Cache在Segment中添加了两条链:write链(writeQueue)和access链(accessQueue),这两条链都是一个双向链表,经过ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue连接而成,可是以Queue的形式表达。WriteQueue和AccessQueue都是自定义了offer、add(直接调用offer)、remove、poll等操做的逻辑,对于offer(add)操做,若是是新加的节点,则直接加入到该链的结尾,若是是已存在的节点,则将该节点连接的链尾;对remove操做,直接从该链中移除该节点;对poll操做,将头节点的下一个节点移除,并返回。 数组

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());
      // add to tail
      connectWriteOrder(head.getPreviousInWriteQueue(), entry);
      connectWriteOrder(entry, head);
      return true;
    }
    @Override
    public ReferenceEntry<K, V> peek() {
      ReferenceEntry<K, V> next = head.getNextInWriteQueue();
      return (next == head) ? null : next;
    }
    @Override
    public ReferenceEntry<K, V> poll() {
      ReferenceEntry<K, V> next = head.getNextInWriteQueue();
      if (next == head) {
        return null;
      }
      remove(next);
      return next;
    }
    @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(previous, next);
      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的配置(即没有expire time或size limit的evict策略)来讲,咱们可使用DISCARDING_QUEUE以节省内存:  数据结构

static final Queue<? extends Object> 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;
    }
....
  };

Segment中的evict
在解决了全部数据结构的问题之后,让咱们来看看LocalCache中的核心类Segment的实现,首先从evict开始。在Guava Cache的evict时机上,它没有使用另外一个后台线程每隔一段时间扫瞄一次table以evict那些已经expire的entry。而是它在每次操做开始和结束时才作一遍清理工做,这样能够减小开销,可是若是长时间不调用方法的话,会引发有些entry不能及时被evict出去。evict主要处理四个Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前两个queue是由于WeakReference、SoftReference被垃圾回收时加入的,清理时只须要遍历整个queue,将对应的项从LocalCache中移除便可,这里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference,要从LocalCache中移除须要有key,于是ValueReference须要有对ReferenceEntry的引用。这里的移除经过LocalCache而不是Segment是由于在移除时由于expand(rehash)可能致使原来在某个Segment中的ReferenceEntry后来被移动到另外一个Segment中了。而对后两个Queue,只须要检查是否配置了相应的expire时间,而后从头开始查找已经expire的Entry,将它们移除便可。有不一样的是在移除时,还会注册移除的事件,这些事件将会在接下来的操做调用注册的RemovalListener触发,这些代码比较简单,不详述。
在put的时候,还会清理recencyQueue,即将recencyQueue中的Entry添加到accessEntry中,此时可能会发生某个Entry实际上已经被移除了,可是又被添加回accessQueue中了,这种状况下,若是没有使用WeakReference、SoftReference,也没有配置expire时间,则会引发一些内存泄漏问题。recencyQueue在get操做时被添加,可是为何会有这个Queue的存在一直没有想明白。

Segment中的put操做
put操做相对比较简单,首先它须要得到锁,而后尝试作一些清理工做,接下来的逻辑相似ConcurrentHashMap中的rehash,不详述。须要说明的是当找到一个已存在的Entry时,须要先判断当前的ValueRefernece中的值事实上已经被回收了,由于它们能够时WeakReference、SoftReference类型,若是已经被回收了,则将新值写入。而且在每次更新时注册当前操做引发的移除事件,指定相应的缘由:COLLECTED、REPLACED等,这些注册的事件在退出的时候统一调用LocalCache注册的RemovalListener,因为事件处理可能会有很长时间,于是这里将事件处理的逻辑在退出锁之后才作。最后,在更新已存在的Entry结束后都尝试着将那些已经expire的Entry移除。另外put操做中还须要更新writeQueue和accessQueue的语义正确性。 app

V put(K key, int hash, V value, boolean onlyIfAbsent) {
      ....
        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)) {
            ValueReference<K, V> valueReference = e.getValueReference();
            V entryValue = valueReference.get();
            if (entryValue == null) {
              ++modCount;
              if (valueReference.isActive()) {
                enqueueNotification(key, hash, valueReference, 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();
              return null;
            } else if (onlyIfAbsent) {
              recordLockedRead(e, now);
              return entryValue;
            } else {
              ++modCount;
              enqueueNotification(key, hash, valueReference, RemovalCause.REPLACED);
              setValue(e, key, value, now);
              evictEntries();
              return entryValue;
            }
          }
        }
...
      } finally {
        ...
        postWriteCleanup();
      }
    }

Segment带CacheLoader的get操做
这部分的代码有点不知道怎么说了,大概上的步骤是:1. 先查找table中是否已存在没有被回收、也没有expire的entry,若是找到,并在CacheBuilder中配置了refreshAfterWrite,而且当前时间间隔已经操做这个事件,则从新加载值,不然,直接返回原有的值;2. 若是查找到的ValueReference是LoadingValueReference,则等待该LoadingValueReference加载结束,并返回加载的值;3. 若是没有找到entry,或者找到的entry的值为null,则加锁后,继续table中已存在key对应的entry,若是找到而且对应的entry.isLoading()为true,则表示有另外一个线程正在加载,于是等待那个线程加载完成,若是找到一个非null值,返回该值,不然建立一个LoadingValueReference,并调用loadSync加载相应的值,在加载完成后,将新加载的值更新到table中,即大部分状况下替换原来的LoadingValueReference。

Segment中的其余操做
其余操做包括不含CacheLoader的get、containsKey、containsValue、replace等操做逻辑重复性很大,并且和ConcurrentHashMap的实现方式也相似,不在详述。

Cache StatsCounter和CacheStats
为了纪录Cache的使用状况,若是命中次数、没有命中次数、evict次数等,Guava Cache中定义了StatsCounter作这些统计信息,它有一个简单的SimpleStatsCounter实现,咱们也能够经过CacheBuilder配置本身的StatsCounter。 ide

public interface StatsCounter {
    public void recordHits(int count);
    public void recordMisses(int count);
    public void recordLoadSuccess(long loadTime);
    public void recordLoadException(long loadTime);
    public void recordEviction();

    public CacheStats snapshot();
  }

在获得StatsCounter实例后,可使用CacheStats获取具体的统计信息: 函数

public final class CacheStats {
  private final long hitCount;
  private final long missCount;
  private final long loadSuccessCount;
  private final long loadExceptionCount;
  private final long totalLoadTime;
  private final long evictionCount;

}

同ConcurrentHashMap,在知道Segment实现之后,其余的方法基本上都是代理给Segment内部方法,于是在LocalCache类中的其余方法看起来就比较容易理解,不在详述。然而Guava Cache并无将ConcurrentMap直接提供给用户使用,而是为了区分Cache和Map,它自定义了一个本身的Cache接口和LoadingCache接口,咱们能够经过CacheBuilder配置不一样的参数,而后使用build()方法返回一个Cache或LoadingCache实例: 工具

public interface Cache<K, V> {
  V getIfPresent(Object key);
  V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
  ImmutableMap<K, V> getAllPresent(Iterable<?> keys);
  void put(K key, V value);
  void putAll(Map<? extends K,? extends V> m);
  void invalidate(Object key);
  void invalidateAll(Iterable<?> keys);
  void invalidateAll();
  long size();
  CacheStats stats();
  ConcurrentMap<K, V> asMap();
  void cleanUp();
}

public interface LoadingCache<K, V> extends Cache<K, V>, Function<K, V> {
  V get(K key) throws ExecutionException;
  V getUnchecked(K key);
  ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException;
  V apply(K key);
  void refresh(K key);
  ConcurrentMap<K, V> asMap();
}
相关文章
相关标签/搜索