Google Guava 被誉为是JAVA类库中的瑞士军刀。能显著简化代码,让代码易写、易读、易于维护。同时可以大幅提高程序员的工作效率,让我们从大量重复的底层代码中脱身。
由于 Google Guava 类库包含大量非常有用的特性,无法在一篇文章中尽述。本篇仅简单介绍 Google Guava 中的缓存工具的使用。
使用 Maven 进行项目构建时,添加下面的依赖:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>29.0-jre</version> <!-- or, for Android: --> <version>29.0-android</version> </dependency>
使用 Gradle 进行项目构建时,添加下面的依赖:
dependencies { // Pick one: // 1. Use Guava in your implementation only: implementation("com.google.guava:guava:29.0-jre") // 2. Use Guava types in your public API: api("com.google.guava:guava:29.0-jre") // 3. Android - Use Guava in your implementation only: implementation("com.google.guava:guava:29.0-android") // 4. Android - Use Guava types in your public API: api("com.google.guava:guava:29.0-android") }
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener(MY_LISTENER) .build( new CacheLoader<Key, Graph>() { @Override public Graph load(Key key) throws AnyException { return createExpensiveGraph(key); } });
缓存有非常广泛的应用场景。比如,你应该为那些计算或者查询代价高昂的数据使用缓存,或者你需要某个输入数据很多次的场景。
一个 `Cache` 类似于 `ConcurrentMap`,不过并不完全相同。基本的差异在于, `ConcurrentMap` 持久化所有添加进来的元素直到它们被显式删除。另一方面,通常将 `Cache` 配置为自动淘汰条目,以限制其内存占用量。在某些情况下, `LoadingCache` 会很有用,虽然它不淘汰条目,但是可以自动加载缓存。
通常,Guava 缓存工具可以适用于下列场景:
如果这些都适用于您的应用场景,那么 Guava 缓存实用程序将很适合您!
如上面的示例代码所示,使用 `CacheBuilder` 生成器模式可以获取 `Cache`,但是自定义缓存是有趣的部分。
注意:如果不需要 `Cache` 的功能,则 `ConcurrentHashMap` 的内存使用效率更高——但是很难用任何旧的 `ConcurrentMap`来复制大多数 `Cache` 的功能。
你需要问自己有关缓存的第一个问题是:是否有一些合理的默认函数来加载或计算与键关联的值?如果是这样,您应该使用 `CacheLoader`。如果不是这样,或者如果您需要覆盖默认值,但是仍然需要原子的 "get-if-absent-compute" 语义,则应该将 `Callable` 传递给 `get` 调用。可以使用 `Cache.put` 直接插入元素,但是首选自动加载缓存,因为这样可以更轻松地推断所有缓存内容的一致性。
`LoadingCache` 是一个通过附属的 `CacheLoader` 构建的 `Cache`。创建一个 `CacheLoader` 通常与实现 `V load(K key) throws Exception` 方法一样。因此,比如,你可以使用下面的代码创建一个 `LoadingCache` :
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` 可能会抛出 `Exception`,因此 `LoadingCache.get(K)` 会抛出 `ExecutionException`。(如果缓存加载器抛出 unchecked 异常,则`get(K)` 会引发包装了 `UncheckedExecutionException` 的异常。)您还可以选择使用 `getUnchecked(K)` 将所有异常包装在 `UncheckedExecutionException` 中, 但是如果底层的 `CacheLoader` 通常会抛出受检查异常,这可能会导致令人惊讶的行为。
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)`。此方法返回与缓存中的键关联的值,或从指定的 `Callable` 中计算出该值并将其添加到缓存中。在加载完成之前,不会修改与此缓存关联的可观察状态。此方法为常规的“如果已缓存,则返回;否则创建,缓存并返回”模式提供了简单的替代方法。
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` 视图上的任何方法都不会导致条目自动加载到缓存中。此外,该视图上的原子操作在自动缓存加载范围之外运行,因此在使用 `CacheLoader` 或 `Callable` 加载值的缓存中,始终应优先选择 `Cache.get(K, Callable<V>)` 而不是 `Cache.asMap().putIfAbsent` 。
冷酷的现实是,我们几乎肯定没有足够的内存来缓存我们可以缓存的所有内容。您必须决定:什么时候不值得保留缓存条目?Guava 提供三种基本的驱逐类型:基于大小的驱逐,基于时间的驱逐和基于引用的驱逐。
如果你的缓存在达到某个大小之后就不应该继续增长,可以使用 `CacheBuilder.maximumSize(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); } });
定时到期是在写入过程中进行定期维护的,偶尔在读取过程中进行维护,如下所述。
Guava 允许你设置你的缓存以允许数据实体的垃圾收集,通过对键或者值使用的 weak references ,或者对值使用的 soft references 进行设置。
任何时刻,你都可以显式废除缓存实体,而不需要等待实体被驱逐。可以通过以下方法:
用 `CacheBuilder` 构建的缓存不会“自动”或在值过期后立即执行清除和逐出值,或类似的任何操作。取而代之的是,它在写操作期间或偶尔进行的读操作(如果很少进行写操作)中执行少量维护。
这样做的原因如下:如果我们要连续执行 `Cache` 维护,则需要创建一个线程,并且该线程的操作将与用户操作竞争共享锁。另外,某些环境限制了线程的创建,这会使 `CacheBuilder` 在该环境中无法使用。
相反,我们会将选择权交给您。如果您的缓存是高吞吐量的,那么您不必担心执行缓存维护以清理过期的条目等。 如果您的缓存确实很少写入,并且您不想清理来阻止缓存读取,则您可能希望创建自己的维护线程,该线程定期调用 `Cache.cleanUp()`。
如果要为很少写入的缓存安排定期的缓存维护,只需使用 `ScheduledExecutorService` 调度维护操作。
刷新与驱逐并不完全相同。如 `LoadingCache.refresh(K)` 所述,刷新键可能会异步加载该键的新值。与驱逐相反,旧键(如果有的话)在刷新键时仍会返回,这迫使检索要等到重新加载该值。
如果刷新时引发异常,则将保留旧值,并记录并吞下该异常。
`CacheLoader` 可以通过覆盖 `CacheLoader.reload(K, V)` 指定某些将要在刷新时执行的明智行为,它允许您在计算新值时使用旧值。
// 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.refreshAfterWrite(long, TimeUnit)` 将自动定时刷新添加到缓存中。与 `expireAfterWrite` 相比,`refreshAfterWrite` 在指定的持续时间后将使键“具有资格”进行刷新,但实际上仅在查询条目时才会启动刷新。(如果将 `CacheLoader.reload` 实现为异步,则刷新不会降低查询的速度。)因此,例如,您可以在同一缓存上同时指定 `refreshAfterWrite` 和 `expireAfterWrite`,以便只要条目符合刷新资格,就不会盲目地重置条目的过期计时器,因此,如果在符合刷新资格后不查询条目,则允许它过期。