LoadingCache学习

2021-07-23日  晚9点

本文主要意思是:学习LoadingCache本地缓存特性和源码。
文中会引用大量的官网观点.

一定要看get源码,注意:回收/删除的动作,可能并不是自动删除,因为涉及到工作线程和用户线程竞争锁。

案例

private static final LoadingCache<String, Long> cache = CacheBuilder.newBuilder()
            .maximumSize(100)
            .expireAfterWrite(10, TimeUnit.SECONDS)
            .removalListener((RemovalListener<String, Long>) notification -> {
                System.out.println("删除监听器: key, value" + notification.getKey() + "," + notification.getValue());
            })
            .build(new CacheLoader<String, Long>() {
                @Override
                public Long load(String key) throws Exception {
                    long curTime = System.currentTimeMillis();
                    System.out.println("curTime=" + curTime);
                    return curTime;
                }
            });

适用性

官方说明

Guava Cache 与ConcurrentMap 很相似,但是也不完全相同。最基本的区别是ConcurrentMap会一直保存所有添加的元素,直到显示的删除。
相对地,Guava Cache 为了限制内存占用,通常都会设定自定回收元素。在某些场景下,尽管LoadingCache 不回收元素,它也是很有用的,因为它会自动加载缓存。

后面这句话其实就是说LoadingCache自动加载缓存是优点: get(key, load); || 重写 load 自动触发   --> 这里的自动其实都不是自动的, 必须要有读/写请求来的时候, 才会触发

通常来说,Guava Cache适用于: 
  • 你愿意消耗一些机器内存空间来提升速度。
  • 你预料到某些键会被查询一次以上。
  • 缓存中存放的数据总量不会超出内存总量。(因为Guava Cache是单个应用运行时的本地缓存,它不把数据存储到文件或外部服务器。如果此时不符合你的业务需求,请尝试Memcached这类工具。)

如果以上场景符合你的业务需要, Guava Cache就适合你。
注意: 如果你不需要Cache 中的特性, 使用ConcurrentHashMap 有更好的内存效率–但 Cache的大多数特性都很难基于旧的ConcurrentMap 复制, 甚至根本不可能做到.

也就是说,官方建议我们使用Guava Cache (在符合场景的前提下)

功能

LoadingCache 是一个支持多线程并发读写、高性能、通用的in-heap(堆)本地缓存,有以下功能:

  • 核心功能: 高性能线程安全的in-heap Map 数据结构(类似于ConcurrentHashMap)
  • 支持key不存在时按照给定的CacheLoader 的loader方法进行loading。如果有多个线程同时get一个不存在的key,那么会有一个线程负责load,其他线程阻塞wait等待。
get方法中: 

    // at this point e is either null or expired; 
    // 翻译: 此时get获取到的元素为 null / 过期
    return lockedGetOrLoad(key, hash, loader);  
    // 啥意思? 加锁去get或者执行load方法。 

lockedGetOrLoad 方法

    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.
      synchronized (e) {
        return loadSync(key, hash, loadingValueReference, loader);
      }
    } finally {
      statsCounter.recordMisses(1);
    }
  • 支持entry的evitBySize, 这是一个LRU cache的基本功能. --> 啥意思
  • 支持对entry设置过期时间, 按照最后访问/写入的时间来淘汰entry.
  • 支持传入entry删除事件监听器, 当entry被删除或者淘汰时执行监听器逻辑.
  • 支持对entry进行定时reload, 默认使用loader逻辑进行同步reload (建议重写 CacheLoader的reload方法实现异步reload)
原生CacheLoader 此时执行这个方法是'同步', 不过返回值确是ListenableFuture
    @Override
    public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception{
    //    return Futures.immediateFuture(load(key));  
        return super.reload(key, oldValue);  
        // 这里直接调用了load方法去获取返回值,然后将返回值设置到一个ImmediateFuture类型的future中。本质上没有异步执行。
    }


如果需要refresh是异步的, 不影响用户线程.
此时需要: 自己使用写一个类AsyncReload, 实现reload方法. 实际上就是启动一个'异步线程',不阻塞用户线程.
    相对于原生的CacheLoader, 也只是只少了'一个'线程阻塞.
AsyncReload
    final static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
    @Override
    public ListenableFuture<V> reload(K key, V oldValue) throws Exception {
        ListenableFutureTask<V> task = create(() -> load(key));
        service.execute(task);
        return task;
    }

    .build(new AsyncReload<String, Long>() {
        load..

        @Override
        public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception {
            return super.reload(key, oldValue);
        }
    });
  • 支持WeakReference封装key, 当key在应用程序里没有别的引用是, jvm会gc回收key对象, LoadingCache也会得到通知从而进行清理.
  • 支持用WeakReference 或者 SoftReference 封装value对象
  • 支持缓存相关运行数据的统计.

关于load方法和reload方法说明:

load方法: 第一次加载, 或key对应的value不存在(已经过期)会触发load方法. 
    load方法是同步的, 对于同一个key, 多次请求只能触发一次加载. 比如thread1访问key1,
    发现cache中不存在key1, 触发key1的load, 同时, 在加载过程完成之前, 其他线程都访问key1, 
    此时这些访问都不会触发key1的load加载, 因为加载load方法被synchronized修饰.
    synchronized (e) {
        return loadSync(key, hash, loadingValueReference, loader);
    }

reload方法: 
    1. 当cache中有值, 但是需要刷新的该值的时候就会触发reload方法. ('不是自动的')
    2. loadingCache的所有更新操作都是'依赖读写操作'触发的, 因为内部没有定时任务或者时钟信号. 
        例如, 上一次写入之后超过了refresh设置的更新时间, 但是之后没有cache的访问了, 
        此时之后下一次get的时候才会进行触发refresh.....   
        比如: 20s 后过期, 5s刷新一次, main开始, 写入一次. 此时初始的5s内(包括第5s)
        没有get请求, 此时不会自动触发refresh, 而是后面15s内来了一个get请求,
        此时才会触发refresh.
    3. 对于同一个key, '多次请求只有一个线程触发reload, 其他请求直接返回旧值'.
        比如: 7s的时候有两个线程访问cache, 只会有一个线程refresh.
        如果是原生CacheLoader, 此时这两个线程是同步执行的. 其中有一个必然会触发refresh, 就看操作系统把锁给哪个线程了. 'A先refresh, B返回旧值'.  
        如果是AsyncLoader重写reload, create一个线程异步执行, '这两个线程返回旧值'. '异步线程执行触发refresh'.

只配置expireAfterWrite: load和refresh都阻塞.
在配置refreshAfterWrite之后, 执行reload时: 
    原生: 一个用户线程去refresh, 其他线程返回旧值.
    异步: 所有用户线程返回旧值, 新创建一个异步线程去refresh, '所以比原生只少了一个线程的阻塞'

主要的类和接口

  • CacheBuilder
    • Builder设计模式, 更友好的支持多参数的类的构建. CacheBuilder在build方法中, 会前面设置的参数, 全部传递个LoaclCache, 它自己实际不参与任何计算. 不错
  • CacheLoader
    • 抽象类, 用户从数据源加载数据, 业务也可以继承它, 重写load, reload.
  • Cache
    • 接口, 定义get, put, invalidate等方法, 这里只有cache的增删改操作, 没有数据加载的操作.
  • AbstractCache
    • 抽象类, 继承自Cache接口. 其中批量操作都是循环执行单次行为, 而单次行为都没有具体定义.
  • LoadingCache
    • 接口, 继承自Cache接口. 定义get, getUnchecked, getAll等操作, 这些操作都会从数据源load数据.
  • AbstractLoadingCache
    • 抽象类, 继承自AbstractCache, 实现LoadingCache接口.
  • LoadCache(核心)
    • guava cache核心类, 包含了guava cache的数据结构以及基本的缓存的操作办法.
  • LocalManualCache
    • LocalCache内部静态类, 实现Cache接口. 其内部的增删改缓存操作全部调用成员变量 loaclCache的相关方法. (缓存的手动加载: 手动get)
  • LocalLoadingCache
    • LocalCache内部静态类, 继承自LocalManualCache类, 实现LoadingCache接口. 其所有操作也是调用成员变量localCache的相关方法. (缓存的自动加载: 自动get)

接口和类关系图: LocalLoadingCache, LocalManualCache, 是loaclCache内部静态类
主要类和接口图

使用

常用参数

  • expireAfterWrite: 当创建或写之后的固定有效期到达时, 数据会过期, 但是不会删除, 因为下一次请求来的时候, 会获取有效值, 无效则删除, 后同步指定load方法.
 V value = getLiveValue(e, now); // 获取存活的值 (如果expired, 则expired 包含: recencyQueue -> 入AccessQueue,删除(writeQueue删除, AccessQueue队列删除))

    V getLiveValue(ReferenceEntry<K, V> entry, long now) {
      if (entry.getKey() == null) {
        tryDrainReferenceQueues();
        return null;
      }
      V value = entry.getValueReference().get();
      if (value == null) {
        tryDrainReferenceQueues();
        return null;
      }

      if (map.isExpired(entry, now)) {   // 如果需要过期
        tryExpireEntries(now);
        return null;
      }
      return value;
    }

    void tryExpireEntries(long now) {
      if (tryLock()) {
        try {
          expireEntries(now);
        } finally {
          unlock();
      }
    }

    void expireEntries(long now) {
      drainRecencyQueue();   // 最近读队列recencyQueue -> 入AccessQueue(最近访问队列)

      ReferenceEntry<K, V> e;
      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();
        }
      }
    }
  • expireAfterAccess: 当创建或者或读之后的固定有效期到达时, 同上. 读写操作都会重置访问时间, 但asMap方法不会.
  • refreshAfterWrite: 当创建或者写之后的固定有效期到达时, 且新请求过来时, 数据会被自动刷新.(注意不是删除而是刷新(原生同步, 改造异步), 其他线程返回旧值)
  • concurrencyLevel: 更新操作之间允许的并发, 也就是segment的数量.
  • maximumSize: 指定缓存可以包含的最大entries数.
  • maximumWeight: 指定缓存可能包含的entries数的最大权重, 使用此方法需要先调用weigher方法.
  • weigher: 指定确定entries的weight的wegher, 通过重写weigh(K key, V value)方法, 来确定每一个Entries的weight.
  • weakKeys: 指定将缓存中存储的每个键都包装在WeakReference(默认情况下, 使用强引用)
  • weakValues: 指定将缓存中存储的每个值都包装在WeakReference(默认情况下, 使用强引用)
  • softValues: 指定将缓存中存储的每个值都包装在SoftReference(默认情况下, 使用强引用)

segment数据结构

WriteQueue:按照写入时间进行排序的元素队列,写入一个元素时会把它加入到队列尾部。

AccessQueue:按照访问时间进行排序的元素队列,访问(包括写入)一个元素时会把它加入到队列尾部。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

加载

获取

  • get 如果没有, cacheLoader 向缓存原子地加载新值.
  • getAll<Iterable<? extend K>>: 批量查询. 默认情况下, 对每个不在缓存中的键, getAll方法会单独调用CacheLoader.load来加载缓存项. 如果批量的加载比多个单独加载高效, 可以重载CacheLoader.loadAll 来利用这一点. getAll(iterable)的性能也会提升.

注: CacheLoader.loadAll 的实现可以为没有明确请求的键加载缓存值. 例如, 为某组中的任意键计算值时, 能够获取该组中的所有键值, loadAll方法就可以实现为在同一时间获取该组的其他键值. getAll(Iterable<? extends K>)方法会调用loadAll, 但会筛选结果, 只会返回请求的键值对.

显示插入

使用cache.put(key, value)方法可以直接向缓存中插入值, 这会直接覆盖掉给定键之前映射的值. 使用Cache.asMap()视图提供的任何方法也能修改缓存. 但一定要注意, asMap视图的任何方法都不能保证缓存项被原子地加载到缓存.
进一步说. asMap视图的原子运算在Guava Cache的原子加载范畴之外, 所以相比于Cache.asMap().putIfAbsent(K, V), Cache.get(K, Callable)应该总是优先使用.

源码分析 不外乎 get/put/remove 因为它类似于concurrentMap

LocalCache构建

 private static final LoadingCache<String, Long> notifyNoticeAndEmailCache = CacheBuilder.newBuilder() //
            .expireAfterWrite(
                    20, TimeUnit.SECONDS) // 缓存项在给定时间内没有被写访问(创建或覆盖),则回收: 10s失效
            .maximumSize(10)
            .removalListener((RemovalListener<String, Long>) notification -> {
                System.out.println("删除监听器: key, value" + notification.getKey() + "," + notification.getValue());
            })
            .refreshAfterWrite(5, TimeUnit.SECONDS)
            .build(new AsyncReLoad<String, Long>() {
                @Override
                public Long load(String key) {
                    long curTime = System.currentTimeMillis();
                    System.out.println("curTime=" + curTime);
                    return curTime;
                }

                @Override
                public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception {
                    System.out.println("异步线程, 执行");
                    return super.reload(key, oldValue);
                }
            });

以上是一个构建LoadingCache的小李子. 流程如下:

  1. 采用建造者模式, 首先创造出CacheBuilder.
  2. 使用流式风格, 依次设置参数.
  3. build方法创建完成.

其中以上参数都是CacheBuilder的属性, 在使用Build方法时, CacheBuilder被当做参数传入LoaclLoadingCache或LoaclManualCache的构造器中进行构造.

    public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
      CacheLoader<? super K1, V1> loader) {
    checkWeightWithWeigher();
    return new LocalCache.LocalLoadingCache<>(this, loader);   // 多个了一个loader
    }
    public <K1 extends K, V1 extends V> Cache<K1, V1> build() {
    checkWeightWithWeigher();
    checkNonLoadingCache();
    return new LocalCache.LocalManualCache<>(this);
  }

从上面代码可以看出来, 区别是在创建时是否传入CacheLoader, 分为’自动加载’与手动加载. 同时创建缓存时, 也将CacheBuilder传入(this).

static class LocalLoadingCache<K, V> extends LocalManualCache<K, V>
      implements LoadingCache<K, V> {

    LocalLoadingCache(
        CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) {
      super(new LocalCache<K, V>(builder, checkNotNull(loader)));
    }

static class LocalManualCache<K, V> implements Cache<K, V>, Serializable {
    final LocalCache<K, V> localCache;

    LocalManualCache(CacheBuilder<? super K, ? super V> builder) {
      this(new LocalCache<K, V>(builder, null));
    }

    private LocalManualCache(LocalCache<K, V> localCache) {
      this.localCache = localCache;
    }

从上面代码看出, LocalLoadingCache继承自LoadManualCache, 区别是: CacherLoader是否自主传入进来. 有/没有.

两个类都有一个LocalCache的引用, 其内部的增删改缓存操作都是调用成员变量localCache. 但是它两都是LocalCache的静态内部类.

LocalCache初始化

LocalCache首先把builder中的所有属性都取出来, 然后赋值给自己的所有成员变量.

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

  static final int MAXIMUM_CAPACITY = 1 << 30; // 缓存最大容量, 该数值必须为2的幂, 同时小于这个最大值 2^30 

  static final int MAX_SEGMENTS = 1 << 16; // segment数组最大容量

  static final int CONTAINS_VALUE_RETRIES = 3; // containsValue方法的重试次数

  static final int DRAIN_THRESHOLD = 0x3F;     // 在更新缓存的最近排序信息之前, 每个段可以缓冲的缓存访问操作的数量. 这是为了避免锁争用, 通过记录读的记忆和延迟获取锁,直到超过阈值或发生突变.

  static final int DRAIN_MAX = 16;            // 一次清理操作中, 最大移除的entry数量

  final int segmentMask;                      // 定位segment

  final int segmentShift; // 定位segment, 同时让entry分布均匀,尽量平均分布在每个segment[i]中
  final Segment<K, V>[] segments;

  final int concurrencyLevel;  // 更新操作之间允许的并发, 也就是**segment**的数量.

  下来我们来看一下LocalCache的构造
     LocalCache(
          CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) {
        // 并发程度, 根据传入参数和默认最大值中取小值
        // 如果没有指定参数的情况下, concurrencyLevel == UNSET_INT == -1 
        // 然后getConcurrencyLevel 方法判断上面这个方式成立, 返回默认值4
        // 否则返回用户传递进来的参数
        concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);

        // 键值的引用类型, 没有指定的话, 默认为强引用类型.
        keyStrength = builder.getKeyStrength();
        valueStrength = builder.getValueStrength();

        // 判断相同的方法, 强引用类型就是Equivalence.equals()  各个引用类型的判断下相同的方法都不同, 在  enum Strength 中可以看到
        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());
        // 创建新的缓存内容(entry) 的工厂, 会根据引用类型选择对应的工厂
        entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());
        globalStatsCounter = builder.getStatsCounterSupplier().get();
        defaultLoader = loader;

        // 初始化缓存容量, 默认为16
        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;
        // 根据并发程度来计算segement的个数, (大于等于concurrencyLevel的最小的2次幂, 这里即为 4)
        while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) {
          ++segmentShift;
          segmentCount <<= 1;
        }
        // 这里的segmentShift和segmentMask用来打散entry, 让缓存内容尽量均匀分布在每个segment下
        this.segmentShift = 32 - segmentShift;
        segmentMask = segmentCount - 1;

        // 初始化segment数组, 大小为4
        this.segments = newSegmentArray(segmentCount);

        // 每个segment的容量, 总容量 / 数组个数, 向上取整, 16 / 4 = 4
        int segmentCapacity = initialCapacity / segmentCount;
        if (segmentCapacity * segmentCount < initialCapacity) {
          ++segmentCapacity;
        }
        // segmentSize 为小于segmentCapacity的最大的2的次幂, 这里为4
        int segmentSize = 1;
        while (segmentSize < segmentCapacity) {
          segmentSize <<= 1;
        }
        // 初始化每个segment[i]
        // 注意: 判断是否有权重, 根据权重来初始化
        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 {
          // 权重为-1, 也就是基本上没有权重
          for (int i = 0; i < this.segments.length; ++i) {
            this.segments[i] =
                createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());
          }
        }
      }

2021年7月25日早11点:

Segment初始化

    static class Segment<K, V> extends ReentrantLock

        @Weak final LocalCache<K, V> map;
        volatile @MonotonicNonNull AtomicReferenceArray<ReferenceEntry<K, V>> table;
            final @Nullable ReferenceQueue<K> keyReferenceQueue;
        final @Nullable ReferenceQueue<V> valueReferenceQueue;
        final Queue<ReferenceEntry<K, V>> recencyQueue;
        final AtomicInteger readCount = new AtomicInteger();
        @GuardedBy("this")
        final Queue<ReferenceEntry<K, V>> writeQueue;
        @GuardedBy("this")
        final Queue<ReferenceEntry<K, V>> accessQueue;

初始化:
    Segment(
        LocalCache<K, V> map,
        int initialCapacity,
        long maxSegmentWeight,
        StatsCounter statsCounter) {
      this.map = map;
      this.maxSegmentWeight = maxSegmentWeight;
      this.statsCounter = checkNotNull(statsCounter);
      initTable(newEntryArray(initialCapacity));

      keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null;

      valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null;

      recencyQueue =
          map.usesAccessQueue()
              ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>()
              : LocalCache.<ReferenceEntry<K, V>>discardingQueue();

      writeQueue =
          map.usesWriteQueue()
              ? new WriteQueue<K, V>()
              : LocalCache.<ReferenceEntry<K, V>>discardingQueue();

      accessQueue =
          map.usesAccessQueue()
              ? new AccessQueue<K, V>()
              : LocalCache.<ReferenceEntry<K, V>>discardingQueue();
    }

从segment的数据结构来看, 它有以下几个队列和table, map组成:

  1. keyReferenceQueue
  2. valueReferenceQueue

keyReference和valueReferenceQueue 在build时设置了soft或weak时会被初始化,
用于接收gc回收key/value对象的通知, 队列中的元素是引用key/value的reference.
显然referenceQueue是线程安全的, 队列的生成者是jvm的gc线程, 消费者是LoadingCache自身.
当key对象除了reference之外没有别的地方引用时, 下次gc时对象会被回收,
同时referenceQueue会受到通知, 然后对应的entry会被清理掉.


其实就是一句话, 在弱引用/软引用的情况下gc回收的内容会放入这两个队列. 加速gc回收.

  1. recencyQueue

recencyQueue 启用条件和accessQueue一样。

  • 每次’访问操作’(读)都会将该entry加入到队列尾部,并更新accessTime。
  • 如果遇到写入操作,则将该队列内容排干,如果accessQueue队列中持有该这些 entry,然后将这些entry add到accessQueue队列。
  • 注意,因为accessQueue是非线程安全的,所以如果每次访问entry时就将该entry加入到accessQueue队列中,就会导致并发问题。
  • 所以这里每次访问先将entry临时加入到并发安全的ConcurrentLinkedQueue队列中,也就是recencyQueue中。
  • 在写入的时候通过加锁的方式,将recencyQueue中的数据添加到accessQueue队列中。
  • 如此看来,recencyQueue是为accessQueue服务的.
  1. accessQueue

按照访问时间进行排序的元素队列, 访问(包括写入)一个元素时会把它加入到队列尾部.

  1. writeQueue

按照写入时间进行排序的元素队列, 写入一个元素时会把它加入到队列尾部.

get

下面我们来介绍一下 get源码, !!! —重要

    @Override
    public V get(K key) throws ExecutionException {
      return localCache.getOrLoad(key);
    }
一路进来: 其实就定位到了那个segment中entry中的值
    V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
        int hash = hash(checkNotNull(key));
        return segmentFor(hash).get(key, hash, loader);
    }

Segment#get()
    V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        // count保存的是该segment中entry的数量, 如果为0, 就直接去加载
        if (count != 0) { // read-volatile
          // don't call getLiveEntry, which would ignore loading values
          // 获取entry, 并做一些清理工作, 在操作开始/结束. 默认清理DRAIN_MAX = 16次.
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            long now = map.ticker.read();
            // 在entry无效, 过期, 正在载入都会返回null, 如果返回不为null, 即为正常命中
            V value = getLiveValue(e, now);
            if (value != null) {
              // 如果有expiredAfterAccess, 更新entry的accessTime. finaly,  丢入recencyQueue 最近访问队列, 为啥不进入accessQueue. TODO 后面会解释... 
              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);
            }
          }
        }

        // 如果不存在或者过期, 就用loader方式加载, loader从上面来: 1. default, 2. get(key, loader)
        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 {
        // 清理, 清除操作总是伴随着写入进行的, 如果是过长时间没有写入, 那我们就根据读线程来完成清理.
        // 很长时间是多长?还记得前面有一个参数DRAIN_THRESHOLD = 0x3F = 3F = 63
        // (readCount.incrementAndGet() & DRAIN_THRESHOLD) == 0 
        // 也就是说 读64次就会清理一次, 具体是怎样清理的, 给你们透露一下, <font color='red'>**上面5个队列都会清理**</font>.......
        postReadCleanup();
      }
    }

Segment#lockedGetOrLoad(): 

  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;

      // 因为Segment继承自ReentrantLock, 所以有这个方法. 为啥会加锁, 因为有多线程.
      lock();
      try {
        // re-read ticker once inside the lock
        long now = map.ticker.read();
        preWriteCleanup(now);
        // 该segment下面的HashTable
        int newCount = this.count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        //这里也是为什么table的大小要为2的幂(最后index范围刚好在0-table.length()-1),  数据需要分散. 如果是奇数, 那么 & 之后永远在 奇数槽
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        //遍历链表
        for (e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) { 
            // keyEquivalence 是指匹配规则, equivalent 类似equals
            valueReference = e.getValueReference();
            // 如果正在载入, 此时不需要创建, 等待即可
            if (valueReference.isLoading()) {
              createNewEntry = false;
            } else {
              V value = valueReference.get();
              // 被gc回收 (配置了weakReference / SoftReference 弱引用/软引用)
              if (value == null) {
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
              } else if (map.isExpired(e, now)) {
                // 过期了呀
                // 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 {
                // 记录被读,== 更新accessTime, 和recency队列, 正常返回value值
                recordLockedRead(e, now);
                statsCounter.recordHits(1);
                // we were concurrent with loading; don't consider refresh
                return value;
              }

              // 针对被gc回收和过期的情况, 从写队列和最近访问队列中移除,
              // 因为后面重新载入后, 会再次添加到队列中
              // immediately reuse invalid entries
              writeQueue.remove(e);
              accessQueue.remove(e);
              this.count = newCount; // write-volatile
            }
            break;
          }
        }


        // 注意: 下面这段代码只是说正在加载中, 实际上没有把具体的值加载进去, 只是为了阻塞其他线程.
        if (createNewEntry) {
          // 先创建一个LoadingValueReference, 表示正在载入中
          loadingValueReference = new LoadingValueReference<>();

          if (e == null) {
            // 如果当前链表为null, 先创建一个头结点
            e = newEntry(key, hash, first);
            e.setValueReference(loadingValueReference);
            table.set(index, e);
          } else {
            e.setValueReference(loadingValueReference);
          }
        }
      } finally {
        unlock();
        // 执行清理, 确实是这样, 因为官网规定, 每次操作都会清空数据.
        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.
          synchronized (e) {
            // 同步加载
            return loadSync(key, hash, loadingValueReference, loader);
          }
        } finally {
          // 记录没有被命中
          statsCounter.recordMisses(1);
        }
      } else {
        // The entry already exists. Wait for loading.
        return waitForLoadingValue(e, key, valueReference);
      }
    }

Segment#loadSync(): 
    V loadSync(
        K key,
        int hash,
        LoadingValueReference<K, V> loadingValueReference,
        CacheLoader<? super K, V> loader)
        throws ExecutionException {
      // 调用loader, 看着是异步, 要注意不是异步!!!!!
      ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader);
      return getAndRecordStats(key, hash, loadingValueReference, loadingFuture);
    }

Segment#getAndRecordStats():
    // 等待载入, 并且记录是否成功
    V getAndRecordStats(
        K key,
        int hash,
        LoadingValueReference<K, V> loadingValueReference,
        ListenableFuture<V> newValue)
        throws ExecutionException {
      V value = null;
      try {
        value = getUninterruptibly(newValue);
        if (value == null) {
          throw new InvalidCacheLoadException("CacheLoader returned null for key " + key + ".");
        }
        // 性能统计是否加载成功!!
        statsCounter.recordLoadSuccess(loadingValueReference.elapsedNanos());
        `真正去把数据,载入到缓存中 (当前还是loadingValueReference, 表示isLoading 不过这个方法内部会把LoadingValueReference变成对应引用类型的ValueReference )`
        storeLoadedValue(key, hash, loadingValueReference, value);
        return value;
      } finally {
        loader方法数据为null, 或者store数据异常, 删除LoadingValue
        if (value == null) {
          statsCounter.recordLoadException(loadingValueReference.elapsedNanos());
          removeLoadingValue(key, hash, loadingValueReference);
        }
      }
    }

Segment#storeLoadedValue():
    boolean storeLoadedValue(
        K key, int hash, LoadingValueReference<K, V> oldValueReference, V newValue) {
      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);
        // 找到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)) {
            ValueReference<K, V> valueReference = e.getValueReference();
            V entryValue = valueReference.get();
            // replace the old LoadingValueReference if it's live, otherwise
            // perform a putIfAbsent
            if (oldValueReference == valueReference
                || (entryValue == null && valueReference != UNSET)) {
              ++modCount;
              if (oldValueReference.isActive()) {
                RemovalCause cause =
                    (entryValue == null) ? RemovalCause.COLLECTED : RemovalCause.REPLACED;
                enqueueNotification(key, hash, entryValue, oldValueReference.getWeight(), cause);
                newCount--;
              }
              // LoadingValueReference变成对应引用类型的ValueReference,并进行赋值操作
              setValue(e, key, newValue, now);
              this.count = newCount; // write-volatile
              evictEntries(e);
              return true;
            }

            // the loaded value was already clobbered
            enqueueNotification(key, hash, newValue, 0, RemovalCause.REPLACED);
            return false;
          }
        }

        ++modCount;
        ReferenceEntry<K, V> newEntry = newEntry(key, hash, first);
        setValue(newEntry, key, newValue, now);
        table.set(index, newEntry);
        this.count = newCount; // write-volatile
        evictEntries(newEntry);
        return true;
      } finally {
        unlock();
        // 清理
        postWriteCleanup();
      }
    }



总结一下get方法的过程:

  1. 指定segment中table中找到没有被回收、没有过期的entry, 如果找到了, 并且在CacheBuilder配置了refreshAfterWrite, 并且当前时间已经超过了这个时间, 则重新加载. 否则, 返回这个值.
  2. 找到的ValueReference是loadingValueReference(正在加载). 此时waitForLoadingValue阻塞等待.
  3. 如果没有找到entry, 或者找到为null/expired. lockedGetOrLoad, 加锁. 在table里面找对应key的entry,
    如果找到valueReference.isLoading() == true为正在加载中, 等待加载完, 拿到value值即可.
    如果找到值为非null&非expired, 返回. 否则, 创建一个LoadingValueReference, 调用loadSync真正加载相关的值入table.
  4. loadSync会执行loader, 然后给出ListenableFuture, 根据future去存储getAndRecordStats并且获取值. 存储storeLoadedValue.
    然后, 大部分情况下替换LoadingValueReference, 少部分情况是删除LoadingValueReference(此时 是因为load方法执行得到的数据是null, 抛出了异常.)

下面是get的大体流程图:
在这里插入图片描述


2021年8月1日14点:

put

下面我们来介绍一下 put源码, !!! —重要

 @Nullable
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
      lock();
      try {
        long now = map.ticker.read();
        // 1. 先去清理key/value 引用队列, 再调用expireEntries 删除writeQueue/accessQueue中过期的entry
        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;
              // 2. value被自动删除, 因为值被回收了
              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
              // 3. 判断权重是否超了, 超了就要被删除
              evictEntries(e);
              return null;
              // 4. 如果是存在&onlyIfAbsent==true
            } else if (onlyIfAbsent) {
              // Mimic
              // "if (!map.containsKey(key)) ...
              // else return map.get(key);
              // 5. 记录被读取, 然后返回值
              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();
      }
    }

public V put(K key, V value); // onlyIfAbsent为fale
public V putIfAbsent(K key, V value); // onlyIfAbsent为true 这个参数的意思是如果缓存中没有才会写入数据哦

下面是put的大概流程:
在这里插入图片描述

preWriteCleanup

void preWriteCleanUp(long now)
传入当前时间
作用: 清理keyQueue和valueQueue中被GC, 等待清除的entry信息.
过期掉 writeQueue和accessQueue中entry.

evictEntries

void evictEntries(ReferenceEntry<K, V> newest);
传入参数为最新的entry, 可能是最新插入的, 也可能是刚更新过的.
这个方法只有设置了maximumSize才会进行.
作用/步骤: 1. 清除recencyQueue, 判断该元素自身的权重是否超过上限, 如果超过则移除元素.
2. 判断总的权重是否大于上限, 如果超过则去accessQueue找到队首(即最不常访问元素)进行移除, 直到小于上限.

remove

public V remove(@Nullable Object key);
调用LocalManualCache的invalidate(Object key)方法即可调用remove.

refresh 其实底层就是同步执行了load方法

数据结构

recencyQueue和accessQueue

如果在build缓存时设置了expiredAfterAccess || maxWeight, recencyQueue于accessQueue就会被初始化.

recencyQueue是最近读队列, accessQueue是最近访问队列(读写). 其中最近读队列是采用的是jdk中线程安全、支持高并发读写的ConcurrentLinkedQueue, 队列中存储的元素是ReferenceEntry. 最近访问队列采用的是LoadingCache自己实现的AccessQueue, 节点是ReferenceEntry、非线程安全. (带头结点的双向链表)

用两个队列实现LRU的原因为:
我们首先来看一下缓存下为啥要用LRU:

  1. 缓存的场景基本是读多写少, LoadingCache的读操作要做到高性能、lock-free的读, 这样就会有多个线程同时读缓存, 意味着LRU队列支持多线程高并发写入(调整元素在LRU队列中的访问顺序)
  2. LoadingCache中元素可能会过期、容量限制、被gc回收等原因被淘汰出缓存, 意味着需要从LRU队列中高效删除元素. 因此我们需要一个支持多线程并发访问的、常数时间删除元素的队列实现.
    显然一个ConcurrentLinkedQueue不能同时满足这两个需求, Guava给的解就是再增加一个简单的AccessQueue做到常数时间删除元素. 具体来说:
  • 每次无锁的读操作都会去写而且只会写最近读队列(将entry直接入队, 方法: recordRead)
  • 每次锁保护下写操作都会涉及到最近访问队列的读写,
    1. 比如每次想缓存新增元素都会做几次清理工作. 清理就需要读accessQueue(淘汰掉队头的元素, 见方法写前expiredEntries, 写后evictEntries);
    2. 每次向缓存新增元素成功后记录元素写操作, 记录会写accessQueue(加到队尾, 见方法recordWrite).
    3. 每次访问accessQueue前都需要先排干recencyQueue至accessQueue中(key相同的, 按先进先出顺序, ==批量调整accessQueue中元素顺序), 然后再去进行accessQueue的读或者写操作, 以尽量保证accessQueue中元素顺序和真实的最近访问顺序一致. (见方法: drainRecencyQueue)

关于这个做法其实是有以下几个问题:

  • 如果在写少读非常多的场景下, 读写accessQueue的机会很少, 大量读操作会在recencyQueue中累积很多元素占用内存而得不到排干的机会. 所以Guava为了解决这个难问题, 为读操作设置了一个DRAIN_THRESHOLD == 63(方法: postReadCleanup), 当累积读次数达到排干阈值时64也会触发一次清理操作, 从而排干recenceyQueue到accessQueue
  • 在高并发场景下, 即使是在每次读写accessQueue前做排干recencyQueue操作, 也不能保证accessQueue中元素顺序和实际元素访问顺序一致. 这个问题也没有那么严重,导致结果无非就是LRU淘汰过程没那么deterministic, 对于一个缓存来说也可以接受.

writeQueue

最近写队列: 如果build缓存时设置expireAfterWrite, 创建segment时就会初始化writeQueue。

实现: 十分简单, 就是一个带头节点的双向循环链表, 并且没有任何并发. 节点对象就是ReferenceEntry本身.
注意到segment中hash表里的散列链表节点也是ReferenceEntry, 这就有一个技巧了: 即一个节点对象可能会同时属于多个链表中, 不同链表使用不同的前后节点指针, 这个技巧的好处在于给定一个节点entry对象, 所有链表都可以做到常数时间的查找和删除(jdk里面的linkedHashMap实现也采用了这个技巧).
由于写操作都会在锁保护进行, 因此这个writeQueue无需是线程安全的.

缓存回收

一个残酷的现实是, 我们好像没有足够的内存缓存所有数据. 你必须决定: 什么时候某个缓存项不值得保留了? Guava Cache 提供了三种基本的缓存回收方式: 基于容量回收、定时回收和基于引用回收.

基于容量回收

如果要规定缓存项的数目不超过固定值, 只需使用CacheBuilder.maximumSize(long). 缓存将尝试回收最近没有使用或者总体上没有使用的缓存项. ----- 注意: 在缓存项的数目达到限定值之前, 缓存就可能进行回收操作— 通常来说, 这种情况发生在缓存项的数目逼近限定值时.

另外, 不同缓存项有不同的权重(weights) --例如, 如果你的缓存值, 占据完全不同的内存空间, 你可以使用CacheBuilder.weight(Weighter)指定一个权重函数, 并可以用CacheBuilder.maximumWeight(long)指定最大总重.

定时回收

CacheBuilder提供了两种定时回收的方法:

  • expiredAfterAccess(long, TimeUnit): 没有在给定时间内访问(读/写), 回收
  • expiredAfterWrite(long, TimeUnit): 没有在给定时间内写, 回收

如下文: 定时回收大多是情况是在写进行, 偶尔在读进行

基于引用回收

通过使用弱引用的键/值, 软值. Cuava Cache可以把缓存设置为允许垃圾回收:

  • CacheBuilder.weakKeys():
  • CacheBuilder.weakValues():
  • CacheBuilder.softKeys():

显示清除

任何时候, 我们都可以显式地清除缓存项, 而不是等着触发回收/被回收:

  • 单个清除: Cache.invalidate(key)
  • 批量清除: Cache.invalidateAll(key)
  • 清除所有缓存项: Cache.invalidateAll()

移除监听器

通过CacheBuilder.removeListener(RemovalListener), 你可以定义一个监听器, 方便在缓存项被删除的时候做一些额外操作, removalListener,会获得移除通知removalNotification, 其中包含移除原因removalCause, 键/值.

    .removalListener((RemovalListener<String, Long>) notification -> {
        System.out.println("删除监听器: key, value" + notification.getKey() + "," + notification.getValue());
    })

注意: 默认情况下, 监听器方法是在移除缓存时同步调用的. 因为缓存的维护和请求响应通常是同时进行的, 代价高昂的监听器方法在同步模式下会拖慢正常的缓存请求. 此时, 可以使用removalListeners.asynchronous(removalListener, executor)把监听器装饰为异步操作. --------------- 有待观察, 因为没看过底层

清理什么时候发生!! 重要!!

使用CacheBuilder构建的缓存是不会自动执行清理和回收工作, 也不会在某个缓存项过期后马上清理, 也没有诸如此类的清理机制. 相反, 它会在写操作时顺带做少量的维护工作, perWriteCleanUp->expiredEntries(write, accessQueue), evictEntries, postCleanUp, 或者偶尔在读操作时做—如果写操作实在是太少了.

这样做的原因是: 如果要自动地持续清理缓存, 就必须有一个线程, 这个线程会和用户线程竞争共享锁. 此外, 某些环境下线程创建可能受限制, 这样CacheBuilder其实就不太好用了.

其实这样做挺好的, 清理选择权交给自己手里, 可以手动Cache.cleanUp. ScheduledExecutorService可以帮助你更好的实现定时调度.

刷新

刷新和回收不太一样. 正如loadingCache.refresh(K)所说, 其实就是为键加载值, 这个过程其实可以是异步的. 在刷新操作进行时, 缓存仍然可以向其他线程返回旧值, 而不像回收操作, 读缓存的的线程必须等待新值加载完成.

如果刷新过程抛出异常, 缓存将保留旧值, 而异常会记录到日志后被丢弃[swallowed].
重载CacheLoader.reload(K, V)可以扩展刷新时的行为, 这个方法允许开发者在计算新值时使用旧的值. 废话. 底层其实就是同步执行了load方法, 不过其他线程refresh访问时, 有旧值返回.

下面是异步线程:



    final static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
    private static final LoadingCache<String, Long> graphs = CacheBuilder.newBuilder()
            .maximumSize(1000)
            .refreshAfterWrite(8, TimeUnit.SECONDS)
            .build(new CacheLoader<String, Long>() {
                @Override
                public Long load(String key) {
                    long timeMillis = System.currentTimeMillis();
                    System.out.println("load, time=" + timeMillis);
                    return timeMillis;
                }

                @Override
                public ListenableFuture<Long> reload(String key, Long oldValue) throws Exception {
                    // asynchronous!
                    ListenableFutureTask<Long> task =
                            ListenableFutureTask.create(new Callable<Long>() {
                                @Override
                                public Long call() throws Exception {
                                    long timeMillis = System.currentTimeMillis();
                                    System.out.println(Thread.currentThread().getName() + " reload, time= " + timeMillis);
                                    // 其实就是异步线程太快了, 以至于10s时有部分线程请求到了新值, 我们可以人为让异步线程放慢, 这时, 全部线程都会拿到旧值.
                                    Thread.sleep(TimeUnit.SECONDS.toMillis(2));
                                    return timeMillis;
                                }
                            });
                    service.execute(task);
                    return task;
                }
            });
    main开始请求:
       new Thread(() -> {
            try {
                Thread.sleep(TimeUnit.SECONDS.toMillis(0));
                graphs.get(key1);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }

        }).start();

        new Thread(() -> {
            try {
                Thread.sleep(TimeUnit.SECONDS.toMillis(10));
                Long a1 = graphs.get(key1);
                System.out.println(Thread.currentThread().getName() + " a1 " + a1);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }

        }).start();
        new Thread(() -> {
            try {
                Thread.sleep(TimeUnit.SECONDS.toMillis(12));
                Long a2 = graphs.get(key1);
                System.out.println(Thread.currentThread().getName() + " a2 " + a2);
            } catch (InterruptedException | ExecutionException e) {
                e.printStackTrace();
            }

        }).start();
    // a1, a2 都拿到的是旧值

注意: CacheBuilder.refreshAfterWrite(long, TimeUnit) 可以为缓存定时自动的刷新缓存. 缓存项只有被检索/访问时才会真正刷新, 这一点就不在赘述了. 上面有原因.

其他特性

统计

CacheBuilder.recordStats() 用来开启Guava Cache统计功能. 统计打开后, Cache.stats() 方法会返回CacheStats 对象以提供如下统计信息:

  • hitRate(): 缓存命中率;
  • averageLoadPenalty(): 加载新值的平均时间, 单位为ns.
  • evictionCount(): 缓存项被回收的总数, 不包括显示清楚.
    此外,还有其他很多统计信息。这些统计信息对于调整缓存设置是至关重要的,在性能要求高的应用中我们建议
    密切关注这些数据。

adMap视图

asMap 视图提供了缓存的ConcurrentMap 形式, 但asMap视图和缓存交互需要注意:

  • cache.asMap() 包含当前所有加载到缓存的项. 因此相应地, cache.asMap().keySet() 包含当前所有已加载键;
  • asMap().get(key) 实质上等同于cache.getIfPresent(key), 而且不会引起缓存项的加载. 这和map的约定是一致的.
  • 所有读写操作都会重置相关缓存项的访问时间, cache.asMap().get, put, 但是不包括.containsKey(), 还有一些对Cache.asMap()的集合视图上的操作, 比如asMap().entrySet()不会重置缓存项的读取时间.

中断

待处理

refreshAfterWrite 和expiredAfterWrite 一般怎样设置/关系, 业务

四种引用

  • 强引用: 创建一个对象并给它赋值一个引用, 引用是存在于jvm的栈中.
  • 软引用: 如果一个对象具有软引用, 内存空间足够, 不回收, 否则, 回收. 看内存空间自己的度量.
  • 弱引用: 垃圾回收器才不管你内存空间够不够, 直接回收. 看垃圾回收器心情.
  • 虚引用: 虚引用其实和上面的软引用和弱引不同, 它并不影响对象的生命周期. 如果一个对象与虚引用关联, 则跟没有引用之前一样, 在任何时候都可能被垃圾回收器回收.

注: 虚引用(PhantomReference)的必须和引用队列(ReferenceQueue)一起使用, 当垃圾回收器准备回收一个对象时, 如果发现它还有虚引用, 就会把这个虚引用加入引用队列. 程序可以通过判断引用队列中是否加入了虚引用, 来了解被引用的对象是否将要被垃圾回收. 如果程序发现某个虚引用已经被加入了引用队列, 那么就可以在所引用的对象的内存被回收之前采取必要的行动.

异步线程 (看他源码)

  1. 怎样创建一个异步线程

final static ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newCachedThreadPool());
ListenableFutureTask<Long> task =
        ListenableFutureTask.create(new Callable<Long>() {
            @Override
            public Long call() throws Exception {
                return timeMillis;
            }
        });
service.execute(task);
return task;
  1. feture->listenFeture

concurrentHashMap1.7 -> 1.8

本文参考:

https://blog.csdn.net/zjccsg/article/details/51932252
https://www.jianshu.com/p/38bd5f1cf2f2
http://www.qishunwang.net/knowledge_show_177365.aspx

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下次遇见说你好

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值