当前位置: 首页 > 科技观察

Java类库中的瑞士军刀:GoogleGuavaCache

时间:2023-03-16 16:26:34 科技观察

GoogleGuava被称为Java类库中的瑞士军刀。它可以显着简化代码,使代码易写、易读、易维护。同时可以大大提高程序员的工作效率,让我们摆脱掉很多重复的底层代码。由于GoogleGuava类库包含大量非常有用的特性,因此无法在一篇文章中穷尽。本文只简单介绍一下GoogleGuava中缓存工具的使用。依赖使用Maven构建项目时,添加如下依赖:com.google.guavaguava29.0-jre29.0-android使用Gradle构建项目时,添加如下依赖:dependencies{//Pickone://1.UseGuavainyourimplementationonly:implementation("com.google.guava:guava:29.0-jre")//2.UseGuavatypesinyourpublicAPI:api("com.google.guava:guava:29.0-jre")//3.Android-UseGuavainyourimplementationonly:implementation("com.google.guava:guava:29.0-android")//4.Android-UseGuavatypesinyourpublicAPI:api("com.google.guava:guava:29.0-android")}示例LoadingCachegraphs=CacheBuilder.newBuilder().maximumSize(1000).expireAfterWrite(10,TimeUnit.MINUTES).removalListener(MY_LISTENER).build(newCacheLoader(){@OverridepublicGraphload(Keykey)throwsAnyException{returncreateExpensiveGraph(key);}});适用性缓存的应用场景非常广泛。例如,您应该对计算或查询成本高的数据使用缓存,或者多次需要某个输入数据。`Cache`类似于`ConcurrentMap`,但不完全相同。基本区别在于ConcurrentMap会保留所有添加的元素,直到它们被显式删除。另一方面,“缓存”通常配置为自动逐出条目以限制其内存占用。在某些情况下,`LoadingCache`会很有用,虽然它不会逐出条目,但可以自动加载缓存。一般来说,Guava的缓存设施可以用在以下场景:你想使用一些内存空间来提高速度。您想要多次查询某些键。您的缓存不需要存储超过RAM可以处理的数据。(Guava缓存的范围仅限于应用程序的单次运行。它们不会将数据存储在文件中或外部服务器上。如果这不符合您的需求,请考虑使用Memcached)如果其中任何一个适用于您的应用程序场景,那么Guava缓存实用程序就是为您准备的!如上面的示例代码所示,可以使用CacheBuilder生成器模式获取缓存,但自定义缓存是有趣的部分。注意:如果不需要Cache的功能,ConcurrentHashMap的内存效率更高——但是很难用任何旧的ConcurrentMap复制Cache的大部分功能。关于缓存,您需要问自己的第一个问题是:是否有一些合理的默认函数来加载或计算与键关联的值?如果是这样,您应该使用`CacheLoader`。如果不是这种情况,或者如果您需要覆盖默认值,但仍需要原子“get-if-absent-compute”语义,则应将`Callable`传递给`get`调用。可以使用“Cache.put”直接插入元素,但首选自动加载缓存,因为它可以更轻松地推断所有缓存内容的一致性。使用CacheLoader`LoadingCache`是从附加的`CacheLoader`构建的`Cache`。创建一个`CacheLoader`通常与实现`Vload(Kkey)throwsException`方法相同。因此,例如,您可以使用以下代码创建一个“LoadingCache”:LoadingCachegraphs=CacheBuilder.newBuilder().maximumSize(1000).build(newCacheLoader(){publicGraphload(Keykey)throwsAnyException{returncreateExpensiveGraph(key);}});...try{returngraphs.get(key);}catch(ExecutionExceptione){thrownewOtherException(e.getCause());}查询LoadingCache的规范方法是使用`get(K)`方法。这将返回一个已经缓存的值,或者使用缓存的CacheLoader自动将一个新值加载到缓存中。由于`CacheLoader`可能抛出`Exception`,`LoadingCache.get(K)`将抛出`ExecutionException`。(如果缓存加载程序抛出未经检查的异常,`get(K)`将引发一个包含`UncheckedExecutionException`的异常。)您还可以选择使用`getUnchecked(K)`将所有异常包装在`UncheckedExecutionException`中,但如果底层的CacheLoader通常抛出已检查的异常,这可能会导致令人惊讶的行为。LoadingCachegraphs=CacheBuilder.newBuilder().expireAfterAccess(10,TimeUnit.MINUTES).build(newCacheLoader(){publicGraphload(Keykey){//nocheckedexceptionreturncreateExpensiveGraph(key);}});...returngraphs.getUnchecked(键);可以使用getAll(Iterable)方法执行批量查找。默认情况下,`getAll`将为缓存中不存在的每个键发出单独的`CacheLoader.load`调用。如果批量检索比许多单独的查询更有效,您可以覆盖`CacheLoader.loadAll`以利用这一点。`getAll(Iterable)`的性能将相应提高。请注意,您可以编写一个`CacheLoader.loadAll`实现,为未明确要求的键加载值。例如,如果计算一个组中任何键的值,就会得到该组中所有键的值,那么loadAll可能会同时加载其余组。使用Callable所有Guava缓存(无论是否加载)都支持方法`get(K,Callable)`。此方法返回与缓存中的键关联的值,或从指定的“Callable”计算值并将其添加到缓存中。在加载完成之前,不会修改与此缓存关联的可观察状态。此方法提供了一种简单的替代方法,可替代正常的“如果已缓存,则返回;否则创建、缓存并返回”模式。Cachecache=CacheBuilder.newBuilder().maximumSize(1000).build();//lookMa,noCacheLoader...try{//如果键不在“易于计算”组中,我们需要//dothingsthehardway.cache。get(key,newCallable(){@OverridepublicValuecall()throwsAnyException{returndoThingsTheHardWay(key);}});}catch(ExecutionExceptione){thrownewOtherException(e.getCause());}直接插入可以直接使用`cache。放(键,值)`。这将覆盖缓存中指定键的所有先前条目。也可以使用`Cache.asMap()`视图公开的任何`ConcurrentMap`方法来更改缓存。请注意,视图上的任何asMap方法都不会导致条目自动加载到缓存中。此外,此视图上的原子操作在自动缓存加载范围之外运行,因此在使用CacheLoader或Callable加载值的缓存中,应始终优先使用Cache.get(K,Callable)而不是`Cache.asMap().putIfAbsent`。逐出的严酷现实是我们几乎肯定没有足够的内存来缓存我们可以缓存的所有内容。您必须决定:什么时候不值得保留缓存条目?Guava提供三种基本类型的驱逐:基于大小的驱逐、基于时间的驱逐和基于引用的驱逐。基于大小的逐出如果您的缓存在达到一定大小后不应增长,请使用CacheBuilder.maximumSize(long)。缓存将尝试逐出最近最少使用的缓存数据实体。警告:缓存可能会在达到大小限制之前逐出实体——通常是在缓存大小接近限制时。或者,如果不同的缓存实体具有不同的“权重”——例如,如果您的缓存值具有不同的内存占用量——您可以使用`CacheBuilder.weigher(Weigher)`指定权重函数,同时使用`CacheBuilder.maximumWeight(long)`指定最大缓存权重。除了需要与maximumSize相同的限制外,请注意权重是在条目创建时计算的,此后是静态的。LoadingCachegraphs=CacheBuilder.newBuilder().maximumWeight(100000).weigher(newWeigher(){publicintweight(Keyk,Graphg){return.vertices().size();}})复制代码.build(newCacheLoader(){publicGraphload(Keykey){//nocheckedexceptionreturncreateExpensiveGraph(key);}});基于时间的驱逐`CacheBuilder`提供了两种基于时间的驱逐方法:`expireAfterAccess(long,TimeUnit)`仅在自上次通过读或写访问后指定的持续时间过去后才使条目过期。请注意,逐出条目的顺序与基于大小的逐出类似。`expireAfterWrite(long,TimeUnit)`在条目创建或值的最近替换后经过指定时间后使条目过期。如果缓存的数据随着时间的推移继续增长,则可能需要这样做。定时到期在写入期间定期维护,偶尔在读取期间维护,如下所述。基于引用的逐出Guava允许您设置缓存以允许通过为键或值设置弱引用或为值设置软引用来垃圾收集数据实体。`CacheBuilder.weakKeys()`使用弱引用存储键。这允许实体在没有其他引用(强引用或软引用)指向它们的键时被垃圾回收。由于垃圾回收是基于id相等规则,这导致整个缓存需要使用id(`==`)相等来比较键,而不是使用`equals()`。`CacheBuilder.weakValues()`使用弱引用存储值。这允许实体在没有其他引用(强引用或软引用)指向它们的值时被垃圾回收。由于垃圾回收是基于id相等规则,这导致整个缓存需要使用id(`==`)相等来比较值,而不是使用`equals()`。`CacheBuilder.softValues()`将值包装成软引用。软引用对象通过全局最近最少使用规则进行垃圾收集,以响应内存需求。由于使用软引用可能存在一些性能问题,因此我们通常建议改用更可预测的最大缓存大小。使用`softValues()`将导致通过id(`==`)比较值是否相等,而不是使用`equals()`。显式删除在任何时候,您都可以显式地使缓存的实体无效,而无需等待实体被逐出。可以通过以下方式完成:单一失效、使用`Cache.invalidate(key)`的批量失效、使用`Cache.invalidateAll(keys)`的全局失效、使用`Cache.invalidateAll()`的清理何时清理?使用`CacheBuilder`构建的缓存不会“自动”或在它们过期后立即执行清除和逐出值,或任何类似的操作。相反,它在写入操作或偶尔的读取操作(如果写入操作不频繁)期间执行少量维护。这样做的原因是:如果我们要持续进行`Cache`维护,我们需要创建一个线程,这个线程的操作会和用户的操作竞争共享锁。此外,某些环境限制线程创建,这使得“CacheBuilder”在该环境中不可用。相反,我们将选择权留给您。如果您的缓存吞吐量很高,那么您不必担心执行缓存维护以清理过期条目等。如果您的缓存确实很少写入,并且您不想清理以防止缓存读取,您可能希望创建自己的维护线程,定期调用`Cache.cleanUp()`。如果您想为很少写入的缓存安排定期的缓存维护,只需使用ScheduledExecutorService来安排维护操作。刷新刷新与驱逐并不完全相同。如“LoadingCache.refresh(K)”中所述,刷新键可能会异步加载该键的新值。与逐出相反,刷新密钥时仍会返回旧密钥(如果有),强制检索等到重新加载值。如果在刷新时抛出异常,旧值将被保留,异常将被记录并吞噬。`CacheLoader`可以通过覆盖`CacheLoader.reload(K,V)`来指定一些要在刷新时执行的合理行为,这允许您在计算新值时使用旧值。//有些键不需要刷新,我们希望异步刷新。LoadingCachegraphs=CacheBuilder.newBuilder().maximumSize(1000).refreshAfterWrite(1,TimeUnit.MINUTES).build(newCacheLoader(){publicGraphload(Keykey){//nocheckedexceptionreturngetGraphFromDatabase(key);}publicListenableFuturereload(finalKeykey,GraphprevGraph){if(neverNeedsRefresh(key)){returnFutures.immediateFuture(prevGraph);}else{//asynchronous!ListenableFutureTasktask=ListenableFutureTask.create(newCallable(){publicGraphcall(){returngetGraphFromDatabase(key);}});executor.execute(task);returntask;}}});您可以使用CacheBuilder.refreshAfterWrite(long,TimeUnit)将自动定时刷新添加到缓存中。与expireAfterWrite相比,refreshAfterWrite将使键在指定的持续时间后“有资格”刷新,但实际上仅在查询条目时才启动刷新。(如果`CacheLoader.reload`实现为异步,刷新不会减慢查询速度。)因此,例如,您可以在同一个缓存上同时指定`refreshAfterWrite`和`expireAfterWrite`,这样条目的过期计时器不会在条目符合刷新条件时盲目重置,因此如果条目不是符合刷新条件的查询条目,让它过期。