Guava Cache

缓存一般分为远程缓存和本地缓存。常见的远程缓存有 redis。与远程缓存相比本地缓存不需要进行网络的传输所以更加的快。本地缓存可以使用 map 实现,使用 get 查询缓存,put 设置缓存。本地缓存需要提供对应的缓存清理机制,否则会导致缓存溢出,并且需要保证操作的并发安全。

介绍

Guava Cache 是 Google Guava 库中的一个组件,用于在内存中实现本地缓存。它类似于 ConcurrentMap,但提供了更丰富的特性,如自动加载、过去策略、淘汰策略等。

Guava Cache 提供了丰富的构造参数:

参数类别 参数名 数据类型 作用
容量控制 initialCapacity int 初始化容量
maximumSize long 缓存最大条目数,超过时按LRU淘汰
maximumWeight long 缓存最大权重和,需配合 weigher 使用,超过时淘汰条目
weigher weigher<K,V> 定义计算缓存条目权重的函数,与maximumWeight 配合使用
并发控制 concurrencyLevel int 设置并发级别,并发级别是指同时可以写缓存的线程数
过期策略 expireAfterWrite long,TimeUnit 条目在写入后一段时间过期
expireAfterAccess long,TimeUnit 条目在最后一次访问(读或写)后一段时间过期
refreshAfterWrite long,TimeUnit 条目在写入后一段时间自动刷新(仅LoadingCache支持)
引用基淘汰 weakKeys 将键设置为弱引用,允许GC回收
weakValues 将值设置为弱引用,允许GC回收
softValues 将值设置为软应用,在内容不足时GC优先回收
移除监听 removalListener 设置缓存条目被移除时的监听器
统计 recordStats 开启缓存统计功能(如命中率)

初始化

初始容量设置

我们在构建缓存时可以为缓存设置一个合理大小初始容量,由于Guava的缓存使用了分离锁的机制,扩容的代价非常昂贵。所以合理的初始容量能够减少缓存容器的扩容次数。

java 复制代码
Cache<String, String> cache = CacheBuilder.newBuilder()
                .initialCapacity(10) // 初始化容量
                .build();

最大容量

Guava Cache可以在构建缓存对象时指定缓存所能够存储的最大记录数量。当Cache中的记录数量达到最大值后再调用put方法向其中添加对象,Guava会先从当前缓存的对象记录中选择一条删除掉,腾出空间后再将新的对象存储到Cache中。

java 复制代码
 Cache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(100) // 设置缓存最大容量,超过根据LRU算法回收
                .build();

监听器

removalListener() 设置缓存条目被移除时的监听器

java 复制代码
package guava;

import com.google.common.cache.*;

public class GuavaCacheTest {
    public static void main(String[] args) {
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(1)
                .removalListener(removal -> {
                    System.out.println(removal.getKey() + "被删除了");
                })
                .build();

        cache.put("key1", "value1");
        cache.put("key2", "value2");
        cache.put("key3", "value3");
    }
}

输出:

java 复制代码
key1被删除了
key2被删除了

缓存清除策略

基于存活时间

expireAfterWrite 写入缓存后n秒过期

java 复制代码
 Cache<String, String> cache = CacheBuilder.newBuilder()
                .expireAfterWrite(1, TimeUnit.SECONDS) // 设置写缓存后n秒过期
                .builder();

expireAfterAccess 读写缓存后n秒过期

java 复制代码
 Cache<String, String> cache = CacheBuilder.newBuilder()
                .expireAfterAccess(1, TimeUnit.SECONDS) // 设置读写缓存后n秒过期
                .builder();

基于最大容量容量

使用 CacheBuilder.maximumSize()方法时,当缓存数量达到该最大值时,将清除掉那些最近最少使用的缓存(LRU)
基于权重

在缓存大型文件或图片的场景下,每个缓存条目(文件内容)的大小差异很大,如果我们只限制缓存条目的数量(maximumSize)是不合理的。比如,缓存10个1MB的图片和缓存10个100MB的视频,占用的内存天差地别。

使用基于权重的回收(maximumWeight + weigher),我们可以限制缓存的总内存占用,而不是条目数量。

下面定义了一个最大权重为10的 cache,当所有条目的内存占用超过10时进行回收。

java 复制代码
package guava;

import com.google.common.cache.*;

public class GuavaCacheTest {
    public static void main(String[] args) {
        // 1. 创建缓存,设置最大权重为10
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumWeight(10) // 总权重不能超过10
                .weigher((Weigher<String, String>) (key, value) -> value.length()) // 权重=值的长度
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) {
                        return key.toUpperCase(); // 简单的加载逻辑:转大写
                    }
                });

        // 2. 使用缓存
        try {
            System.out.println("获取 A: " + cache.get("a"));      // 权重=1 (A)
            System.out.println("获取 Hello: " + cache.get("hello")); // 权重=5 (HELLO)
            System.out.println("获取 Test: " + cache.get("test"));   // 权重=4 (TEST)

            // 现在总权重=1+5+4=10,刚好满
            // 为了看到效果,我们手动检查哪些还在
            System.out.println("\n缓存中现有内容:");
            System.out.println("A: " + cache.getIfPresent("a"));     // 可能被回收
            System.out.println("Hello: " + cache.getIfPresent("hello")); // 可能被回收
            System.out.println("Test: " + cache.getIfPresent("test"));   // 可能还在

            System.out.println();
            // 3. 尝试再放入一个,会触发回收
            System.out.println("获取 World: " + cache.get("world")); // 权重=5 (WORLD)

            // 为了看到效果,我们手动检查哪些还在
            System.out.println("\n缓存中现有内容:");
            System.out.println("A: " + cache.getIfPresent("a"));     // 可能被回收
            System.out.println("Hello: " + cache.getIfPresent("hello")); // 可能被回收
            System.out.println("Test: " + cache.getIfPresent("test"));   // 可能还在
            System.out.println("World: " + cache.getIfPresent("world")); // 新加入的

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

执行结果:

java 复制代码
获取 A: A
获取 Hello: HELLO
获取 Test: TEST

缓存中现有内容:
A: A
Hello: HELLO
Test: TEST

获取 World: WORLD

缓存中现有内容:
A: null
Hello: null
Test: TEST
World: WORLD

显示清除

清除单个key

java 复制代码
 cache.invalidate("key1");

清除所有key

java 复制代码
cache.invalidateAll();

使用弱引用储存键。在下个GC时,若键在缓存外部没有其他强引用,整个条目被回收。

java 复制代码
Cache<String,String> cache = CacheBuilder.newBuilder()
               .weakKeys()
               .build();

使用弱引用存储值。在下个GC时,若值在缓存外部没有其他强引用,值将会被回收,键可能仍存在

java 复制代码
 Cache<String,String> cache = CacheBuilder.newBuilder()
               .weakValues()
               .build();

使用软引用储存值。当一个对象只被软引用关联时,如果内存充足,它就不会被垃圾回收器回收,就像强引用一样。但是,当内存不足,垃圾收集器即将抛出OutOfMemoryError时,它会清理掉这些仅被软引用关联的对象,从而释放内存

java 复制代码
Cache<String,String> cache = CacheBuilder.newBuilder()
               .softValues()
               .build();

写入

cache

显示put操作置入内存

java 复制代码
package guava;

import com.google.common.cache.*;

public class GuavaCacheTest {
    public static void main(String[] args) {
       Cache<String,String> cache = CacheBuilder.newBuilder()
               .initialCapacity(10)
               .build();

       cache.put("key1","value1");
       cache.put("key2","value2");
       cache.put("key3","value3");
    }
}

LoadingCache

使用自定义 ClassLoader 加载数据,置入内存中。从 LoadingCache 中获取数据时,若数据存在则直接返回;若数据不存在,则根据 ClassLoader 的 load 方法加载数据至内存,然后返回该数据

java 复制代码
package guava;

import com.google.common.cache.*;

public class GuavaCacheTest {
    public static void main(String[] args) {
        // 1. 创建缓存,设置最大权重为10
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumWeight(10) // 总权重不能超过10
                .weigher((Weigher<String, String>) (key, value) -> value.length()) // 权重=值的长度
                .build(new CacheLoader<String, String>() {
                    @Override
                    public String load(String key) {
                        return key.toUpperCase(); // 简单的加载逻辑:转大写
                    }
                });

        // 2. 使用缓存
        try {
            System.out.println(cache.get("hello"));
        }catch (Exception e){return;}
    }
}

实现原理

数据结构

LocalMaunalCache

实现了 Cache 接口,是手动加载型缓存。需要显式调用 put() 方法向缓存中添加数据,获取数据时若不存在缓存命中,需手动处理(如返回 null 或自行加载数据)

LocalLoadingCache

实现了 LoadingCache 接口(LoadingCache 继承自 Cache),是自动加载型缓存。初始化时需传入 CacheLoader,当缓存未命中时,会自动调用 CacheLoader.load() 方法加载数据并放入缓存,无需手动 put()。

LocalMaunalCache 和 LocalLoadingCache 两者都继承自 AbstractLocalCache,共享底层的分段锁(Segment)、哈希表(table)、过期 / 淘汰策略等核心机制

LocalCache

LocalCache是 Guava Cache的核心类,本质上是一个Map

java 复制代码
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {
      // 缓存的最大容量,必须是2的幂次方且不超过1<<30
      static final int MAXIMUM_CAPACITY = 1 << 30;

      // 允许的最大段数,用于限制构造函数的参数
      static final int MAX_SEGMENTS = 1 << 16; // slightly conservative

      // 在containsValue方法中未同步的重试次数
      static final int CONTAINS_VALUE_RETRIES = 3;

      // 每个段的访问次数阈值
      static final int DRAIN_THRESHOLD = 0x3F;

      // 一次清理中可以清理的最大条目数
      static final int DRAIN_MAX = 16;


      static final Logger logger = Logger.getLogger(LocalCache.class.getName());

      // 用于索引到段中的掩码值,键的哈希码的高位用于选择段
      final int segmentMask;

      // 用于在段内进行索引的偏移值,有助于防止最终位于同一段中的条目也最终位于同一个桶中。
      final int segmentShift;

      // 段组数,每个段都是一个专门的哈希表
      final Segment<K, V>[] segments;

      // 并发级别
      final int concurrencyLevel;

      // 键的比较策略
      final Equivalence<Object> keyEquivalence;

      // 值的比较策略
      final Equivalence<Object> valueEquivalence;

      // 引用键的策略
      final Strength keyStrength;

      // 引用值的策略
      final Strength valueStrength;

      // 最大重量
      final long maxWeight;

      // 键值对的重量
      final Weigher<K, V> weigher;

      // 键值对在最后一次访问后保留的时间
      final long expireAfterAccessNanos;

      // 键值对在最后一次写入后保留的时间
      final long expireAfterWriteNanos;

      // 键值对在最后一次写入后成为刷新候选的时间
      final long refreshNanos;

      // 等待被移除监听器消费的键值对队列
      final Queue<RemovalNotification<K, V>> removalNotificationQueue;

      // 键值对移除监听器
      final RemovalListener<K, V> removalListener;

      // 以可测试的方式测量时间的工具
      final Ticker ticker;

      // 创建新键值对的工厂
      final EntryFactory entryFactory;

      // 累积全局缓存统计信息的计数器
      final StatsCounter globalStatsCounter;

      // 在加载操作中使用的默认缓存加载器
      final @Nullable CacheLoader<? super K, V> defaultLoader;

  }

Segment

LocalCache里维护了一个Segment数组

java 复制代码
static class Segment<K, V> extends ReentrantLock {

    // 指向外部 LocalCache 实例的弱引用,用于访问缓存
    @Weak final LocalCache<K, V> map;

    // 记录当前段Segment中活跃元素的数量
    volatile int count;

    // 当前段中所有活跃元素的总权重,受this锁保护,确保线程安全
    @GuardedBy("this")
    long totalWeight;
    
    // 记录对段结构进行修改的次数,用于在批量读取操作中检测一致性,若在遍历过程中modCount发生变化,需要重试
    int modCount;

    // 扩容阈值,元素数量超过此值时触发扩容,计算公式为 capacity * 0.75 
    int threshold;

    // 哈希表,存储具体数据
    volatile @Nullable AtomicReferenceArray<ReferenceEntry<K, V>> table;

    // 最大权重。默认为无限制
    final long maxSegmentWeight;

    // 键的引用队列。用于清理被回收的键
    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;

    // 用于累积缓存统计信息
    final StatsCounter statsCounter;
}

源码分析

put

调用 put 方法写入key value的链路如下:

  1. 对 key 进行哈希运算,定位分段 segment

  2. 段内加锁,保证线程安全

  3. 更新当前活动元素数量,如果超过阈值则进行两倍扩容

  4. 计算获取对应位置的 table,遍历,判断 key 是否已经存在

    1. 如果 key 已存在

      1. 如果 value为空且 value的引用仍为活跃状态,则将原键值对的移除事件写入通知队列,并更新数据
      2. 如果 value不为空且仅当key不存在时更新,则返回旧值
      3. 如果vlaue不为空,则将原键值对的移除事件写入通知队列,并更新数据
    2. 如果 key 不存在,插入数据

  5. 如果设置了 maxSegmentWeight 且当前超重,则执行清理逻辑

    1. 如果新插入的键值对的重量超过 maxSegmentWeight,则删除当前键值对
    2. 从 accessQueue 中依次删除队头元素(最久未访问),直到当前段的 totalWeight <= maxSegmentWeight
java 复制代码
 @Nullable
    V put(K key, int hash, V value, boolean onlyIfAbsent) {
      // 加锁,保证线程安全
      lock();
      try {
        long now = map.ticker.read();
        preWriteCleanup(now);
        
        // 更新当前活动元素数量
        int newCount = this.count + 1;
        // 如果当前活动元素数量大于阈值,触发扩容
        if (newCount > this.threshold) { // ensure capacity
          expand();
          newCount = this.count + 1;
        }

        // 计算插入的表,并获取该表的首个元素 first
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        // 遍历table,查找是否已经存在相同的键
        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)) {
            // 如果 key 已存在,获取 value

            ValueReference<K, V> valueReference = e.getValueReference();
            V entryValue = valueReference.get();

            if (entryValue == null) {
              // 更新修改次数
              ++modCount;
              if (valueReference.isActive()) {
                // 如果值引用活跃的,将原数据移除事件封装成通知并放入通知队列
                enqueueNotification(
                    key, hash, entryValue, valueReference.getWeight(), RemovalCause.COLLECTED);
                // 更新值
                setValue(e, key, value, now);
                newCount = this.count; // count remains unchanged
              } else {
                // 如果值引用不活跃,直接更新数据
                setValue(e, key, value, now);
                newCount = this.count + 1;
              }
              this.count = newCount; 
              // 如果设置了 maxSegmentWeight 且当前超重,则执行清理逻辑:
              // 1. 如果新插入的键值对的重量超过 maxSegmentWeight,则删除当前键值对
              // 2. 从 accessQueue 中依次删除队头元素(最久未访问),直到当前段的 totalWeight <= maxSegmentWeight
              evictEntries(e);
              return null;
            } else if (onlyIfAbsent) {
              // 如果 key 不为空仅当 key 不存在时更新,则返回旧值
              recordLockedRead(e, now);
              return entryValue;
            } else {
              // 如果key不为空,且onlyIfAbsent为fasle,覆盖现有键值对,计数保持不变
              ++modCount;
              // 将原数据移除事件封装成通知并放入通知队列
              enqueueNotification(
                  key, hash, entryValue, valueReference.getWeight(), RemovalCause.REPLACED);
              // 更新值
              setValue(e, key, value, now);
              evictEntries(e);
              return entryValue;
            }
          }
        }

        // 如果key不存在,插入新的键值对
        ++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();
      }
    }

get

调用get方法的执行链路如下:

  1. 对 key 进行哈希运算,定位分段 segment
  2. 如果当前key不存在,则加载并返回 loader 方法的返回值
  3. 如果当前key存在,检查该键值对是否有效
    1. 如果值有效,更新该键值对的最后读取时间,增加命中统计,并尝试刷新该值
    2. 如果值无效,但条目的应用正在加载,则等待加载完成

LocalMaunalCache:

java 复制代码
public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException {
      checkNotNull(valueLoader);
      // 调用 LocalCache的get方法
      return localCache.get(
          key,
          new CacheLoader<Object, V>() {
            @Override
            public V load(Object key) throws Exception {
              return valueLoader.call();
            }
          });
    }

LocalCache:

java 复制代码
V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException {
    int hash = hash(checkNotNull(key));
    // 通过hash定位到segment,再调用segment的get方法
    return segmentFor(hash).get(key, hash, loader);
  }

Segment:

java 复制代码
V get(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException {
      checkNotNull(key);
      checkNotNull(loader);
      try {
        // 如果 count == 0 不会存在数据
        if (count != 0) { 
          // 定位键值对,并判断该键值对是否有效
          ReferenceEntry<K, V> e = getEntry(key, hash);
          if (e != null) {
            long now = map.ticker.read();
            // 获取还没过去的value,如果过期了,则返回null
            V value = getLiveValue(e, now);
            if (value != null) {
              // 如果缓存过期策略为expireAfterAccess,刷新键值对的访问时间
              // 将该键值对添加到 recencyQueue
              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);
            }
          }
        }

        // 之前没有写入过数据 || 数据已经过期 || 数据不是在加载中
        return lockedGetOrLoad(key, hash, loader);
      } catch (ExecutionException ee) {
        Throwable cause = ee.getCause();
        if (cause instanceof Error) {
          throw new ExecutionError((Error) cause);
        } else if (cause instanceof RuntimeException) {
          throw new UncheckedExecutionException(cause);
        }
        throw ee;
      } finally {
        postReadCleanup();
      }
    }
   
 // 如果键值对无效返回null,有效返回value    
 V getLiveValue(ReferenceEntry<K, V> entry, long now) {
      // 被GC回收
      if (entry.getKey() == null) {
        tryDrainReferenceQueues();
        return null;
      }
      V value = entry.getValueReference().get();
      // 被GC回收
      if (value == null) {
        tryDrainReferenceQueues();
        return null;
      }
      // 判断是否过期
      if (map.isExpired(entry, now)) {
        tryExpireEntries(now);
        return null;
      }
      return value;
    }

scheduleRefresh

java 复制代码
如果 entry 还没过期,则会调用此方法,尝试刷新数据
 V scheduleRefresh(
        ReferenceEntry<K, V> entry,
        K key,
        int hash,
        V oldValue,
        long now,
        CacheLoader<? super K, V> loader) {
      // 1. 配置了refreshAfterWrite
      // 2. 用writeTime判断是否达到刷新时间
      // 3. 是否在加载中,如果是则没必要再进行刷新
      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;
    }

// refresh    
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) {
      // 为key插入一个LoadingValueReference
      final LoadingValueReference<K, V> loadingValueReference =
          insertLoadingValueReference(key, hash, checkTime);
      if (loadingValueReference == null) {
        return null;
      }

      // 通过loader异步加载数据
      ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader);
      // 判断Futer是否已经完成,如果是则返回结果。否则返回null
      if (result.isDone()) {
        try {
          return Uninterruptibles.getUninterruptibly(result);
        } catch (Throwable t) {
          // don't let refresh exceptions propagate; error was already logged
        }
      }
      return null;
    }
    
 LoadingValueReference<K, V> insertLoadingValueReference(
        final K key, final int hash, boolean checkTime) {
      ReferenceEntry<K, V> e = null;
      // segment加锁
      lock();
      try {
        long now = map.ticker.read();
        // 预清理
        preWriteCleanup(now);

        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        ReferenceEntry<K, V> first = table.get(index);

        // 如果key对应的entry存在.
        for (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();
            if (valueReference.isLoading()
                || (checkTime && (now - e.getWriteTime() < map.refreshNanos))) {
              // 如果是在加载中,或者还没达到刷新时间,则返回null
              return null;
            }

            // new一个LoadingValueReference,然后把entry的valueReference替换掉
            ++modCount;
            LoadingValueReference<K, V> loadingValueReference =
                new LoadingValueReference<>(valueReference);
            e.setValueReference(loadingValueReference);
            return loadingValueReference;
          }
        }
        // 如果key对应的entry不存在,则新建一个entry
        ++modCount;
        LoadingValueReference<K, V> loadingValueReference = new LoadingValueReference<>();
        e = newEntry(key, hash, first);
        e.setValueReference(loadingValueReference);
        table.set(index, e);
        return loadingValueReference;
      } finally {
        unlock();
        postWriteCleanup();
      }
    }

lockedGetOrLoad

如果之前没有写如果数据 || 数据已过期 || 数据不是在加载中,则会调用lockedGetOrLoad

java 复制代码
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;
      // 是否需要创建一个新的entry
      boolean createNewEntry = true;
      // segment加锁
      lock();
      try {
        // re-read ticker once inside the lock
        long now = map.ticker.read();
        // 预清理
        preWriteCleanup(now);

        int newCount = this.count - 1;
        AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table;
        int index = hash & (table.length() - 1);
        // 定位到具体的table
        ReferenceEntry<K, V> first = table.get(index);
        
        for (e = first; e != null; e = e.getNext()) {
          K entryKey = e.getKey();
         
          // 定位key
          if (e.getHash() == hash
              && entryKey != null
              && map.keyEquivalence.equivalent(key, entryKey)) {
            valueReference = e.getValueReference();
            // 如果value在加载中则不需要重复创建entry
            if (valueReference.isLoading()) {
              createNewEntry = false;
            } else {
              V value = valueReference.get();
              // value为null说明已经过期且被清理掉了
              if (value == null) {
              // 写通知queue
                enqueueNotification(
                    entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED);
               // 过期但还没被清理
              } else if (map.isExpired(e, now)) {
              // 写通知queue
                // 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 {
                recordLockedRead(e, now);
                statsCounter.recordHits(1);
                // 其他情况直接返回value
                // we were concurrent with loading; don't consider refresh
                return value;
              }

              // immediately reuse invalid entries
              writeQueue.remove(e);
              accessQueue.remove(e);
              this.count = newCount; // write-volatile
            }
            break;
          }
        }

        // 创建一个 entry,切set一个新的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 {
        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);
      }
    }

总结

Guava Cache 作为一个成熟的本地缓存实现,在很多场景下都能很好地满足需求。理解其内部实现(如分段锁机制、过期策略实现等)有助于我们更好地使用它,并在出现问题时能够快速定位和解决。

相关推荐
znhy@1232 小时前
十二、SpringMVC
spring
JIngJaneIL2 小时前
图书馆自习室|基于SSM的图书馆自习室座位预约小程序设计与实现(源码+数据库+文档)
java·数据库·vue.js·spring boot·论文·毕设·图书馆自习室
毕设源码-赖学姐2 小时前
【开题答辩全过程】以 J2EE技术在在线购物分享应用中的应用为例,包含答辩的问题和答案
java·java-ee
Tadas-Gao2 小时前
微服务可观测性的“1-3-5”理想:从理论到实践的故障恢复体系
java·开发语言·微服务·云原生·架构·系统架构·可观测
Nᴏsᴛᴀʟɢɪᴀ念2 小时前
多线程奇幻漂流:从单核到多核质变(一)
java·开发语言·jvm·多线程
Swift社区2 小时前
SQL 执行异常排查 java.sql.SQLException:从 SQLException 说起
java·数据库·sql
ss2732 小时前
手写MyBatis第88弹:从XML配置到可执行SQL的完整旅程
java·开发语言·mybatis
那我掉的头发算什么3 小时前
【数据结构】二叉树的高频热门面试题大全
java·开发语言·数据结构·python·算法·链表·intellij idea
遇安.YuAn3 小时前
JAVA之求平方根
java·开发语言·算法