Guava的缓存是本地缓存,所以我觉得在使用场景上适合那种并非是高一致性的场景中,而且他的实现和ConcurrentHashMap很类似。但是毕竟是缓存嘛,肯定有自动清除的功能。外加一些什么清除策略等等。
我们看guava的cache包下面也就是才十几个类,所以可以说知识一个基础工具,如果要使用到生产环境中去的话,那么还需要我们好好的进一步封装。
最主要的一个类就是LoadingCache,主要的创建方式也是很常见的构造器方式。
@Test public void defaultCache(){ LoadingCache<String, Integer> cache=CacheBuilder.newBuilder() .maximumSize(100) .expireAfterAccess(10, TimeUnit.SECONDS) //默认情况下 监听器是和移除操作是同步的 但是可以使用RemovalListeners.asynchronous 设置为异步 .removalListener(RemovalListeners.asynchronous(new RemovalListenerImpl() { }, Executors.newCachedThreadPool())) .build( new CacheLoader<String, Integer>(){ @Override public Integer load(String key) throws Exception { return loadValue(); } } );
主要的几个参数最大值 过期时间,移除缓存的操作,加载缓存的操作(CacheLoader).
CacheLoader
缓存的加载器,我们可以通过实现load()方法,通过key来加载value,如果值一旦加载完了之后,如果值没有失效并且没有被取代的话,那么下次根据这个KEY来获取值的话,那么就直接返回缓存中的值了。其他还有很多方法,比如reload()重载方法时调用,静态方法asyncReloading(...)对自定义的CacheLoader进行装饰,返回一个异步处理加载缓存发CacheLoader
获取缓存
1.
至于获取缓存,那么直接使用cache.get(key)就ok啦,但是当我们使用这个方法的时候,返回会抛出一个受检查的异常,这是因为我们在加载value的时候,会有可能抛出异常(load方法),这个要取决我们自己,如果我们方法没有排除异常的话,像这样:
new CacheLoader<String, Integer>(){ @Override public Integer load(String key){ return loadValue(); } }
那么就可以不使用cache.get(key),而采用cache.getUnchecked(key),那么我们就不用处理异常
2.
还有第二种方式加载缓存,就是使用Callable,但是如果我们自己也实现了CacheLoader的话,那么就会把这个逻辑给覆盖掉。如果要使用这种方式的哈,那么应该这样:
Cache<String, Integer> cache=CacheBuilder.newBuilder() .maximumSize(100) .expireAfterAccess(10, TimeUnit.SECONDS) //默认情况下 监听器是和移除操作的同步的 但是可以使用RemovalListeners.asynchronous 设置为异步 .removalListener(RemovalListeners.asynchronous(new RemovalListenerImpl() { }, Executors.newCachedThreadPool())) .build();
显式插入
对于有一些场景 我们可能会一开始就将缓存加载到进去,那么之后获取的时候,就不需要那么麻烦的进行每次没有命中的时候,重新的计算。这个时候我们可以使用显式的加载缓存,
//这个map将反应到缓存的中的项中 但是不能保证其的原子性 cache.asMap().putIfAbsent("1", 1); cache.put("2", 2); //一般都优先的使用这种方式
其中asMap()方式返回一个缓存中的视图,我们也可以在上面进行操作,但是个人建议是只能拿来进行缓存的获取,不要拿来进行显式的插入,毕竟不是原子性的!
缓存回收的策略
基于容量最大值来进行回收
也就是缓存到达了一定数量之后,或者数量逼近的时候,那么就开始进行缓存的回收
.maximumSize(100)
基于权重的方式。权重越接近,那么就越接近回收
.maximumWeight(100) .weigher(new Weigher<String, Integer>() { public int weigh(String key, Integer value) { if("100".equals(key)) return 100; return 0; } })
定时回收,...Access 表示的是读写都会刷新时间 ...Write表示的是写入之后开始计时
.expireAfterAccess(3, TimeUnit.MINUTES)
.expireAfterWrite(10, TimeUnit.MINUTES)
基于引用的回收(Reference-based Eviction)
通过使用弱引用的键、或弱引用的值、或软引用的值,Guava Cache可以把缓存设置为允许垃圾回收
CacheBuilder.weakKeys():使用弱引用存储键。当键没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用键的缓存用==而不是equals比较键。
CacheBuilder.weakValues():使用弱引用存储值。当值没有其它(强或软)引用时,缓存项可以被垃圾回收。因为垃圾回收仅依赖恒等式(==),使用弱引用值的缓存用==而不是equals比较值。
CacheBuilder.softValues():使用软引用存储值。软引用只有在响应内存需要时,才按照全局最近最少使用的顺序回收。考虑到使用软引用的性能影响,我们通常建议使用更有性能预测性的缓存大小限定(见上文,基于容量回收)。使用软引用值的缓存同样用==而不是equals比较值
显式清除
任何时候,你都可以显式地清除缓存项,而不是等到它被回收:
个别清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有缓存项:Cache.invalidateAll()
刷新缓存
刷新和回收不太一样。正如LoadingCache.refresh(K)所声明,刷新表示为键加载新值,这个过程可以是异步的。在刷新操作进行时,缓存仍然可以向其他线程返回旧值,而不像回收操作,读缓存的线程必须等待新值加载完成。
如果刷新过程抛出异常,缓存将保留旧值,而异常会在记录到日志后被丢弃[swallowed]。
重载CacheLoader.reload(K, V)可以扩展刷新时的行为,这个方法允许开发者在计算新值时使用旧的值。
统计
CacheBuilder.recordStats()用来开启Guava Cache的统计功能。统计打开后,Cache.stats()方法会返回CacheStats对象以提供如下统计信息:
hitRate():缓存命中率;
averageLoadPenalty():加载新值的平均时间,单位为纳秒;
evictionCount():缓存项被回收的总数,不包括显式清除。
中断:
缓存加载方法(如Cache.get)不会抛出InterruptedException。我们也可以让这些方法支持InterruptedException,但这种支持注定是不完备的,并且会增加所有使用者的成本,而只有少数使用者实际获益。详情请继续阅读。
Cache.get请求到未缓存的值时会遇到两种情况:当前线程加载值;或等待另一个正在加载值的线程。这两种情况下的中断是不一样的。等待另一个正在加载值的线程属于较简单的情况:使用可中断的等待就实现了中断支持;但当前线程加载值的情况就比较复杂了:因为加载值的CacheLoader是由用户提供的,如果它是可中断的,那我们也可以实现支持中断,否则我们也无能为力。
如果用户提供的CacheLoader是可中断的,为什么不让Cache.get也支持中断?从某种意义上说,其实是支持的:如果CacheLoader抛出InterruptedException,Cache.get将立刻返回(就和其他异常情况一样);此外,在加载缓存值的线程中,Cache.get捕捉到InterruptedException后将恢复中断,而其他线程中InterruptedException则被包装成了ExecutionException。
原则上,我们可以拆除包装,把ExecutionException变为InterruptedException,但这会让所有的LoadingCache使用者都要处理中断异常,即使他们提供的CacheLoader不是可中断的。如果你考虑到所有非加载线程的等待仍可以被中断,这种做法也许是值得的。但许多缓存只在单线程中使用,它们的用户仍然必须捕捉不可能抛出的InterruptedException异常。即使是那些跨线程共享缓存的用户,也只是有时候能中断他们的get调用,取决于那个线程先发出请求。
对于这个决定,我们的指导原则是让缓存始终表现得好像是在当前线程加载值。这个原则让使用缓存或每次都计算值可以简单地相互切换。如果老代码(加载值的代码)是不可中断的,那么新代码(使用缓存加载值的代码)多半也应该是不可中断的。
如上所述,Guava Cache在某种意义上支持中断。另一个意义上说,Guava Cache不支持中断,这使得LoadingCache成了一个有漏洞的抽象:当加载过程被中断了,就当作其他异常一样处理,这在大多数情况下是可以的;但如果多个线程在等待加载同一个缓存项,即使加载线程被中断了,它也不应该让其他线程都失败(捕获到包装在ExecutionException里的InterruptedException),正确的行为是让剩余的某个线程重试加载。为此,我们记录了一个bug。然而,与其冒着风险修复这个bug,我们可能会花更多的精力去实现另一个建议AsyncLoadingCache,这个实现会返回一个有正确中断行为的Future对象。