Guava Cache:原理详解和源码分析

目录

一、基础信息

1、版本

2、构造方式

3、核心参数

二、基本原理

1、数据结构

1.1 分段segment的数量

1.2 分段segment的定位

1.3 原子数组table

1.4 链表节点ReferenceEntry

2、并发控制

3、缓存淘汰

3.1 用户显式清除的两种场景

3.2 被垃圾回收淘汰

3.3 超时淘汰

3.4 容量超限淘汰(LRU算法)

三、核心源码:缓存淘汰

1、基本原理

2、相关实现

3、写方法缓存回收

4、访问后缓存回收

5、缓存刷新

四、核心源码:主要流程

1、构造缓存

1.1 Cache的实现类

1.2 LoadingCache的构造

1.3 LocalCache的构造

2、查询缓存

2.1 主要方式

2.2 基本流程

2.3 步骤详解:查询和刷新

2.4 步骤详解:加锁并加载

3、添加/更新缓存

3.1 主要方式

3.2 源码分析

3.3 扩容

3.4 驱逐缓存


    建议对Guava Cache完全不了解的同学,先看一下Guava Cache核心参数及其原理的讲解,先了解一些关键的节点,再看下面的原理分析,可能更轻松一些:Guava Cache:核心参数深度剖析和相关源码分析

一、基础信息

1、版本

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>29.0-jre</version>
</dependency>

2、构造方式

3、核心参数

    核心参数详解参考:Guava Cache:核心参数深度剖析和相关源码分析

1、容量

  • initialCapacity:初始容量;
  • maximumSize:最大容量;
  • maximumWeight:最大权重,每条缓存的默认权重为1,可以增加单挑缓存的权重;
  • weither:权重器,用于衡量不同缓存条目的权重。

2、超时时长

  • expireAfterAccess:超时计算方式:在访问请求之后重置超时计时器;
  • expireAfterWrite:超时计算方式:在写请求之后重置超时计时器。

3、刷新

  • refreshAfterWrite:写操作后多久刷新缓存内容,刷新使用下面的加载器;
  • build(CacheLoader loader):构造LoadingCache实例,入参是用于刷新的加载器;
  • removalListener:移除监听器,接收条目被移除的通知,可以过滤需要的缓存条目,进行相应处理。

4、引用强度

  • weakValues:指定所有的value都是弱引用;
  • weakKeys:指定所有的key都是弱引用;
  • softValues:指定所有的key都是软引用。

5、其他

  • concurrencyLevel:并发级别,级别越高支持的最大并发数越大;
  • recordStats:启用缓存统计,即缓存操作期间的性能相关的统计;
  • ticker:指定纳秒精度的时间源,默认使用System.nanoTime()。

二、基本原理

1、数据结构

    Guava Cache的数据结构,和JDK 1.7版本的ConcurrentHashMap非常相似:

  • 分段segment:最外层是分段segment,用于控制最大的写并发数量;
  • 分段内的数组table:每个分段内维护一个原子引用数组table,根据元素的hash值确定在数组中的位置;
  • 数组内的链表:数组的任一元素,存放的都是一个链表,用于解决哈希碰撞的情况;
  • 和JDK 1.7的ConcurrentHashMap的一个重要区别在于,Guava Cache的数组中始终存放的都是链表,不会变成红黑树。

1.1 分段segment的数量

    分段segment是缓存工具Cache的最外层结构。一个缓存可能会有多个segment,所有segment的内容之和,表示整个缓存。

    segmentCount的值主要取决于建造者类CacheBuilder的参数并发级别concurrencyLevel,另外还会受到CacheBuilder参数最大加权值maximumWeight

    详情参考: Guava Cache:核心参数深度剖析和相关源码分析 # 并发级别concurrencyLevel

    分段数量segmentCount的取值规则简述:

  • segmentCount是2的整数倍;
  • segmentCount在允许的取值范围内取最大值;
  • concurrencyLevel的约束:1/2 * segmentCount满足:小于concurrencyLevel ;
  • maxWeight的约束:如果maxWeight < 0(不限制缓存最大容量),则对segmentCount无影响;如果设置了有效的maxWeight,则 1/2 * segmentCount 小于等于1/20 * maxWeight

    分段数量segmentCount的计算代码(取自LocalCache.LocalLoadingCache的构造器):


    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      // 这时的segmentShift还是表示segmentCount是2的多少次幂
      ++segmentShift;
      // segmentCount是满足while条件的最大值的2倍
      segmentCount <<= 1;
    }
    // 最终的segmentShift用于取hash的高位的相应位数,用来计算寻找一个元素在哪个segment中
    this.segmentShift = 32 - segmentShift;
    // 掩码,取hash的低位的相应位数的值,即为在segment中的角标
    segmentMask = segmentCount - 1;
 
 
  // 是否限制容量
  boolean evictsBySize() {
    return maxWeight >= 0;
  }

1.2 分段segment的定位

    如何找到一条缓存唯一对应的segment,是缓存使用过程中首先要考虑的。无论是缓存更新还是缓存查询,必须首先先根据缓存的key,匹配到一个segment,才能进行后续的操作。

1、相关成员:

    Guava Cache是通过高位哈希的原理给缓存分配segment的。通过1.1分段segment的数量 可知,缓存工具Cache中会维护一些跟segment相关的参数,包括:

  • segmentCount:segment的数量;
  • segmentShift:计算segment使用的位数。指的是段的数量是1通过左移多少位计算得到的,再让32减去这个数字。例如segmentCount = 4的情况,segmentShift就是 32 - 2 = 30,表示计算segment的时候,使用hash值的30~32位;
  • segmentMask:计算segment使用的掩码。segmentMask所有位的值都是1,它位数等于32 - segmentShift的值。例如segmentCount = 4的情况,segmentMask就是111。

2、分段segment的定位方式:

  1. Cache中维护着一个segment的数组segments,数组的大小是segmentCount;
  2. Cache计算缓存key的hash值;
  3. Cache让hash右移segmentShift位,得到hash从第segmentShift位开始的高位的值(例如当segmentCount = 4时,segmentShift=30,此时会右移30位,得到从30~32位的值);
  4. Cache将得到的hash高位的值,和掩码segmentMask相与(例如当segmentCount = 4时,segmentMask = 111,此时相与得到的结果就是hash的30~32位的值本身);
  5. 上一步得到的相与结果,就是Cache中维护的segment数组segments中的角标。

    注意事项:

  • 之所以有第四步和segmentMask相与的操作,是为了保证计算得到的segments的角标不会数组越界,因为segmentMask实际上就是segments中最大的角标;
  • 这里计算segment,之所以使用hash的高位,是因为在segment中的数组中定位元素,使用的hash的低位。两者分开,可以让缓存在segments中的分布和缓存在segment的table中的分布互相正交,减少哈希碰撞导致的性能较差的情况。

3、代码:

  /**
   * Returns the segment that should be used for a key with the given hash.
   *
   * @param hash the hash code for the key
   * @return the segment
   */
  Segment<K, V> segmentFor(int hash) {
    // TODO(fry): Lazily create segments?
    return segments[(hash >>> segmentShift) & segmentMask];
  }

1.3 原子数组table

    segment中维护了一个原子引用数组AtomicReferenceArray的实例table。table类似于一个数组,数组中的不同元素的hash值是不同的。

    segment通过hash的低位,定位缓存在table中的位置。用key的hash值,和table的长度-1,进行与运算,得到的数值就是key在数组table中的位置。

    /** Returns first entry of bin for given hash. */
    ReferenceEntry<K, V> getFirst(int hash) {
      // read this volatile field only once
      AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
      return table.get(hash & (table.length() - 1));
    }

1.4 链表节点ReferenceEntry

    table中维护的是一个链表,链表的每个节点的类型,是ReferenceEntry。

    ReferenceEntry是个接口,有多个实现类。Guava Cache会构造时设置的引用强度,选择相应的实现类:

2、并发控制

    并发控制是通过参数并发级别concurrencyLevel设置的,还会受参数最大加权值maximumWeight的影响,最终通过分段segment的数量来起作用。分段数量的计算规则参考上述1.1分段segment的数量

    缓存的写操作(包括显式的写操作,以及读操作触发的缓存失效、缓存加载等)是需要加锁的,而加锁的基本单元是segment。类Segment是Java的重入锁ReentrantLock的子类。在写操作前后会分别调用ReentrantLock的lock()和unlock()方法,进行加锁和解锁操作。

3、缓存淘汰

    Guava Cache定义了枚举类RemovalCause,用来标识缓存被移除的原因。这里通过分析RemovalCause的枚举值,来分析不同的缓存淘汰原因。

    枚举类RemovalCause要求枚举值分别实现wasEvicted()方法,表示缓存是否是被淘汰的,而不是被用户显式清除的。

  /**
   * Returns {@code true} if there was an automatic removal due to eviction (the cause is neither
   * {@link #EXPLICIT} nor {@link #REPLACED}).
   */
  abstract boolean wasEvicted();

3.1 用户显式清除的两种场景

    如果用户通过invalite或invalidateAll等方法主动失效了相应缓存,或者通过put方法使用新值替换了旧的缓存值,这两种情况表示缓存是被用户显式清除掉的,而不是被缓存内部清除的。

    枚举值:

  /**
   * The entry was manually removed by the user. This can result from the user invoking {@link
   * Cache#invalidate}, {@link Cache#invalidateAll(Iterable)}, {@link Cache#invalidateAll()}, {@link
   * Map#remove}, {@link ConcurrentMap#remove}, or {@link Iterator#remove}.
   */
  // 用户主动失效掉了缓存
  EXPLICIT {
    @Override
    boolean wasEvicted() {
      return false;
    }
  },

  /**
   * The entry itself was not actually removed, but its value was replaced by the user. This can
   * result from the user invoking {@link Cache#put}, {@link LoadingCache#refresh}, {@link Map#put},
   * {@link Map#putAll}, {@link ConcurrentMap#replace(Object, Object)}, or {@link
   * ConcurrentMap#replace(Object, Object, Object)}.
   */
  // 用户主动使用新值替换了缓存
  REPLACED {
    @Override
    boolean wasEvicted() {
      return false;
    }
  },

3.2 被垃圾回收淘汰

    Guava Cache支持将配置的引用强度设置成软引用和弱引用,以避免缓存过多导致内存溢出等问题。

    相关的建造者类的方法包括:softValues()、weakValues()、weakKeys()。

    详情参考:Guava Cache:核心参数深度剖析和相关源码分析 # 软引用和弱引用

    枚举值:

  /**
   * The entry was removed automatically because its key or value was garbage-collected. This can
   * occur when using {@link CacheBuilder#weakKeys}, {@link CacheBuilder#weakValues}, or {@link
   * CacheBuilder#softValues}.
   */
  COLLECTED {
    @Override
    boolean wasEvicted() {
      return true;
    }
  },

3.3 超时淘汰

    Guava Cache支持两种超时机制:访问后超时 写后超时

  • 访问后超时会设置一个访问时间,每次读取缓存内容或者设置缓存的值,都会刷新访问的时间;如果下一次访问的时候,发现访问时长超时,会直接让缓存失效。访问超时通过方法expireAfterAccess进行设置;
  • 写后超时会设置一个写时间,每次设置缓存的值,都会刷新写时间;如果下一次访问的时候,发现访问时长超时,会直接让缓存失效。访问超时通过方法expireAfterWrite进行设置。

    详情参考:Guava Cache:核心参数深度剖析和相关源码分析 # 超时

    枚举值:

  /**
   * The entry's expiration timestamp has passed. This can occur when using {@link
   * CacheBuilder#expireAfterWrite} or {@link CacheBuilder#expireAfterAccess}.
   */
  EXPIRED {
    @Override
    boolean wasEvicted() {
      return true;
    }
  },

3.4 容量超限淘汰(LRU算法)

    如果缓存设置了最大容量(maximumSize,或者maximumWeight),则在添加缓存的时候,会去判断当前容量是否已经超限。如果缓存容量超限,则会通过LRU算法,淘汰掉最久没有访问的缓存。

    枚举值:

  /**
   * The entry was evicted due to size constraints. This can occur when using {@link
   * CacheBuilder#maximumSize} or {@link CacheBuilder#maximumWeight}.
   */
  SIZE {
    @Override
    boolean wasEvicted() {
      return true;
    }
  };

三、核心源码:缓存淘汰

1、基本原理

    缓存淘汰主要包括三方面:

  • 垃圾回收回收软引用、弱引用的缓存:这是通过JVM进行的,基本不需要程序主动进行回收,所以这里不进行讨论;
  • 缓存超时被淘汰:主要分为访问时间超时expireAfterAccess和写时间超时expireAfterWrite两种;
  • 容量超限淘汰:这种是通过LRU算法,淘汰掉最久没有访问/写入最晚的缓存。

    在容量超限时,Guava Cache通过LRU算法进行缓存淘汰。

    GuavaCache并没有独立的线程来管理缓存,以避免和应用程序发生资源的争夺。主要靠在写操作时来做一部分清理工作,如果清理写操作太少,也可能在读操作中触发清理操作清理部分缓存。

    另外,用户可以自己决定如何清理缓存。如果写操作非常少,又需要有更快的读取速率,或者是又想长期维持较大的内存开销,可以自定义维护线程,定期清理失效缓存。

2、相关实现

    LocalCache的Segment中实现了一些队列,用来协助完成缓存的淘汰:

  • final Queue<ReferenceEntry<K, V>> recencyQueue:记录节点被访问的顺序,会在写操作执行或者DRAIN_THRESHOLD被触发的时候全部出队;
  • final @Nullable ReferenceQueue<V> valueReferenceQueue:值引用队列,记录被垃圾回收、且需要被内部清理的值;
  • final @Nullable ReferenceQueue<V> keyReferenceQueue:值键引用队列,记录被垃圾回收、且需要被内部清理的节点;
  • @GuardedBy("this") final Queue<ReferenceEntry<K, V>> writeQueue:缓存中的全部节点,按写顺序排序,最近加入的元素会被加到队列的尾部;
  • @GuardedBy("this") final Queue<ReferenceEntry<K, V>> accessQueue:缓存中的全部节点,按访问顺序排序,最近访问的元素会被加到队列的尾部,写操作也是访问的一种。

3、写方法缓存回收

    主要步骤:

  1. 执行写入操作;
  2. 检查是否totalWeight > maxSegmentWeight,如果是,则执行移除操作
  3. 从当前segment的第一个节点开始删除,循环进行直到totalWeight <= maxSegmentWeight

    循环逻辑:

      while (totalWeight > maxSegmentWeight) {
        // 获取下一个可回收的节点
        ReferenceEntry<K, V> e = getNextEvictable();
        if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
          throw new AssertionError();
        }
      }

    删除逻辑:

    @VisibleForTesting
    @GuardedBy("this")
    boolean removeEntry(ReferenceEntry<K, V> entry, int hash, RemovalCause cause) {
      int newCount = this.count - 1;
      AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
      int index = hash & (table.length() - 1);
      ReferenceEntry<K, V> first = table.get(index);

      for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
        if (e == entry) {
          ++modCount;
          // 删除头节点,并更新头节点的指向
          ReferenceEntry<K, V> newFirst =
              removeValueFromChain(
                  first,
                  e,
                  e.getKey(),
                  hash,
                  e.getValueReference().get(),
                  e.getValueReference(),
                  cause);
          newCount = this.count - 1;
          table.set(index, newFirst);
          this.count = newCount; // write-volatile
          return true;
        }
      }

      return false;
    }

    具体的删除逻辑:

    @GuardedBy("this")
    @Nullable
    ReferenceEntry<K, V> removeValueFromChain(
        ReferenceEntry<K, V> first,
        ReferenceEntry<K, V> entry,
        @Nullable K key,
        int hash,
        V value,
        ValueReference<K, V> valueReference,
        RemovalCause cause) {
      enqueueNotification(key, hash, value, valueReference.getWeight(), cause);
      writeQueue.remove(entry);
      accessQueue.remove(entry);

      if (valueReference.isLoading()) {
        valueReference.notifyNewValue(null);
        return first;
      } else {
        return removeEntryFromChain(first, entry);
      }
    }

4、访问后缓存回收

    核心过程:

  1. 读操作完成后进行清理工作
  2. 如果读操作的累计次数readCount达到了DRAIN_THRESHOLD指示的次数,即0x3F次,则执行清理(这是为了避免长期没有写操作,导致缓存长时间没有被清理);
  3. 清理逻辑:根据选择的超时方式(expireAfterAccess或expireAfterWrite),分别从writeQueue或accessQueue删除元素。

    注意事项:

  • 读操作的累计次数readCount,会在每次清空缓存、写操作、读之后的清理工作进行后,被清零。

    源码:

    void cleanUp() {
      long now = map.ticker.read();
      runLockedCleanup(now);
      runUnlockedCleanup();
    }

    void runLockedCleanup(long now) {
      if (tryLock()) {
        try {
          // 清空keyReferenceQueue和valueReferenceQueue
          drainReferenceQueues();
          // 清理超时节点
          expireEntries(now); // calls drainRecencyQueue
          readCount.set(0);
        } finally {
          unlock();
        }
      }
    }

    @GuardedBy("this")
    void expireEntries(long now) {
      drainRecencyQueue();

      ReferenceEntry<K, V> e;
      // 判断节点是否超时,和设置的超时方式有关expireAfterWrite或expireAfterAccess
      while ((e = writeQueue.peek()) != null && map.isExpired(e, now)) {
        if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
          throw new AssertionError();
        }
      }
      while ((e = accessQueue.peek()) != null && map.isExpired(e, now)) {
        if (!removeEntry(e, e.getHash(), RemovalCause.EXPIRED)) {
          throw new AssertionError();
        }
      }
    }

    // 如果没有持有锁,可以给缓存移除监听器发送通知消息
    void runUnlockedCleanup() {
      // locked cleanup may generate notifications we can send unlocked
      if (!isHeldByCurrentThread()) {
        map.processPendingNotifications();
      }
    }
  }

5、缓存刷新

1、基本原理:

    Guava Cache提供了一种缓存刷新的机制。在缓存超时前,可以设置一个刷新时间,在缓存写入后多久进行刷新。刷新时间在构造缓存时使用refreshAfterWrite方法进行设置。

    缓存刷新的注意事项:

  • 缓存刷新是异步实现的,但是第一条触发缓存刷新的线程,会阻塞等待异步任务完成;
  • 如果缓存刷新任务获取新的缓存失败,则触发缓存刷新的线程,会返回缓存中现有的旧值;
  • 第一条请求之后的其他线程,如果发现该缓存正在被刷新,它不会阻塞等待刷新任务的完成,而是会直接返回缓存中现有的旧值。

2、核心代码:

    1)第一条线程发现超过refreshNanos,执行刷新;后面的线程访问到正在刷新的缓存时,直接返回旧值:

    V scheduleRefresh(
        ReferenceEntry<K, V> entry,
        K key,
        int hash,
        V oldValue,
        long now,
        CacheLoader<? super K, V> loader) {
      // 判断: 是否需要刷新 && 并不是正在刷新
      if (map.refreshes()
          && (now - entry.getWriteTime() > map.refreshNanos)
          && !entry.getValueReference().isLoading()) {
        V newValue = refresh(key, hash, loader, true);
        if (newValue != null) {
          return newValue;
        }
      }
      // 不需要刷新,或者有其他线程这个字刷新,就返回现在的旧值
      return oldValue;

    2)第一条触发refresh逻辑的线程,阻塞等待异步执行结果:


    @Nullable
    V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
      // 插入一个LoadingValueReference,这样后面的线程访问到的时候,可以知道这个缓存正在被刷新
      final LoadingValueReference<K, V> loadingValueReference =
          insertLoadingValueReference(key, hash, checkTime);
      if (loadingValueReference == null) {
        return null;
      }
 
      // 异步执行,生成future占位符
      ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
      // 服务线程阻塞等待异步任务执行完成
      if (result.isDone()) {
        try {
          return Uninterruptibles.getUninterruptibly(result);
        } catch (Throwable t) {
          // don't let refresh exceptions propagate; error was already logged
        }
      }
      return null;

    3)异步刷新缓存的具体逻辑:


    ListenableFuture<V> loadAsync(
        final K key,
        final int hash,
        final LoadingValueReference<K, V> loadingValueReference,
        CacheLoader<? super K, V> loader) {
      final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
      loadingFuture.addListener(
          new Runnable() {
            @Override
            public void run() {
              try {
                // 异步任务执行完成时,回调getAndRecordStats方法去设置缓存的值,并记录统计结果
                getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
              } catch (Throwable t) {
                logger.log(Level.WARNING, "Exception thrown during refresh", t);
                loadingValueReference.setException(t);
              }
            }
          },
          directExecutor());
      return loadingFuture;

四、核心源码:主要流程

1、构造缓存

    由于Cache有多种实现类,以及多个复杂的参数,所以Cache的实例是使用建造者模式,通过CacheBuilder进行构造的。CacheBuilder中会对各个参数的内容、使用场景等进行组合和校验,并最终调用相应Cache接口的实现类构造实例。

    CacheBuilder的方法详解参考:Guava Cache:核心参数深度剖析和相关源码分析

1.1 Cache的实现类

    Cache有多个子接口和实现类:

    这些实现类分为两种:Cache接口的直接实现类,以及Cache的子接口LoadingCache的实现类。LoadingCache在继承Cache接口的接触上,还要求实现类可以实现缓存的自动刷新。对用户来说,两个最直接的感知:

  • 构造Cache的直接实现类时,使用CacheBuilder的build()无参方法;而构造LoadingCache的实现类,需要使用CacheBuilder的build(CacheLoader loader)方法指定默认的缓存加载器;
  • 从Cache的直接实现类查询缓存时,使用get(K key, Callable<? extends V> loader),loader用来在缓存失效或不存在时加载缓存;从LoadingCache的实现类获取缓存时,可以使用get(K key)方法,因为LoadingCache的实现类有默认的loader。

    当然,LoadingCache还提供了其他一些方法,包括:

  • V getUnchecked(K key):get方法会抛ExecutionException,而getUnchecked会将ExecutionException转换成UncheckedExecutionException并抛出;
  • ImmuableMap<K, V> getAll(Iterale<? extends K> keys):获取一批缓存;
  • V apply(K key):从Function中继承来的接口,内部是通过get方法或者getUnchecked方法实现的;
  • void refresh(K key):刷新key对应的缓存值,可能同步也可能异步。LocalLoadingCache的实现,是异步刷新,但是会同步等待异步结果。

1.2 LoadingCache的构造

    一般来说,推荐使用的是LoadingCache子接口的实现类,好处是在get方法获取缓存的时候,不需要再次指定CacheLoader,

    LoadingCache最重要的一个实现类,就是LocalLoadingCache。使用CacheBuilder的build(CacheLoader loader)方法,构造出来的就是LocalLoadingCache的实例。

    LoadingCache的详细构造方法,参考:Guava Cache:核心参数深度剖析和相关源码分析

    源码分析:

    1)CacheBuilder.build(CacheLoader loader)创建LocalLoadingCache的实例:

  public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
      CacheLoader<? super K1, V1> loader) {
    checkWeightWithWeigher();
    return new LocalCache.LocalLoadingCache<>(this, loader);
  }

    2)LocalLoadingCache是LocalManuelCache的子类,最终调用了LocalManuelCache的构造器:

    // LocalLoadingCache的构造器,这是CacheBuilder中直接调用的
    LocalLoadingCache(
        CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
      // 可以看出,Cache的实例实际上是通过LocalCache的实例,对缓存的内部逻辑进行管理的
      super(new LocalCache<K, V>(builder, checkNotNull(loader)));
    }

    // 这是父类LocalManualCache的构造器,上面LocalLoadingCache的构造器中super的调用目标
    private LocalManualCache(LocalCache<K, V> localCache) {
      this.localCache = localCache;
    }

    从上面的源码可以看出,Cache至少一个对外提供接口的封装,实际上缓存内部的各种控制逻辑,都是通过LocalCache来实现的。

1.3 LocalCache的构造

    LocalCache和Cache没有任何继承方面的关系,LocalCache一般用作Cache实现类的成员变量。Cache是对外提供相关缓存功能的客户端,而客户端内部的逻辑控制,是通过LocalCache来完成的。

    LocalCache实际上是一个哈希结构:

class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>

    LocalCache构造器中的主要逻辑,主要是将CacheBuilder的参数,转换成自身的参数。

    一般参数的转化逻辑比较简单,就是“取值+边界校验”,这里不再详细解析,对特定某个参数感兴趣可以参考另一篇文章:Guava Cache:核心参数深度剖析和相关源码分析

    构造器中还有些较为复杂的逻辑,包括计算段数量的逻辑,以及计算段的最大权重值的逻辑。这里只展示源码和注释,逻辑概述可以参考上面一段中的文章。

1、计算段数量:

    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      // 这时的segmentShift还是表示segmentCount是2的多少次幂
      ++segmentShift;
      // segmentCount是满足while条件的最大值的2倍
      segmentCount <<= 1;
    }
    // 最终的segmentShift用于取hash的高位的相应位数,用来计算寻找一个元素在哪个segment中
    this.segmentShift = 32 - segmentShift;
    // 掩码,取hash的低位的相应位数的值,即为在segment中的角标
    segmentMask = segmentCount - 1;
 
 
  // 是否限制容量
  boolean evictsBySize() {
    return maxWeight >= 0;
  }

2、计算段的最大权重值

    // 构造器中maxWeight对段生效的代码
    // segmentCapacity = initialCapacity 除以 segmentCount 向上取整
    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }
 
    // segmentSize = 不小于segmentCapacity的 最小的 2的整数幂
    // segmentSize用作段的初始容量
    int segmentSize = 1;
    while (segmentSize < segmentCapacity) {
      segmentSize <<= 1;
    }
 
    // 是否限制容量
    if (evictsBySize()) {
      // Ensure sum of segment max weights = overall max weights
      // 段容量基础值 = 总容量 除以 段数 向上取整
      long maxSegmentWeight = maxWeight / segmentCount + 1;
      long remainder = maxWeight % segmentCount;
      for (int i = 0; i < this.segments.length; ++i) {
        // 从第余数段开始,段容量减1,以保证各段容量之和等于总容量
        if (i == remainder) {
          maxSegmentWeight--;
        }
        this.segments[i] =
            createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
      }
    } else {
      // 如果未设置总的最大容量,则每个分段都不设置最大容量
      for (int i = 0; i < this.segments.length; ++i) {
        this.segments[i] =
            createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
      }
    }
 
 
  // 是否会根据容量进行淘汰
  boolean evictsBySize() {
    return maxWeight >= 0;
  }

3、构造器完整源码:

  /**
   * Creates a new, empty map with the specified strategy, initial capacity and concurrency level.
   */
  LocalCache(
      CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
    concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);

    keyStrength = builder.getKeyStrength();
    valueStrength = builder.getValueStrength();

    keyEquivalence = builder.getKeyEquivalence();
    valueEquivalence = builder.getValueEquivalence();

    maxWeight = builder.getMaximumWeight();
    weigher = builder.getWeigher();
    expireAfterAccessNanos = builder.getExpireAfterAccessNanos();
    expireAfterWriteNanos = builder.getExpireAfterWriteNanos();
    refreshNanos = builder.getRefreshNanos();

    removalListener = builder.getRemovalListener();
    removalNotificationQueue =
        (removalListener == NullListener.INSTANCE)
            ? LocalCache.<RemovalNotification<K, V>>discardingQueue()
            : new ConcurrentLinkedQueue<RemovalNotification<K, V>>();

    ticker = builder.getTicker(recordsTime());
    entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
    globalStatsCounter = builder.getStatsCounterSupplier().get();
    defaultLoader = loader;

    int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);
    if (evictsBySize() && !customWeigher()) {
      initialCapacity = (int) Math.min(initialCapacity, maxWeight);
    }

    // Find the lowest power-of-two segmentCount that exceeds concurrencyLevel, unless
    // maximumSize/Weight is specified in which case ensure that each segment gets at least 10
    // entries. The special casing for size-based eviction is only necessary because that eviction
    // happens per segment instead of globally, so too many segments compared to the maximum size
    // will result in random eviction behavior.
    int segmentShift = 0;
    int segmentCount = 1;
    while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
      ++segmentShift;
      segmentCount <<= 1;
    }
    this.segmentShift = 32 - segmentShift;
    segmentMask = segmentCount - 1;

    this.segments = newSegmentArray(segmentCount);

    int segmentCapacity = initialCapacity / segmentCount;
    if (segmentCapacity * segmentCount < initialCapacity) {
      ++segmentCapacity;
    }

    int segmentSize = 1;
    while (segmentSize < segmentCapacity) {
      segmentSize <<= 1;
    }

    if (evictsBySize()) {
      // Ensure sum of segment max weights = overall max weights
      long maxSegmentWeight = maxWeight / segmentCount + 1;
      long remainder = maxWeight % segmentCount;
      for (int i = 0; i < this.segments.length; ++i) {
        if (i == remainder) {
          maxSegmentWeight--;
        }
        this.segments[i] =
            createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get());
      }
    } else {
      for (int i = 0; i < this.segments.length; ++i) {
        this.segments[i] =
            createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
      }
    }
  }

2、查询缓存

2.1 主要方式

    Cache通过get方法查询缓存:

  • get(K key):
  • get(K key, Callable loader):

2.2 基本流程

    这里分析LocalLoadingCache的get(K key)的逻辑。两方法的底层原理其实是一样的,get(K key)可以看做是调用了get(key, defaultLoader)。defaultLoader是在构造实例时通过build方法传入的。

1、调用成员变量localCache的getOrLoad方法

    @Override
    public V get(K key) throws ExecutionException {
      return localCache.getOrLoad(key);
    }

2、localCache的getOrLoad方法去调用它的重载方法。

  V getOrLoad(K key) throws ExecutionException {
    return get(key, defaultLoader);
  }

    这也是Cache的get(K key, Callable loader)实际上调用的方法,使用Callable loader构造一个CacheLoader的实例,再调用localCache的get方法:

    @Override
    public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException {
      checkNotNull(valueLoader);
      return localCache.get(
          key,
          new CacheLoader<Object, V>() {
            @Override
            public V load(Object key) throws Exception {
              return valueLoader.call();
            }
          });
    }

3、localCache的get方法:

    localCache的get方法,主要作用是:

  • 检查key是否有效(不是null);
  • 根据key计算hash;
  • 根据hash的高位使用hash算法寻找相应的segment;
  • 调用segment的get方法,查询或者加载缓存。

    源码:

  V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = hash(checkNotNull(key));
    return segmentFor(hash).get(key, hash, loader);
  }

    segmentFor方法用来根据hash的高位从segments数组中取出相应的segment实例。详细解析参考上面第二章1.2的分析。

  /**
   * Returns the segment that should be used for a key with the given hash.
   *
   * @param hash the hash code for the key
   * @return the segment
   */
  Segment<K, V> segmentFor(int hash) {
    // TODO(fry): Lazily create segments?
    return segments[(hash >>> segmentShift) & segmentMask];
  }

4、segment的get方法:

    segment的get方法获取缓存的真正核心逻辑。它的主要流程包括:

    1)预操作:检查参数key和loader是否有效(不是null);

    2)主流程:

  1. 判断segment是否为空,如果为空则去加载缓存;
  2. 根据hash和key获取键值对,如果键值对为空,则去加载缓存;
  3. 根据取到的键值对,获取存活的value,如果value不存在、或者已经被清理掉、或者已经超时,则去加载或者等待加载结果;
  4. 此时说明存在有效的缓存,统计缓存的访问,并返回结果或者刷新再返回结果。

    对于最后异步,如果未达到refreshAfterWrite设置的超时时间,则直接返回结果;如果达到了,则第一个线程会阻塞等待后台异步刷新,后面的线程不会等待,而是直接返回现有结果。

    3)后操作:特定情况下清理超时节点。如果在上一次写操作或者清理操作以后,已经经历过多次(readCount == 0x3F,即63次)读操作而未清理超时节点,就会执行清理工作,清理超时节点。清理完成后,会重置上述次数readCount。

    V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        // count指的是当前segment中元素的数量。count == 0 表示当前segment为空
        if (count != 0) { // read-volatile
          // don't call getLiveEntry, which would ignore loading values
          // getEntry会校验key,所以key为弱引用被回收的场景,取到的e是null
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            // ticker.read()计算纳秒时间戳,用于判断是否超时、是否需要刷新
            long now = map.ticker.read();
            // getLiveValue取到的是存活的有效的value:
            // 1)如果超时会取到null;
            // 2)如果value是软引用或者弱引用,被GC回收了,也会取到null;
            // 3)如果value没有超时且没有被回收,但是超过了refreshAfterWrite设置的时间,也会取到有效值。
            V value = getLiveValue(e, now);
            if (value != null) {
              // 记录该缓存被访问了。此时expireAfterAccess相关的时间会被刷新
              recordRead(e, now);
              // 记录缓存击中
              statsCounter.recordHits(1);
              // 用来判断是直接返回现有value,还是等待刷新
              return scheduleRefresh(e, key, hash, value, now, loader);
            }
            ValueReference<K, V> valueReference = e.getValueReference();
            // 只有key存在,但是value不存在(被回收)、或缓存超时的情况会到达这里
            // 如果已经有线程在加载缓存了,后面的线程不会重复加载,而是等待加载的结果
            if (valueReference.isLoading()) {
              return waitForLoadingValue(e, key, valueReference);
            }
          }
        }

        // at this point e is either null or expired;
        // 走到这里的场景:
        // 1)segment为空
        // 2)key或value不存在(没有缓存,或者弱引用、软引用被回收),
        // 3)缓存超时(expireAfterAccess或expireAfterWrite触发的)
        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 {
        // 读后的清理工作。如果在上一次写操作或者清理操作以后,已经有多次(0x3F次)读操作,就会执行清理工作,清理超时节点
        postReadCleanup();
      }
    }

2.3 步骤详解:查询和刷新

1、根据hash和key获取键值对:getEntry

    @Nullable
    ReferenceEntry<K, V> getEntry(Object key, int hash) {
      // getFirst用来根据hash获取table中相应位置的链表的头元素
      for (ReferenceEntry<K, V> e = getFirst(hash); e != null; e = e.getNext()) {
        // hash不相等的,key肯定不相等。hash判等是int判等,比直接用key判等要快得多
        if (e.getHash() != hash) {
          continue;
        }

        K entryKey = e.getKey();
        // entryKey == null的情况,是key为软引用或者弱引用,已经被GC回收了。直接清理掉
        if (entryKey == null) {
          tryDrainReferenceQueues();
          continue;
        }

        if (map.keyEquivalence.equivalent(key, entryKey)) {
          return e;
        }
      }

      return null;
    }

    其中getFirst的代码:

    // 获取table中hash相应位置的链表的头元素
    /** Returns first entry of bin for given hash. */
    ReferenceEntry<K, V> getFirst(int hash) {
      // read this volatile field only once
      AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
      return table.get(hash & (table.length() - 1));
    }

2、清理被回收的缓存:tryDrainReferenceQueues

    这个操作只会在查询缓存结果、判断缓存是否存在的场景才会触发。

    /** Cleanup collected entries when the lock is available. */
    void tryDrainReferenceQueues() {
      // tryLock是继承自ReentrantLock的方法。尝试获取锁,成功获取到锁则返回true
      if (tryLock()) {
        try {
          // 清理存储非强引用的缓存的队列。真正的清理被回收配置的逻辑
          drainReferenceQueues();
        } finally {
          unlock();
        }
      }
    }

    drainReferenceQueues用来清空key的非强引用的队列和value的非强引用的队列。队列里存储的是ReferenceEntry。

    /**
     * Drain the key and value reference queues, cleaning up internal entries containing garbage
     * collected keys or values.
     */
    @GuardedBy("this")
    void drainReferenceQueues() {
      if (map.usesKeyReferences()) {
        drainKeyReferenceQueue();
      }
      if (map.usesValueReferences()) {
        drainValueReferenceQueue();
      }
    }

    清理key的非强引用队列的具体逻辑:

    @GuardedBy("this")
    void drainKeyReferenceQueue() {
      Reference<? extends K> ref;
      int i = 0;
      while ((ref = keyReferenceQueue.poll()) != null) {
        @SuppressWarnings("unchecked")
        ReferenceEntry<K, V> entry = (ReferenceEntry<K, V>) ref;
        // reclaimKey方法用来清理一个key被垃圾回收的缓存的键值对ReferenceEntry
        map.reclaimKey(entry);
        if (++i == DRAIN_MAX) {
          break;
        }
      }
    }

    清理value的非强引用队列的具体逻辑:

    @GuardedBy("this")
    void drainValueReferenceQueue() {
      Reference<? extends V> ref;
      int i = 0;
      while ((ref = valueReferenceQueue.poll()) != null) {
        @SuppressWarnings("unchecked")
        ValueReference<K, V> valueReference = (ValueReference<K, V>) ref;
        map.reclaimValue(valueReference);
        if (++i == DRAIN_MAX) {
          break;
        }
      }
    }

3、获取存活的value: getLiveValue

    /**
     * Gets the value from an entry. Returns null if the entry is invalid, partially-collected,
     * loading, or expired.
     */
    V getLiveValue(ReferenceEntry<K, V> entry, long now) {
      // 软引用或者弱引用的key被清理掉了
      if (entry.getKey() == null) {
        // 清理非强引用的队列
        tryDrainReferenceQueues();
        return null;
      }
      // 软引用或者弱引用的value被清理掉了
      V value = entry.getValueReference().get();
      if (value == null) {
        // 清理非强引用的队列
        tryDrainReferenceQueues();
        return null;
      }

      if (map.isExpired(entry, now)) {
        // 如果获取锁成功,就清理超时的缓存
        tryExpireEntries(now);
        return null;
      }
      return value;
    }

4、获取结果或刷新缓存:scheduleRefresh

    scheduleRefresh的主要逻辑:

  1. 如果没有开启刷新功能,或者没有达到刷新的时间,则直接返回现有的value;
  2. 如果达到了刷新时间,并且value没有在刷新,则调用refresh,刷新value并阻塞获取刷新结果;
  3. 如果刷新失败,则返回现有的value的值;
  4. 如果达到了刷新时间,但是value已经在刷新,则直接返回现有的value。
    V scheduleRefresh(
        ReferenceEntry<K, V> entry,
        K key,
        int hash,
        V oldValue,
        long now,
        CacheLoader<? super K, V> loader) {
      // refreshes判断是否开启了自动刷新,即是否配置了refreshAfterWrite
      if (map.refreshes()
          // 达到了刷新时间
          && (now - entry.getWriteTime() > map.refreshNanos)
          // 已经有线程触发了该条缓存的刷新
          && !entry.getValueReference().isLoading()) {
        // 调用refresh刷新缓存
        V newValue = refresh(key, hash, loader, true);
        // 如果刷新失败,即newValue == null,则返回现有值
        if (newValue != null) {
          return newValue;
        }
      }
      return oldValue;
    }

    刷新缓存的逻辑refresh:

    /**
     * Refreshes the value associated with {@code key}, unless another thread is already doing so.
     * Returns the newly refreshed value associated with {@code key} if it was refreshed inline, or
     * {@code null} if another thread is performing the refresh or if an error occurs during
     * refresh.
     */
    @Nullable
    V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
      // 用LoadingValueReference替换现在的ValueReference。LoadingValueReference的特点是isLoading方法返回true
      final LoadingValueReference<K, V> loadingValueReference =
          insertLoadingValueReference(key, hash, checkTime);
      if (loadingValueReference == null) {
        return null;
      }

      // Future说明是异步在刷新缓存
      ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
      // Future的isDone方法用来判断这个Future是不是完整的,即是否能够返回结果(不代表已经指向完成)
      if (result.isDone()) {
        try {
          // 阻塞,不断的轮询调用Future的get方法获取future的结果,直到线程被打断
          return Uninterruptibles.getUninterruptibly(result);
        } catch (Throwable t) {
          // don't let refresh exceptions propagate; error was already logged
        }
      }
      return null;
    }

5、等待其他线程刷新:waitForLoadingValue

    V waitForLoadingValue(ReferenceEntry<K, V> e, K key, ValueReference<K, V> valueReference)
        throws ExecutionException {
      // double check机制。在调用这个方法前,实际上会先验证valueReference.isLoading() == true
      if (!valueReference.isLoading()) {
        throw new AssertionError();
      }

      // 还是在检查。如果当前线程持有锁,就会抛异常
      checkState(!Thread.holdsLock(e), "Recursive load of: %s", key);
      // don't consider expiration as we're concurrent with loading
      try {
        // 阻塞的去获取锁。底层就是getUninterruptibly(futureValue)
        V value = valueReference.waitForValue();
        if (value == null) {
          throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
        }
        // re-read ticker now that loading has completed
        long now = map.ticker.read();
        recordRead(e, now);
        return value;
      } finally {
        // 这种情况要记录缓存没有击中
        statsCounter.recordMisses(1);
      }
    }

    防止读上面的检查代码时搞不清楚,贴上Preconditions.checkState的代码:

  // 防止读上面的检查代码时搞不清楚,贴上Preconditions.checkState的代码
  public static void checkState(
      boolean b, @Nullable String errorMessageTemplate, @Nullable Object p1) {
    if (!b) {
      throw new IllegalStateException(lenientFormat(errorMessageTemplate, p1));
    }
  }

6、加锁获取缓存:lockedGetOrLoad(key, hash, loader)

    会走到这一步的场景:

  1. segment为空;
  2. key或value不存在(没有缓存,或者弱引用、软引用被回收);
  3. 缓存超时(expireAfterAccess或expireAfterWrite触发的)

    lockedGetOrLoad的逻辑相当于:根据key,使用loader获取到value并写入到缓存中,然后再返回。这个跟写很接近了,源码也比较长,重新拉一节来解析。

2.4 步骤详解:加锁并加载

    会走到这一步的场景:

  1. segment为空;
  2. key或value不存在(没有缓存,或者弱引用、软引用被回收);
  3. 缓存超时(expireAfterAccess或expireAfterWrite触发的)

    这一步的主要逻辑:

  1. 加锁;
  2. 清理无效节点(被GC回收的非强引用节点、超时节点);
  3. 根据key查询缓存,并验证value是否存在、是否有效;
  4. 如果需要加载缓存,则创建LoadingValueReference节点并加载缓存;
  5. 释放锁,并执行清理操作(发布缓存清理事件)
  6. 等待缓存加载

    源码:

    V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      ReferenceEntry<K, V> e;
      ValueReference<K, V> valueReference = null;
      LoadingValueReference<K, V> loadingValueReference = null;
      boolean createNewEntry = true;

      // 加锁,避免并发问题
      lock();
      try {
        // 重新计算现在的时间,是为了应对加锁前有其他线程插入了key的缓存的场景
        // re-read ticker once inside the lock
        long now = map.ticker.read();
        // 写前清理的主要内容:清理被回收的弱引用;清理超时节点;readCount重置为0
        preWriteCleanup(now);

        // newCount用来给this.count赋值。应用场景:
        // 如果读到了超时的缓存,或者被GC回收的缓存,就清理掉读到的缓存,同时让count-1。清理之后,会再去加载缓存
        int newCount = this.count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        // for循环用来遍历,是否存在key相应的缓存
        for (e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          // if判断是否存在缓存
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            valueReference = e.getValueReference();
            // if判断读到的缓存引用,是否是正在加载缓存
            if (valueReference.isLoading()) {
              // 如果已经有其他线程在加载缓存,则本线程不再触发加载操作,而是等待加载结果
              createNewEntry = false;
            } else {
              V value = valueReference.get();
              if (value == null) {
                // 此时value已经被GC回收了,enqueueNotification表示发布缓存回收事件,原因是COLLECTED
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
              } else if (map.isExpired(e, now)) {
                // preWriteCleanup中已经清理超时节点了,但是这里还是重复检查是否超时。
                // 双重检查机制是加锁场景的一种常用手段,避免加锁前的检查和加锁之间其他触发了影响操作目标的操作
                // This is a duplicate check, as preWriteCleanup already purged expired
                // entries, but let's accommodate an incorrect expiration queue.
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED);
              } else {
                // 如果读到的value存在且没有超时,说明value是有效值
                // 记录一次加锁读
                recordLockedRead(e, now);
                // 记录一次缓存命中
                statsCounter.recordHits(1);
                // 返回有效值
                // we were concurrent with loading; don't consider refresh
                return value;
              }

              // 此时说明读到的value无效,应该移除相应的缓存,并让缓存数量减1
              // immediately reuse invalid entries
              writeQueue.remove(e);
              accessQueue.remove(e);
              this.count = newCount; // write-volatile
            }
            break;
          }
        }

        // createNewEntry用来判断是否需要新建一个LoadingValueReference,用来加载缓存
        // 如果上面的for循环中读到了LoadingValueReference类型的节点,就不用再加载缓存了
        if (createNewEntry) {
          loadingValueReference = new LoadingValueReference<>();

          if (e == null) {
            e = newEntry(key, hash, first);
            e.setValueReference(loadingValueReference);
            table.set(index, e);
          } else {
            e.setValueReference(loadingValueReference);
          }
        }
      } finally {
        // 针对key的写操作处理完成,释放锁
        unlock();
        // 写后清理,主要逻辑是调用无锁清理方法,发布缓存清理事件(缓存清理事件通过enqueueNotification加入队列)
        postWriteCleanup();
      }

      if (createNewEntry) {
        try {
          // Synchronizes on the entry to allow failing fast when a recursive load is
          // detected. This may be circumvented when an entry is copied, but will fail fast most
          // of the time.
          // 如果当前线程触发了缓存加载的操作,就针对e加锁等待加载完成
          synchronized (e) {
            return loadSync(key, hash, loadingValueReference, loader);
          }
        } finally {
          statsCounter.recordMisses(1);
        }
      } else {
        // 如果当前线程不是触发缓存加载的操作的线程,就调用waitForLoadingValue方法同步等待缓存加载完成
        // The entry already exists. Wait for loading.
        return waitForLoadingValue(e, key, valueReference);
      }
    }

1、写前加锁清理preWriteCleanup:

    preWriteClean本质上是调用了加锁场景的清理方法runLockedCleanUp方法:

    @GuardedBy("this")
    void preWriteCleanup(long now) {
      runLockedCleanup(now);
    }

    runLockedCleanUp方法要清理的内容,包括:被GC回收的非强引用节点、超时节点,同时还会将readCount重置为0。readCount会在每次读操作后累加,并在达到一定门限(63)时执行缓存清理工作。

    readCount重置的场景:

  • 写操作,preWriteClean调用runLockedCleanUp;
  • 读操作,postReadCleanup,readCount达到一定门限(63)时会调用runLockedCleanUp;
  • clear()方法被调用,清除全部缓存的时候,会重置readCount;
    void runLockedCleanup(long now) {
      if (tryLock()) {
        try {
          drainReferenceQueues();
          expireEntries(now); // calls drainRecencyQueue
          readCount.set(0);
        } finally {
          unlock();
        }
      }
    }

2、写后无锁清理postWriteCleanup:

    postWriteCleanup主要是用来发布缓存清理通知。

    postWriteCleanup调用了无锁清理方法runUnlockedCleanup:

    void postWriteCleanup() {
      runUnlockedCleanup();
    }

    runUnlockedCleanup方法是在当前线程无锁的情况下,才会去发布缓存清理通知。之所以要判断当前线程没有持锁,是因为持锁的情况下是可能产生新的通知的。

    void runUnlockedCleanup() {
      // locked cleanup may generate notifications we can send unlocked
      if (!isHeldByCurrentThread()) {
        map.processPendingNotifications();
      }
    }
  }

    发布通知的具体逻辑。实际上就是从通知队列中取出所以的缓存清理通知,并分别传入缓存移除器的onRemoval方法。

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

3、添加/更新缓存

3.1 主要方式

    用户主动添加缓存的方式包括:

  • put(K key, V value):存入一条缓存;
  • putAll(Map<? extends K, ? extends V> m):存储一批缓存。

3.2 源码分析

    putAll是通过对Map迭代分别调用put方法实现的。所以,这里分析put的逻辑。

1、调用成员变量localCache的put方法

    @Override
    public void put(K key, V value) {
      localCache.put(key, value);
    }

2、localCache的put方法:

    localCache的put方法,主要作用是:

  • 检查key是否有效(不是null);
  • 根据key计算hash;
  • 根据hash的高位使用hash算法寻找相应的segment;
  • 调用segment的put方法,查询或者加载缓存。
  @Override
  public V put(K key, V value) {
    checkNotNull(key);
    checkNotNull(value);
    int hash = hash(key);
    return segmentFor(hash).put(key, hash, value, false);
  }

    segmentFor方法用来根据hash的高位从segments数组中取出相应的segment实例。详细解析参考上面第二章1.2的分析。

  /**
   * Returns the segment that should be used for a key with the given hash.
   *
   * @param hash the hash code for the key
   * @return the segment
   */
  Segment<K, V> segmentFor(int hash) {
    // TODO(fry): Lazily create segments?
    return segments[(hash >>> segmentShift) & segmentMask];
  }

3、segment的put方法:

    segment的get方法获取缓存的真正核心逻辑。它的主要流程包括:

  1. 加锁;
  2. 清理无效节点(被GC回收的非强引用节点、超时节点);
  3. 判断缓存数量是否达到了扩容门限,如果达到了则进行扩容;
  4. 根据key查询缓存,并验证value是否存在、是否有效
  5. 创建缓存节点并保存;
  6. 判断是否需要由于容量超限驱逐节点;
  7. 释放锁,并执行清理操作(发布缓存清理事件)

    源码如下:

    @Nullable
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
      lock();
      try {
        long now = map.ticker.read();
        // 写前清理,主要逻辑是清理被GC的节点和超时节点,同时将readCount置0。详情参见上一节
        preWriteCleanup(now);

        int newCount = this.count + 1;
        // 门限是当前长度的0.75倍:this.threshold = newTable.length() * 3 / 4; // 0.75
        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用来判断节点存在
          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用来判断value是否存在。key存在而value不存在,说明value不是强引用,被GC回收了
            if (entryValue == null) {
              // modCount用来记录修改table大小的次数。
              // modCount主要用在批量读取中,判断批量读取的缓存是不是来自于同一个内存镜像。如果不是,可能需要重试。
              // 比如,在isEmpty方法中会校验segment的modCount
              ++modCount;
              // valueReference是否包含激活的value:如果包含有效的value,或者已经被驱逐的value,或者是已经被GC回收的value,都会返回true
              // 目前所有的实现类都会return true
              // 结合上面的判断,说明这个Entry曾经存入过缓存,但是value已经不存在,说明被GC清理了
              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
              // 驱逐缓存。会判断是否容量超限,如果超限则按LRU清除一部分缓存保证容量不超限
              evictEntries(e);
              // put传入的value被存入的情况,会返回null
              return null;
            } else if (onlyIfAbsent) {
              // 此时value存在,且put方法要求onlyIfAbsent才会写入缓存。当前版本Cache没有提供putIfAbsent方法,所以这里走不到
              // Mimic
              // "if (!map.containsKey(key)) ...
              // else return map.get(key);
              recordLockedRead(e, now);
              // 如果缓存已经存在,则返回旧值
              return entryValue;
            } else {
              // 没有修改table的大小,依然改了modCount。。。这里有点迷,感觉以后的版本可能会扩展modCount的用途
              // clobber existing entry, count remains unchanged
              ++modCount;
              enqueueNotification(
                  key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
              setValue(e, key, value, now);
              // 驱逐缓存。会判断是否容量超限,如果超限则按LRU清除一部分缓存保证容量不超限
              evictEntries(e);
              // 如果缓存已经存在,则返回旧值
              return entryValue;
            }
          }
        }

        // 创建新节点,所以需要修改modCount
        // 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();
      }
    }

3.3 扩容

1、需要重新计算的变量:

  • 扩容门限threshold: newTable.length() * 3 / 4
  • Hash计算的掩码newMask: ewTable.length() - 1

2、扩容的基本逻辑:

  1. 长度是否超限(MAXIMUM_CAPACITY = 1 << 30);
  2. 生成新的数组newTable,长度是现有数组的2倍;
  3. 重新计算数组的相应变量,包括threshold和newMask;
  4. 旧数组中的元素迁移到新数组(下面详细讲解);
  5. 新表替换旧表

3、数组迁移的详细逻辑:

  1. 遍历旧数组,找到数组每个位置的头节点;
  2. 如果头节点不为空,则从头节点开始,遍历链表;
  3. 对每个节点判断是否key、value被GC回收,如果回收则清理节点
  4. 对每个没有被GC的节点,使用新的掩码newMask和key的哈希,得到一个新的值,这个新的值就是在新数组中的位置;
  5. 如果上一步计算得到的位置不再是现在的位置,则放入新位置

4、源码:

    /** Expands the table if possible. */
    @GuardedBy("this")
    void expand() {
      AtomicReferenceArray<ReferenceEntry<K, V>> oldTable = table;
      int oldCapacity = oldTable.length();
      if (oldCapacity >= MAXIMUM_CAPACITY) {
        return;
      }

      /*
       * Reclassify nodes in each list to new Map. Because we are using power-of-two expansion, the
       * elements from each bin must either stay at same index, or move with a power of two offset.
       * We eliminate unnecessary node creation by catching cases where old nodes can be reused
       * because their next fields won't change. Statistically, at the default threshold, only about
       * one-sixth of them need cloning when a table doubles. The nodes they replace will be garbage
       * collectable as soon as they are no longer referenced by any reader thread that may be in
       * the midst of traversing table right now.
       */

      int newCount = count;
      AtomicReferenceArray<ReferenceEntry<K, V>> newTable = newEntryArray(oldCapacity << 1);
      threshold = newTable.length() * 3 / 4;
      int newMask = newTable.length() - 1;
      for (int oldIndex = 0; oldIndex < oldCapacity; ++oldIndex) {
        // We need to guarantee that any existing reads of old Map can
        // proceed. So we cannot yet null out each bin.
        ReferenceEntry<K, V> head = oldTable.get(oldIndex);

        if (head != null) {
          ReferenceEntry<K, V> next = head.getNext();
          int headIndex = head.getHash() & newMask;

          // Single node on list
          if (next == null) {
            newTable.set(headIndex, head);
          } else {
            // Reuse the consecutive sequence of nodes with the same target
            // index from the end of the list. tail points to the first
            // entry in the reusable list.
            ReferenceEntry<K, V> tail = head;
            int tailIndex = headIndex;
            for (ReferenceEntry<K, V> e = next; e != null; e = e.getNext()) {
              int newIndex = e.getHash() & newMask;
              if (newIndex != tailIndex) {
                // The index changed. We'll need to copy the previous entry.
                tailIndex = newIndex;
                tail = e;
              }
            }
            newTable.set(tailIndex, tail);

            // Clone nodes leading up to the tail.
            for (ReferenceEntry<K, V> e = head; e != tail; e = e.getNext()) {
              int newIndex = e.getHash() & newMask;
              ReferenceEntry<K, V> newNext = newTable.get(newIndex);
              ReferenceEntry<K, V> newFirst = copyEntry(e, newNext);
              if (newFirst != null) {
                newTable.set(newIndex, newFirst);
              } else {
                removeCollectedEntry(e);
                newCount--;
              }
            }
          }
        }
      }
      table = newTable;
      this.count = newCount;
    }

3.4 驱逐缓存

    驱逐缓存的方法evictEntries主要在新加入缓存元素后触发,用来从访问队列中按LRU算法驱逐缓存,直到segment的当前的权重值不大于最大权重值。需要注意的是,由于容量限制进行LRU算法时,是按找访问时间进行LRU的,而不是按照写入时间进行的。

    主要逻辑:

  1. 先将最近访问的元素按顺序从recencyQueue存入到accessQueue;
  2. 判断新加入的元素是否过大,超过了segment的容量。如果超过了容量,则直接丢弃;
  3. 如果最大权重仍然超限,则不断从accessQueue中获取元素并进行驱逐,直到不再超限。

    源码如下:

    /**
     * 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;
      }

      // 把recencyQueue的内容全部按FIFO顺序放入到accessQueue,并把recencyQueue清空
      drainRecencyQueue();

      // 如果新加入的节点权重太高,超过了segment的最大容量,则直接驱逐
      // 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();
        }
      }

      // 如果目前的全部权重totalWeight仍然大于最大权重,则不断从accessQueue中取出元素并驱逐
      while (totalWeight > maxSegmentWeight) {
        ReferenceEntry<K, V> e = getNextEvictable();
        if (!removeEntry(e, e.getHash(), RemovalCause.SIZE)) {
          throw new AssertionError();
        }
      }
    }

1、排空recencyQueue队列drainRecencyQueue:

    drainRecencyQueue方法涉及到两个队列:

  • recencyQueue:每次读操作都会将读操作访问的元素添加到recencyQueue的队尾;每次写操作都会将recencyQueue的元素都放入accessQueue,并清空recencyQueue
  • accessQueue:accessQueue用于存放访问元素的顺序。accessQueue的类型是Guava自定义的AccessQueue,每次在写入一个元素时,会把这个元素写入前在accessQueue的位置替换成队尾,而不是直接放到队尾,避免了一个元素在队列的多个位置重复存在。
    /**
     * Drains the recency queue, updating eviction metadata that the entries therein were read in
     * the specified relative order. This currently amounts to adding them to relevant eviction
     * lists (accounting for the fact that they could have been removed from the map since being
     * added to the recency queue).
     */
    @GuardedBy("this")
    void drainRecencyQueue() {
      ReferenceEntry<K, V> e;
      while ((e = recencyQueue.poll()) != null) {
        // An entry may be in the recency queue despite it being removed from
        // the map . This can occur when the entry was concurrently read while a
        // writer is removing it from the segment or after a clear has removed
        // all of the segment's entries.
        // 每次写入元素的时候,都会将元素写入到accessQueue,所以如果recencyQueue的元素在accessQueue不存在,表示这个元素在真实的缓存中也不存在
        // 这种情况出现的场景是:有其他线程并行的移除了这个元素
        if (accessQueue.contains(e)) {
          // accessQueue的add方法实际上是调用了offer方法,可以看下下面的源码
          accessQueue.add(e);
        }
      }
    }

2、AccessQueue实现的offer方法:

    @Override
    public boolean offer(ReferenceEntry<K, V> entry) {
      // unlink
      // connectAccessOrder用来拼接入参中的两个节点
      // 这个方法是用来将元素从现有顺序中移除的。可以看下下面的方法源码
      connectAccessOrder(entry.getPreviousInAccessQueue(), entry.getNextInAccessQueue());

      // 这两行代码,实际上就是将entry放到head和head.previous之间
      // 清理的时候,是从head.next开始清理的。相当于,head.next是实际的队首元素,head.previous是实际的队尾元素
      // 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);
  }

3、移除节点removeEntry

    这个方法是从table中找到entry节点,然后调用removeValueFromChain方法删除节点。

    @VisibleForTesting
    @GuardedBy("this")
    boolean removeEntry(ReferenceEntry<K, V> entry, int hash, RemovalCause cause) {
      int newCount = this.count - 1;
      AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
      int index = hash & (table.length() - 1);
      ReferenceEntry<K, V> first = table.get(index);

      for (ReferenceEntry<K, V> e = first; e != null; e = e.getNext()) {
        if (e == entry) {
          ++modCount;
          ReferenceEntry<K, V> newFirst =
              removeValueFromChain(
                  first,
                  e,
                  e.getKey(),
                  hash,
                  e.getValueReference().get(),
                  e.getValueReference(),
                  cause);
          newCount = this.count - 1;
          table.set(index, newFirst);
          this.count = newCount; // write-volatile
          return true;
        }
      }

      return false;
    }

    从链表移除value:

    removeValueFromChain方法封装了移除一个value的底层细节,包括:

  • 入队缓存删除通知;
  • 从writeQueue队列移除。这个队列记录的是写entry的顺序;
  • 从accessQueue移除。上面有介绍,这个队列记录的是访问accessQueue的顺序;
  • 如果节点正在加载,则节点类型是LoadingValueReference,此时就失效掉旧值,即把旧值oldValue设置为UNSET;
  • 如果节点不在加载,就删除掉整个entry。
    @GuardedBy("this")
    @Nullable
    ReferenceEntry<K, V> removeValueFromChain(
        ReferenceEntry<K, V> first,
        ReferenceEntry<K, V> entry,
        @Nullable K key,
        int hash,
        V value,
        ValueReference<K, V> valueReference,
        RemovalCause cause) {
      enqueueNotification(key, hash, value, valueReference.getWeight(), cause);
      writeQueue.remove(entry);
      accessQueue.remove(entry);

      if (valueReference.isLoading()) {
        valueReference.notifyNewValue(null);
        return first;
      } else {
        return removeEntryFromChain(first, entry);
      }
    }

    从链表移除节点entry:

    对于entry所在的链表,entry后面的节点先拼在链表的头节点后面,然后使用头插法从头节点开始一个节点一个节点往链表的最前面插,知道碰到entry为止。

    示例:原链表:1->2->3->4->5->6,其中4是需要删除的节点,则使用下面的逻辑移除4以后,顺序变成:3->2->1->5->6

    @GuardedBy("this")
    @Nullable
    ReferenceEntry<K, V> removeEntryFromChain(
        ReferenceEntry<K, V> first, ReferenceEntry<K, V> entry) {
      int newCount = count;
      ReferenceEntry<K, V> newFirst = entry.getNext();
      for (ReferenceEntry<K, V> e = first; e != entry; e = e.getNext()) {
        // 这里需要重点关注一下:复制e的值复制生成新节点next,同时将next的下一个节点设置为newFirst
        ReferenceEntry<K, V> next = copyEntry(e, newFirst);
        if (next != null) {
          newFirst = next;
        } else {
          removeCollectedEntry(e);
          newCount--;
        }
      }
      this.count = newCount;
      return newFirst;
    }

 

  • 7
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值