谷歌Java开发工具包之Cache缓存源码分析(一)

前言

本篇文章是对Google的Guava中Cache进行一个源码级的分析,因为篇幅有限,而单单就LocalCache的量就达到了5000+行,还是有很多细节无法展现出来,富文本格式代码展示不太容易,就用了图片来替换.尽量让格式看起来好点.

文章包括两部分:

  • 一: 前言扩展,官方文档及基础架构设计

  • 二. 通过源码阅读来分析其数据结构,cache命中率等状态,数据引用类型,使用. 犹豫篇幅较长建议收藏阅读 ! 如果看完觉得还有不明白的地方,可以在下方留言.本篇文章是从全局的进行阅读分析,第二篇会从锁及并发方面分析其性能,感兴趣的同学,可以关注下

官方介绍

关键词及API科普

  • LRU (Least Recently Used): 最近最少使用,常用与回收策略

  • void invalidate(Object key); 删除缓存

  • void invalidateAll(); 清楚所有的缓存,相当远map的clear操作。

  • long size(); 获取缓存中元素的大概个数。为什么是大概呢?元素失效之时,并不会实时的更新size,所以这里的size可能会包含失效元素。

  • CacheStats stats(); 缓存的状态数据,包括(未)命中个数,加载成功/失败个数,总共加载时间,删除个数等。

    扫描二维码关注公众号,回复: 2212194 查看本文章

GITHUB-WIKI文档:

https://github.com/google/guava/wiki/CachesExplained

官方使用介绍:

Generally, the Guava caching utilities are applicable whenever:

  • You are willing to spend some memory to improve speed.

  • You expect that keys will sometimes get queried more than once.

  • Your cache will not need to store more data than what would fit in RAM. (Guava caches are local to a single run of your application. They do not store data in files, or on outside servers. If this does not fit your needs, consider a tool like Memcached.)

翻译:

  • 愿意消耗一些内存空间来提升速度;

  • 能够预计某些key会被查询一次以上;

  • 缓存中存放的数据总量不会超出内存容量(`Guava Cache`是单个应用运行时的本地缓存)。

实现原理

Cache本质上就是ConcurrentMap,区别是:Cache除了拥有Map集合属性,为了限制内存使用还拥有可设置的自动移除的策略,而ConcurrentMap只是数据结构,会一直保存数据,直到显式移除.

Guava Cache提供了三种基本的缓存回收方式:

基于容量回收、定时回收和基于引用回收。

定时回收有两种:按照写入时间,最早写入的最先回收;按照访问时间,最早访问的最早回收。

Guava设计架构分析

CacheBuilder

提供了build方法的两种返回类型,其分别创建两种类型的缓存器,这两种类型的缓存器都属于LocalCache的内部类,

CacheBuilder的内部属性(下文有详细讲解),会为LocalCache缓存的生命周期等操作赋值(注意看构造中参数):

1. 自动从数据源加载到缓存中 其是实现LoadingCache接口是对Cache接口的一个扩展,eg:实现了自动从CacheLoading中加载数据

2. 当调用get方法从Callbale参数中同步得到,其是实现了Cache接口

下图是分别实现了上面那些接口的缓存类

通过上面我们知道Guava的Cache有两种方式去读缓存

1. get(key) 获取,当获取不到的时候,就从构造中提前指定的loader中获取

2. get(key,Callable loader) 当从缓存中获取不到的时候,就从Callable中获取

那么我们现在开始分析,Guava是如何通过接口的形式来定义这个呢?

首先我们看Cache接口和LoadingCache

我们可以想到当如果是继承了Cache接口就要实现从Callable中获取,如果继承了LoadingCache就可以提前从创建时候指定的CacheLoader中获取.

当我们对接口有一个正确的认识的时候,我们开始关注缓存的基础实现类

LocalCache

LocalCache是缓存的最终实现类,大部分逻辑代码都在改类中编写通过该类分析,该类中包含了很多的静态内部类和接口定义建议阅读源码学习

通过其是继承ConcurrentMap可知,Guava的缓存其实就是ConcurrentMap其利用分段锁的特性,来提高性能.

其提供了两个内部类

  1. LocalLoadingCache内部类

2. LocalManualCache内部类


问题解答

当你看到这里,相信你对Guava的设计架构已经有一个大概的认识,现在我们开始了解一下细节的问题

eg:

1. CacheBuilder 中都默认有那些属性?

2. Cache的CacheStats状态及Hit热度及命中率是如何统计的?

3. Cache的数据格式和ConcurrentMap是一样的,那么他的数据结构到底是什么样的?来提高效率的?

4. Cache是如果做到强引用和弱引用及软应用的?

5. 最后我们在看我们应该如何正确的使用这个呢?

CacheBuilder 中都默认有那些属性?

CacheBuilder 中有一个非常值得我们学习的小细节,这个参数检查是不是非常优美!

(首先设置默认值,当,this.值不等于默认值,说明已经赋值过,在赋值就报错),之所以说是细节,是因为大部分人不会注意到,学到就要用到奥!

Cache的CacheStats状态及Hit热度及命中率是如何统计的?

我们看LocalCache内部接口StatsCounter是怎么定义的 !

当我们对缓存状态的接口掌握后,我们开始猜测他调用的时机

统计加载时间

统计清理次数

Cache的数据格式和ConcurrentMap是一样的,那么他的数据结构到底是什么样的?来提高效率的?

我们知道ConcurrentMap的数据结构是分段,通过分段锁的方式来提高效率的

现在我们看LocalCache的数据结构,

我们看LocalCache的内部类Segment,即段

每个Segment段里面,有一个AtomicReferenceArray数组,数组中每个元素即:ReferenceEntry,是一个队列

抽查一个他的实现类

UML图

Cache是如何提高效率的呢?ConcurrentHashMap是如何提高效率的呢?

单单从数据结构上来分析(LocalCache和ConcurrentHashMap原理一样),我们知道一个ConcurrentHashMap由多个segment组成,每个segment包含一个Entity的数组。这里比HashMap多了一个segment类。该类继承了ReentrantLock类,所以本身是一个锁。当多线程对ConcurrentHashMap操作时,不是完全锁住map,而是锁住相应的segment。这样提高了并发效率. Cache和其类似,所以就不多说了.另外Cache提供了多种引用类型,及自动回收的方式.


Cache是如果做到强引用和弱引用及软应用的?

我的步骤是这样的,因为当我们在添加缓存的时候,都默认是Strong强引用,guava既然支持引入回收,那么他肯定是在报错的时候,对我们的数据,进行了wrap

包装(pool数据池化分析里面的思路),那么我们就从put作为入口,开始分析数据结构.

首先我们先猜测一下put会做哪些操作:

1. 根据key 哈希找到区段 入参中获取hash

2. 检查容量,执行清理 expand() evictEntries(newEntry)

3. 如果不存在,就包装为指定的引用值 [重点分析] setValue

在setValue中我们找到了包装的代码:即将缓存的key和value包装为用户设置cache的引用方式

在构造时候LocalCache根据CacheBuilder的属性设置

我们再来看Guava缓存中的引用是如何定义的! 这里不做详细介绍,如果不懂,可以看<<大部分Java程序猿所不了解的引用关系>>

  • 强引用: Strong new出来的都是强引用

  • 软引用: Soft 当jvm内存充足,gc不会回收,只有在内存不够时候回收

  • 弱引用: weak 不论内存是否充足,gc都会在触发时候去回收

接下来我们来讲最后一个细节: 我们应该如何正确的使用!

相信当大家看到这里的时候,对CacheBuilder有正确的认识之后,基本就不用介绍了吧! 直接撸代码

分为两部分

1. 创建Cache

2. 创建移除事件

猜你喜欢

转载自blog.csdn.net/message_lx/article/details/79296241