Guava缓存器源码分析——缓存统计器

时间:2022-09-25 17:13:34
Guava缓存器统计器实现:
全局统计器——

        1、CacheBuilder的静态成员变量Supplier<StatsCounter> CACHE_STATS_COUNTER初始化时,重载的get方法,返回了一个SimpleStatsCounter实例。
        2、当缓存器开启缓存统计时(recordStats),其成员变量statsCounterSupplier被赋值为CACHE_STATS_COUNTER,若没开启则为初始值NULL_STATS_COUNTER。
 3、在LocalCache的构造函数中,缓存器的全局统计器globalStatsCounter将从CacheBuilder中获取:builder.getStatsCounterSupplier().get();
因此Guava缓存器的全局统计器实际上是SimpleStatsCounter类型。
       全局统计器只有在调用getAll, getAllPresent, getIfPresent, loadAll方法时才会更新。

段统计器——

缓存器的每个段都有自己的统计器statsCounter,在LocalCache的构造函数中,通过createSegment方法创建所有的段,同时通过 builder.getStatsCounterSupplier().get()完成对段统计器的初始化,因此段统计器也是SimpleStatsCounter类型。

当用户想查看缓存统计信息时,会调用stats方法,将全局统计器及每一个段统计器的信息综合起来:
       public CacheStats stats() {
              SimpleStatsCounter aggregator = new SimpleStatsCounter();
              aggregator.incrementBy(localCache.globalStatsCounter);
              for (Segment<K, V> segment : localCache.segments) {
                    aggregator.incrementBy(segment.statsCounter);
              }
              return aggregator.snapshot();
        }
        最终通过snapshot方法,返回包含所有统计信息的CacheStats对象:
        public CacheStats snapshot() {
              return new CacheStats(
              hitCount.sum(),
              missCount.sum(),
              loadSuccessCount.sum(),
              loadExceptionCount.sum(),
              totalLoadTime.sum(),
              evictionCount.sum());
        }

Guava缓存器源码分析——缓存统计器
SimpleStatsCounter中所有的LongAddable类型成员变量,都是通过
LongAddables的create方法初始化,该静态方法实际调用的为 SUPPLIER.get()方法,返回一个
LongAddable实例。

        在LongAddables 中有一段静态代码段,完成了对其成员变量Supplier<LongAddable> SUPPLIER的初始化,并重载了Supplier的get方法,该get方法返回一个LongAdder对象,如果初始化出现异常,则重新初始化SUPPLIER,但是重载的get方法中,返回一个PureJavaLongAddable对象。

1、PureJavaLongAddable继承至AtomicLong,AtomicLong使用原子方法实现了对一个Long对象的增、减、更新等操作。 比如对于++运算符 AtomicLong 可以将它持有的 Long对象原子地递增。 PureJavaLongAddable中的方法都通过AtomicLong来实现,以保证所有操作都能原子地完成,比如add方法实际调用的即为AtomicLong.getAndAdd,该方法将当前值加上一个数,并返回原值:
       public final long getAndAdd(long delta) {
            while (true) {
                    long current = get();
                    long next = current + delta;
                    if (compareAndSet(current, next))
                            return current;
            }
        }

2、按Guava的说法,当多条线程在更新统计数据时,而不是细粒度同步控制的情况下,LongAdder比AtomicLong更好用。当更新争用的频率低时,两个类效果比较相似,当争用频率很高时,LongAdder的吞吐率将会大大提升,但会消耗更大的空间。
        下面分析下LongAdder的add方法——
        其中cells和base都为Striped64的成员变量,cells为数组类型,base作为一个保底值,当不发生争用时更新它。
        public void add(long x) {
                Cell[] as; long b, v; HashCode hc; Cell a; int n;
                //当cells不为空,或对base值更新失败时,进入分支;

                if ((as = 
cells) != null || !casBase(b = 
base, b + x)) {
                        boolean uncontended = true;
                        //获取hash值;
                        int h = (hc = threadHashCode.get()).code; 
                        //当cells为空,或其长度小于1,或从cells中随机取的cell为空,或对随机所取得cell更新失败时(发生争用),则进入分支,重新更新,若发生争用,此时uncontended为false,在重新更新时会使用到busy锁;
                        if (as == null || (n = as.length) < 1 ||(a = as[(n - 1) & h]) == null || !(uncontended = a.cas(v = a.value, v + x)))
                                retryUpdate(x, hc, uncontended);
                }
        }

因此,LongAdder主要是通过Cell[] cells,将同步操作,转嫁到随机取得的cells元素上,从而使得争用的概率大大降低,同时在发生争用时,retryUpdate方法中还可能会对cells数据进行扩容,以降低争用的发生:
        Cell[] rs = new Cell[n << 1];
        for (int i = 0; i < n; ++i)
                rs[i] = as[i];
        
cells = rs;
虽然这确实会带来一定的空间消耗,但缓存器本身对性能要求很高,以空间换时间是可以接受的。