Guava LocalCache源码分析:LocalCache生成

avatar
作者
猴君
阅读量:1

Guava LocalCache源码分析:Cache生成

LocalCache为guava本地缓存的解决方案,提供了基于容量,时间和引用的缓存回收方式,其数据读写都在一个进程内,相对与 redis 等分布式缓存,不需要网络传输的过程,访问速度很快,同时也受到 JVM 内存的制约,无法在数据量较多的场景下使用。

基于以上特点,guava cache 的主要应用场景为以下几种:

  • 对于访问速度有较大要求
  • 存储的数据不经常变化
  • 数据量不大,占用内存较小
  • 需要访问整个集合
  • 能够容忍数据不是实时的

版本

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

LocalCache参数说明

类型参数名默认值说明
static final intMAXIMUM_CAPACITY1 << 30最大容量,如果其中一个具有参数的构造函数隐式指定了更高的值,则使用该容量。必须是2的幂{2^30},以确保Entry可以使用整数进行索引
static final intMAX_SEGMENTS1 << 16允许的最大分段数,并发数,这里设置的2^16;用于绑定构造函数参数
static final intCONTAINS_VALUE_RETRIES3containsValue方法中的(未同步)重试次数
static final intDRAIN_THRESHOLD0x3F在更新缓存的最近顺序信息之前,每个段可以缓冲的缓存访问操作数。这用于通过记录读取的memento并延迟锁获取,直到超过阈值或发生突变,来避免锁争用
static final intDRAIN_MAX16单次清理运行中要排出的最大Entry数。这独立适用于清理队列和两个引用队列
static finalloggerLogger.getLogger(LocalCache.class.getName())java.util.logging.Logger日志
final intsegmentMask——用于索引分段的掩码值。密钥哈希码的高位用于选择段
final intsegmentShift——分段内索引的移位值。有助于防止最终位于同一段中的Entry也位于同一桶中
final Segment<K, V>[]segments——每个段都是一个继承了ReentrantLock的哈希表
final intconcurrencyLevel——并发级别
final Equivalence<Object>keyEquivalence——键比较策略
final Equivalence<Object>valueEquivalence——值比较策略
final StrengthkeyStrength——键策略
final StrengthvalueStrength——值策略
final longmaxWeight——此Map的最大容量。如果没有最大值,则返回UNSET_INT
final Weigher<K, V>weigher——缓存容量计算器
final longexpireAfterAccessNanos——在Entry最后一次访问的多久内Map保留该Entry
final longexpireAfterWriteNanos——在Entry最后一次写入的多久内Map保留该Entry
final longrefreshNanos——当Entry上次写入多久后,Entry成为刷新的候选项。设置自动刷新间隔的
final Queue<RemovalNotification<K, V>>removalNotificationQueue——存放removalListener占用的Entry的等待队列
final RemovalListener<K, V>removalListener——当Entry因软/弱Entry的过期或垃圾回收而被删除时调用的监听器
final Tickerticker——com.google.common.base.Ticker计数器,用于计算时间
final EntryFactoryentryFactory——用于创建新Entry
final StatsCounterglobalStatsCounter——全局缓存数。请注意,还有每个分段的实体数量,必须聚合这些分段中的Entry数量才能获得全局总数
final CacheLoader<? super K, V>defaultLoader——加载操作时使用的默认缓存加载器

Cache构建过程

guava cache 最常用的方式是通过 CacheBuilder 进行构建, CacheBuilder 通过 build 方法返回一个new LocalCache.LocalLoadingCache<K1, V1>(this, loader);其中 LocalLoadingCache 继承了 LocalManualCache,而 LocalManualCache 定义了一个 final LocalCache<K, V>。

在这里插入图片描述
最终返回的LocalManualCache的所有操作均为对LocalCache的操作。

LocalCache介绍

LocalCache继承了AbstractMap并实现了ConcurrentMap。
LocalCache基本策略是对Entry分段存储,每个Segment本身都是一个并发可读的哈希表。该映射支持跨不同段的非阻塞读取和并发写入。如果指定了最大大小,则使用页面替换算法对段内的Entry进行替换。

LocalCache实例化

将builder中的属性赋值到LocalCache中

LocalCache(CacheBuilder<? super K, ? super V> builder, @CheckForNull CacheLoader<? super K, V> loader) {         //从builder和默认(2^16)中选的最小的作为并发级别         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();         //监听Entry被GC回收的监听器         removalListener = builder.getRemovalListener();         //removalListener的Entry队列,这里如果removalListener未设置,         //则用的是discardingQueue,如果已设置则使用的是ConcurrentLinkedQueue         removalNotificationQueue = (removalListener == NullListener.INSTANCE) ? LocalCache.discardingQueue()  : new ConcurrentLinkedQueue<>();         //计数器         ticker = builder.getTicker(recordsTime());         //实体工厂         entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries());         //全局缓存数         globalStatsCounter = builder.getStatsCounterSupplier().get();         //默认加载器         defaultLoader = loader;         //设置初始容量,在builder的初始容量和2^30选更小的一个         int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY);         //如果Map的最大容量>0且weigher是OneWeigher的实例         if (evictsBySize() && !customWeigher()) {             //初始容量是当前值与maxWeight之间更小的一个             initialCapacity = (int) Math.min(initialCapacity, maxWeight);         }         …… } 

其中evictsBySize方法和customWeigher方法如下:

    boolean evictsBySize() {         return maxWeight >= 0;     }     boolean customWeigher() {         return weigher != OneWeigher.INSTANCE;     } 

分段

  1. 计算段个数,如果未指定最大容量时,段个数=超过并发级别的最小2次幂,指定了最大容量时,段个数=超过并发级别和最大容量/20之间较小的值的最小2次幂。
        // 除非指定了maximumSize/Weight(在这种情况下,请确保每个段至少有10个Entry),         // 否则找到超过并发级别的最小2次幂的段数量,然后对该段中的Entry进行移除,         // 因为移除是按段而不是全局进行的,因此与最大大小相比,过多的段将导致随机移除行为。         int segmentShift = 0;         int segmentCount = 1;         //未指定最大容量时,segmentCount=超过concurrencyLevel的最小2次幂         //指定了最大容量时,segmentCount=超过Math.Min(concurrencyLevel,maxWeight/20)的最小2次幂         while (segmentCount < concurrencyLevel                 && (!evictsBySize() || segmentCount * 20L <= maxWeight)) {             ++segmentShift;             segmentCount <<= 1;         }          this.segmentShift = 32 - segmentShift;         segmentMask = segmentCount - 1; 
  1. 通过newSegmentArray()创建段
        this.segments = newSegmentArray(segmentCount); 
  1. 计算段容量,cache的段容量由两个变量控制,segmentSize为初始容量,maxSegmentWeight为能够扩容的最大容量。
        //段容量=Math.Ceiling(总容量/段数)         int segmentCapacity = initialCapacity / segmentCount;         if (segmentCapacity * segmentCount < initialCapacity) {             ++segmentCapacity;         }          //segmentSize=超过segmentCapacity的最小2次幂         int segmentSize = 1;         while (segmentSize < segmentCapacity) {             segmentSize <<= 1;         }          //如果设置了Map的最大容量         if (evictsBySize()) {             //确保分段最大容量之和=总最大容量             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 {             //没有设置最大容量,则每段大小为UNSET_INT             for (int i = 0; i < this.segments.length; ++i) {                 this.segments[i] =                         createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get());             }         } 

初始容量计算比较简单,为超过Math.Ceiling(总容量/段数)的最小2次幂。如果设置了Map的最大容量,最大段容量计算如下图所示:
最大段容量计算
假设分段最大容量为25,段数量为7,则通过公式计算得出maxSegmentWeight=4,remainder=4。根据for循环,当下标为4时,maxSegmentWeight=maxSegmentWeight-1,则前4个段最大容量为4,而后三个段的最大容量为3。如此,确保了分段最大容量之和=总最大容量。
当没有设置最大容量,则每段大小为UNSET_INT。

相关链接

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!