原文地址 http://blog.csdn.net/guozebo/article/details/51590517
前言
在多线程高并发场景中每每是离不开cache的,须要根据不一样的应用场景来须要选择不一样的cache,好比分布式缓存如redis、memcached,还有本地(进程内)缓存如ehcache、GuavaCache。以前用spring cache的时候集成的是ehcache,但接触到GuavaCache以后,被它的简单、强大、及轻量级所吸引。它不须要配置文件,使用起来和ConcurrentHashMap同样简单,并且能覆盖绝大多数使用cache的场景需求!html
GuavaCache是google开源java类库Guava的其中一个模块,在maven工程下使用可在pom文件加入以下依赖:java
- <dependency>
- <groupId>com.google.guava</groupId>
- <artifactId>guava</artifactId>
- <version>19.0</version>
- </dependency>
Cache接口及其实现
先说说通常的cache都会实现的基础功能包括:git
提供一个存储缓存的容器,该容器实现了存放(Put)和读取(Get)缓存的接口供外部调用。 缓存一般以<key,value>的形式存在,经过key来从缓存中获取value。固然容器的大小每每是有限的(受限于内存大小),须要为它设置清除缓存的策略。github
在GuavaCache中缓存的容器被定义为接口Cache<K, V>的实现类,这些实现类都是线程安全的,所以一般定义为一个单例。而且接口Cache是泛型,很好的支持了不一样类型的key和value。做为示例,咱们构建一个key为Integer、value为String的Cache实例:redis
- final static Cache<Integer, String> cache = CacheBuilder.newBuilder()
-
- .initialCapacity(10)
-
- .concurrencyLevel(5)
-
- .expireAfterWrite(10, TimeUnit.SECONDS)
-
- .build();
听说GuavaCache的实现是基于ConcurrentHashMap的,所以上面的构造过程所调用的方法,经过查看其官方文档也能看到一些相似的原理。好比经过initialCapacity(5)定义初始值大小,要是定义太大就好浪费内存空间,要是过小,须要扩容的时候就会像map同样须要resize,这个过程会产生大量须要gc的对象,还有好比经过concurrencyLevel(5)来限制写入操做的并发数,这和ConcurrentHashMap的锁机制也是相似的(ConcurrentHashMap读不须要加锁,写入须要加锁,每一个segment都有一个锁)。spring
接下来看看Cache提供哪些方法(只列了部分经常使用的):缓存
- public interface Cache<K, V> {
-
-
- V getIfPresent(Object key);
-
-
- V get(K key, Callable<? extends V> valueLoader) throws ExecutionException;
-
-
- void put(K key, V value);
-
-
- void invalidate(Object key);
-
-
- void invalidateAll();
-
-
- void cleanUp();
- }
使用过程仍是要认真查看官方的文档,如下Demo简单的展现了Cache的写入,读取,和过时清除策略是否生效:安全
- public static void main(String[] args) throws Exception {
- cache.put(1, "Hi");
-
- for(int i=0 ;i<100 ;i++) {
- SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
- System.out.println(sdf.format(new Date())
- + " key:1 ,value:"+cache.getIfPresent(1));
- Thread.sleep(1000);
- }
- }
清除缓存的策略
任何Cache的容量都是有限的,而缓存清除策略就是决定数据在何时应该被清理掉。GuavaCache提了如下几种清除策略:
基于存活时间的清除(Timed Eviction)
这应该是最经常使用的清除策略,在构建Cache实例的时候,CacheBuilder提供两种基于存活时间的构建方法:
(1)expireAfterAccess(long, TimeUnit):缓存项在建立后,在给定时间内没有被读/写访问,则清除。
(2)expireAfterWrite(long, TimeUnit):缓存项在建立后,在给定时间内没有被写访问(建立或覆盖),则清除。
expireAfterWrite()方法有些相似于redis中的expire命令,但显然它只能设置全部缓存都具备相同的存活时间。若遇到一些缓存数据的存活时间为1分钟,一些为5分钟,那只能构建两个Cache实例了。
基于容量的清除(size-based eviction)
在构建Cache实例的时候,经过CacheBuilder.maximumSize(long)方法能够设置Cache的最大容量数,当缓存数量达到或接近该最大值时,Cache将清除掉那些最近最少使用的缓存。
以上是这种方式是以缓存的“数量”做为容量的计算方式,还有另一种基于“权重”的计算方式。好比每一项缓存所占据的内存空间大小都不同,能够看做它们有不一样的“权重”(weights)。你可使用CacheBuilder.weigher(Weigher)指定一个权重函数,而且用CacheBuilder.maximumWeight(long)指定最大总重。
显式清除
任什么时候候,你均可以显式地清除缓存项,而不是等到它被回收,Cache接口提供了以下API:
(1)个别清除:Cache.invalidate(key)
(2)批量清除:Cache.invalidateAll(keys)
(3)清除全部缓存项:Cache.invalidateAll()
基于引用的清除(Reference-based Eviction)
在构建Cache实例过程当中,经过设置使用弱引用的键、或弱引用的值、或软引用的值,从而使JVM在GC时顺带实现缓存的清除,不过通常不轻易使用这个特性。
(1)CacheBuilder.weakKeys():使用弱引用存储键
(2)CacheBuilder.weakValues():使用弱引用存储值
(3)CacheBuilder.softValues():使用软引用存储值
清除何时发生?
也许这个问题有点奇怪,若是设置的存活时间为一分钟,难道不是一分钟后这个key就会当即清除掉吗?咱们来分析一下若是要实现这个功能,那Cache中就必须存在线程来进行周期性地检查、清除等工做,不少cache如redis、ehcache都是这样实现的。
但在GuavaCache中,并不存在任何线程!它实现机制是在写操做时顺带作少许的维护工做(如清除),偶尔在读操做时作(若是写操做实在太少的话),也就是说在使用的是调用线程,参考以下示例:
- public class CacheService {
- static Cache<Integer, String> cache = CacheBuilder.newBuilder()
- .expireAfterWrite(5, TimeUnit.SECONDS)
- .build();
-
- public static void main(String[] args) throws Exception {
- new Thread() {
- public void run() {
- while(true) {
- SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
- System.out.println(sdf.format(new Date()) +" size: "+cache.size());
- try {
- Thread.sleep(2000);
- } catch (InterruptedException e) {
- }
- }
- };
- }.start();
- SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
- cache.put(1, "Hi");
- System.out.println("write key:1 ,value:"+cache.getIfPresent(1));
- Thread.sleep(10000);
-
- cache.put(2, "bbb");
- System.out.println("write key:2 ,value:"+cache.getIfPresent(2));
- Thread.sleep(10000);
-
- System.out.println(sdf.format(new Date())
- +" after write, key:1 ,value:"+cache.getIfPresent(1));
- Thread.sleep(2000);
-
- System.out.println(sdf.format(new Date())
- +" final, key:2 ,value:"+cache.getIfPresent(2));
- }
- }
控制台输出:
- 00:34:17 size: 0
- write key:1 ,value:Hi
- 00:34:19 size: 1
- 00:34:21 size: 1
- 00:34:23 size: 1
- 00:34:25 size: 1
- write key:2 ,value:bbb
- 00:34:27 size: 1
- 00:34:29 size: 1
- 00:34:31 size: 1
- 00:34:33 size: 1
- 00:34:35 size: 1
- 00:34:37 after write, key:1 ,value:null
- 00:34:37 size: 1
- 00:34:39 final, key:2 ,value:null
- 00:34:39 size: 0
经过分析发现:
(1)缓存项<1,"Hi">的存活时间是5秒,但通过5秒后并无被清除,由于仍是size=1
(2)发生写操做cache.put(2, "bbb")后,缓存项<1,"Hi">被清除,由于size=1,而不是size=2
(3)发生读操做cache.getIfPresent(1)后,缓存项<2,"bbb">没有被清除,由于仍是size=1,看来读操做确实不必定会发生清除
(4)发生读操做cache.getIfPresent(2)后,缓存项<2,"bbb">被清除,由于读的key就是2
这在GuavaCache被称为“延迟删除”,即删除老是发生得比较“晚”,这也是GuavaCache不一样于其余Cache的地方!这种实现方式的问题:缓存会可能会存活比较长的时间,一直占用着内存。若是使用了复杂的清除策略如
基于容量的清除,还可能会占用着线程而致使响应时间变长。但优势也是显而易见的,没有启动线程,不论是实现,仍是使用起来都让人以为简单(轻量)。
若是你仍是但愿尽量的下降延迟,能够建立本身的维护线程,以固定的时间间隔调用Cache.cleanUp(),ScheduledExecutorService能够帮助你很好地实现这样的定时调度。不过这种方式依然没办法百分百的肯定必定是本身的维护线程“命中”了维护的工做。
总结
请必定要记住GuavaCache的实现代码中没有启动任何线程!!Cache中的全部维护操做,包括清除缓存、写入缓存等,都是经过调用线程来操做的。这在须要低延迟服务场景中使用时尤为须要关注,可能会在某个调用的响应时间忽然变大。
GuavaCache毕竟是一款面向本地缓存的,轻量级的Cache,适合缓存少许数据。若是你想缓存上千万数据,能够为每一个key设置不一样的存活时间,而且高性能,那并不适合使用GuavaCache。
参考
官方github多线程
官方文档中文翻译并发