文章内容源于:https://github.com/google/guava/wiki/CachesExplained 这里只是自己简单看了一下,直接翻译
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.removalListener(MY_LISTENER)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
缓存在各种用例中非常有用。尤其是当计算或者检索的代价很高,而需要多次在输入上检索这个值得时候,应该使用缓存。
Cache和ConcurrentHashMap是非常相似的,但是并不完全相同。根本的区别在于ConcurrentHashMap会永久保存添加到它的元素,直到它们被明确删除。而缓存通常被配置为自动移除元素,以限制内存的占用。在某些情况下,LoadingCache是非常有用的,由于它的自动缓存加载机制,即使没有严格移除元素的情况下。
Guava caching适用于以下情况:
可以使用CacheBuilder的构建器模式来获取缓存,但是自定义缓存也很有趣。
问自己的第一个问题是:是否需要一些合理的默认函数来加载或计算一个key的值,如果是,那么应该使用CacheLoader。如果没有,或者需要覆盖默认值,但是仍然想要“get-if-absent-compute”原子语义,应该讲Callable传递给get调用。可以使用Cache.put直接插入元素,但是自动缓存加载应该作为首选,因为可以更容易地推断出缓存内容的一致性。
LoadingCache是使用附加的Cache Loader构建的缓存。创建CacheLoader和实现V load(K key)throws expcetion一样简单。你可以使用如下代码来实现缓存:
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) throws AnyException {
return createExpensiveGraph(key);
}
});
...
try {
return graphs.get(key);
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
查询LoadingCache的规范方法是使用get(K),这将会返回被缓存的值,或者使用已缓存的CacheLoader以原子方式将新值加载到缓存,因为CacheLoader可能抛出异常,LoadingCache.get(K)可能会抛出ExecutionException(如果缓存加载器抛出了一个未经检查的异常,get(K)将抛出一个UnCheckedExecutionException来包装它)。当然你可以使用getUnChecked(K),它包装了UncheckedExecutionException中的所有异常,但是可能出现意外的情形,底层的CacheLoader可能会抛出checked Exception
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.expireAfterAccess(10, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
...
return graphs.getUnchecked(key);
可以使用方法getAll(Iterable<? Extends K>)执行批量查找。默认情况下,getAll将为缓存中不存在的每个秘钥发出对CacheLoader.load的单独调用。当批量检索比单独查找更有效时,可以覆盖CacheLoader.loadAll来利用它,getAll(Iterable)的性能将相应提高。
注意:可以编写一个CacheLoader.loadAll来加载未特别请求的键的值,例如,如果计算某个组中任何键的值会为您提供组中所有键的值,则loadAll可能会同时加载该组的其余键。
所有的Guava缓存,不管加载与否,都支持方法get(K,Callable<V>)。此方法返回与缓存中的键关联的值,或者从指定的Callable计算它并将其添加到缓存中。在加载完成之前,不会修改与此高速缓存关联的可观察状态,该方法提供了传统的if cached,return,否则 cache, and return。
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000)
.build(); // look Ma, no CacheLoader
...
try {
// If the key wasn't in the "easy to compute" group, we need to
// do things the hard way.
cache.get(key, new Callable<Value>() {
@Override
public Value call() throws AnyException {
return doThingsTheHardWay(key);
}
});
} catch (ExecutionException e) {
throw new OtherException(e.getCause());
}
可以直接使用cache.put(key,value)直接将值插入到缓存中,这将会覆盖指定键在缓存中之前的值。也可以使用Cache.asMap()视图公开的任何ConcurrentMap的方法对缓存进行修改。注意asMap上的任何方法都不会导致条目自动加载到缓存中。此外,该视图上的原子操作在自动缓存加载的范围之外运行,因Cache.get(K,Callable<V>)应始终优先于Cache.asMap()。putIfAbsent在使用CacheLoader的缓存中或者Callable
现实情况是,我们肯定没有足够的内存来缓存我们可以缓存的所有数据。必须决定什么时候缓存条码不值得保存,Guava提供了三种基本的逐出策略,size-based 逐出,time-based逐出,reference-based 逐出。
如果你的缓存不应该超出一定的大小,请使用CacheBuilder.maximum(long),缓存将尝试驱逐最近或者最不常使用的条目,需要注意的是,缓存可能会在达到内存限制之前逐出条目,通过是在接近缓存大小接近限制时。
或者,如果不同的缓存条目具有不同的权重。比如,如果你缓存的值有完全不同的内存占用,可以使用CacheBuilder.weigher(Weigher)指定权重函数,使用CacheBuilder.maximumWeight(long)指定最大权重的缓存。除了与maximumSize要求相同的警告之外,注意权重是在条目创建时计算的,在此后是静态的。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumWeight(100000)
.weigher(new Weigher<Key, Graph>() {
public int weigh(Key k, Graph g) {
return g.vertices().size();
}
})
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return createExpensiveGraph(key);
}
});
CacheBuilder提供了两种定时驱逐方法:
expireAfterAccess(long, TimeUnit)仅在读取或写入最后一次访问条目后经过指定的持续时间后条目到期。请注意,条目被驱逐的顺序与基于大小的驱逐顺序类似。
expireAfterWrite(long, TimeUnit)在创建条目后经过指定的持续时间或最近替换值后过期条目。如果缓存数据在一定时间后变得陈旧,则可能需要这样做。
如下所述,在写入期间以及在读取期间偶尔进行定期维护来执行定时到期。
测试定时驱逐
测试定时驱逐并不一定很痛苦,并且实际上不需要花费两秒来测试两秒钟的到期时间,使用Ticker接口和CacheBuilder.ticker(Ticker)方法在缓存构建器中指定时间源,而不必等待系统时钟。
Guava允许你设置缓存允许条目的垃圾回收,使用对键或值得弱引用或者对值的软引用。
CacheBuilder.weakKeys()使用弱引用存储键,如果没有其他(强或软引用),则允许对条目进行垃圾回收。由于垃圾回收仅依赖于identity相等性,因此这会导致整个缓存使用identity(==)相等来比较键,而不是equals()
CacheBuilder.weakValues()使用弱引用存储值。如果没有对值得其他(强或软引用),这允许垃圾回收对条目进行回收。由于垃圾回收仅依赖于identity相等性,因此这会导致整个缓存使用identity(==)相等来比较值,而不是equals()
CacheBuilder.softValues()包装软引用中的值,为响应内存需求,软引用的对象以全局的最近最少使用的方式进行垃圾回收。由于使用软引用对性能有影响,通常的建议是使用更可预测的最大缓存大小。使用softValues()将导致identity(==)相等而不是equals()来比较值。
你可以在任何时候显式移除缓存条目而不是等待被移除。使用:
你可以通过CacheBuilder.removalListener(RemovalLister)为缓存指定删除监听器。RemovalListener传递一个RemovalNotification,指定了RemovalCause,key和value。
注意,任何RemovalListener排除的异常都会被记录(使用Logger)并且被吞掉。
CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {
public DatabaseConnection load(Key key) throws Exception {
return openConnection(key);
}
};
RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {
public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {
DatabaseConnection conn = removal.getValue();
conn.close(); // tear down properly
}
};
return CacheBuilder.newBuilder()
.expireAfterWrite(2, TimeUnit.MINUTES)
.removalListener(removalListener)
.build(loader);
注意,移除监听器的动作通常是同步执行的,并且由于缓存的维护在正常缓存操作期间执行,expensive的移除监听器可能会影响正常的缓存功能,如果你的移除监听器很耗性能,可以使用RemovalListener.asynchronous(RemovalListener,Excutor)来装饰一个RemovalListener以异步操作。
由CacheBuilder构建的缓存不会自动执行cleanup和驱逐值,或者在值到期后立即执行或者逐出任何类型。相反,在写入期间执行少量维护,或者在写入很少的情况下偶尔执行读取操作。
// Some keys don't need refreshing, and we want refreshes to be done asynchronously.
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder()
.maximumSize(1000)
.refreshAfterWrite(1, TimeUnit.MINUTES)
.build(
new CacheLoader<Key, Graph>() {
public Graph load(Key key) { // no checked exception
return getGraphFromDatabase(key);
}
public ListenableFuture<Graph> reload(final Key key, Graph prevGraph) {
if (neverNeedsRefresh(key)) {
return Futures.immediateFuture(prevGraph);
} else {
// asynchronous!
ListenableFutureTask<Graph> task = ListenableFutureTask.create(new Callable<Graph>() {
public Graph call() {
return getGraphFromDatabase(key);
}
});
executor.execute(task);
return task;
}
}
});
原因如下:如果我们想要连续执行缓存维护,我们需要创建一个县城,这个操作将会和共享锁的用户操作竞争。此外,某些环境会限制线程的创建,这会使得CacheBuilder在该环境中无法使用。
相反,选择在你自己手中,如果缓存是高吞吐量的,那么不必担心执行缓存维护以清理过期的条目,如果缓存很少写入并且不希望清除阻止缓存读取,可能希望创建自己的维护线程,定期调用Cache.cleanUp()
如果要为很少写入的告诉缓存安排常规高速缓存维护,只需要使用ScheduledExecutorService安排维护。
刷新与驱逐并不完全相同,在LoadingCache.refresh(K)中指定的,刷新key会加载这个key的新的值,可能是异步的。在刷新的过程中,旧的值仍然会被返回,在逐出值时,会强制检索等待,直到重新加载该值。
在刷新时如果抛出了异常,那么保存旧值,记录并吞下异常。
CacheLoader可以通过重写CacheLoader.reload(K,V)来指定要在刷新时使用的智能行为,允许在计算新值的时候使用旧值。
使用CacheBuilder.refreshAfterWrite(long, TimeUnit)将会自动定时刷新缓存。与expireAfterWrite相反,refreshAfterWrite使得键在指定的持续时间后符合刷新条件,但是只有在查询条目时才会实际刷新。如果CacheLoader.reload实现为异步,则刷新不会降低查询速度。因此,可以在同一缓存上指定refreshAfterWrite和expireAfterWrite,以便条目上的到期计时器不会再每当条目符合刷新条件时都盲目重置,因此条目如果在符合刷新条件但是没有被查询,可以允许过期。
通过使用CacheBuilder.recordStats(),可以打开Guava缓存的统计信息。Cache.stats()方法返回一个CacheStats对象,该对象提供以下统计信息:
还有其他统计数据,这些统计信息在缓存调整中至关重要,建议在性能关键型应用程序密切关注这些统计信息。
你可以通过asMap视图将任何Cache视为ConcurrentMap,但是asMap视图如何与Cache交互需要一些解释。
加载方法(如get)从不会抛出InterruptedException。这些方法本来支持,但是我们的支持不完整,会导致所有用户付出代价,但是部分用户获益。
get调用请求未缓存的值被分为两大类,加载值以及那些等待另一个线程正在加载的。我们对这两者的支持不同,简单的清华是等待另一个线程正在进行的加载,这里我们可以输入一个可中断的等待。但是比较难的是自己加载这些值,如果它恰好支持中断,那么我们就可以支持中断,如果它不支持,那么我们就不支持。
为什么不提供CacheLoader时支持中断呢?从某种意义上说,如果CacheLoader抛出了InterruptedException,所有对该键的请求会立即返回(就像是其他任何异常一样)。另外,get将恢复加载线程的中断位。令人惊讶的是,InterruptedException是被包含在ExecutionException中。
原则上,可以抛出这个异常,但是这会强制所有的LoadingCache用户处理interruptedException,及时大多数CacheLoader实现从不抛出它,当考虑到所有非加载线程的等待仍然可能被中断时,这是值得的。但是许多缓存仅仅在单个线程中使用。他们的用户仍然必须补货不可能的InterruptedException,甚至那些跨线程共享缓存的用户也可以根据哪个线程首先发出请求来中断它们的get调用。
决定的指导原则是缓存的行为就像是所有值都在调用线程中加载一样。这个和原则可以很容易地将缓存引入到以前在每次调用中重新计算其值得代码中,如果旧的代码不可中断,那么新的代码也可能不行。
这里只在某种意义上支持中断,但是其他情况下不支持,可能会导致漏洞。如果加载线程被中断,会想其他异常一样处理,在大多数情况下是ok的,但是多个get调用在等待值时,会出现问题。所有的调用者都会受到InterruptedException,即时加载没有像abort那样失败,正确的行为是剩余线程重试加载。但是修复存在风险。反之,这里建议在AsyncLoadingCache中添加额外的工作,会返回具有正确中断行为的Future对象。