1. Introduction
In this article, let's take a look at Caffeine - a high-performance Java cache library.
One fundamental difference between cache and map is that caches can recycle stored items.
The recycling policy is to delete objects at a specified time. This strategy directly affects the cache hit rate - an important feature of the cache library.
Caffeine provides a near-optimal hit rate due to its use of Window TinyLfu recycling strategy.
2. Dependence
We need to add the caffeine dependency in pom.xml:
<dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.5.5</version></dependency>
You can find the latest version of caffeine on Maven Central.
3. Fill in the cache
Let's take a look at Caffeine's three cache filling strategies: manual, synchronous loading, and asynchronous loading.
First, we write a class for the value type to be stored in the cache:
class DataObject { private final String data; private static int objectCounter = 0; // standard constructors/getters public static DataObject get(String data) { objectCounter++; return new DataObject(data); }}3.1. Manual filling
In this strategy, we manually put the value into the cache before retrieving it.
Let's initialize the cache:
Cache<String, DataObject> cache = Caffeine.newBuilder() .expireAfterWrite(1, TimeUnit.MINUTES) .maximumSize(100) .build();
Now we can use the getIfPresent method to get some values from the cache. If this value does not exist in the cache, this method returns null:
String key = "A";DataObject dataObject = cache.getIfPresent(key); assertNull(dataObject);
We can use the put method to manually fill the cache:
cache.put(key, dataObject);dataObject = cache.getIfPresent(key); assertNotNull(dataObject);
We can also use the get method to get the value, which passes a function with the parameter key as a parameter. If the key does not exist in the cache, the function will be used to provide a fallback value, which is inserted into the cache after calculation:
dataObject = cache .get(key, k -> DataObject.get("Data for A")); assertNotNull(dataObject); assertEquals("Data for A", dataObject.getData());The get method can perform calculations atomically. This means you only do the calculation once - even if multiple threads request the value at the same time. This is why using get is better than getIfPresent.
Sometimes we need to manually invalidate some cached values:
cache.invalidate(key);dataObject = cache.getIfPresent(key); assertNull(dataObject);
3.2. Synchronous loading
This method of loading cache uses a get method with a manual strategy similar to the Function used to initialize values. Let's see how to use it.
First, we need to initialize the cache:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));Now we can use the get method to retrieve the value:
DataObject dataObject = cache.get(key); assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData());We can also use the getAll method to get a set of values:
Map<String, DataObject> dataObjectMap = cache.getAll(Arrays.asList("A", "B", "C")); assertEquals(3, dataObjectMap.size());Retrieve values from the underlying backend initialization function passed to the build method. This allows the use of cache as the main facade of accessing values.
3.3. Asynchronous loading
This policy does the same thing as before, but performs the operation asynchronously and returns a CompletableFuture containing the value:
AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize(100) .expireAfterWrite(1, TimeUnit.MINUTES) .buildAsync(k -> DataObject.get("Data for " + k));We can use the get and getAll methods in the same way, taking into account that they are returning a CompletableFuture:
String key = "A"; cache.get(key).thenAccept(dataObject -> { assertNotNull(dataObject); assertEquals("Data for " + key, dataObject.getData());}); cache.getAll(Arrays.asList("A", "B", "C"))) .thenAccept(dataObjectMap -> assertEquals(3, dataObjectMap.size()));CompleteFuture has many useful APIs, and you can get more in this article.
4. Value recovery
Caffeine has three value recovery strategies: size-based, time-based, and reference-based.
4.1. Recycling based on size
This recycling method assumes that recycling occurs when the configured cache size limit is exceeded. There are two ways to get the size: count the object in the cache, or get the weight.
Let's see how to calculate objects in the cache. When the cache is initialized, its size is equal to zero:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize(1) .build(k -> DataObject.get("Data for " + k)); assertEquals(0, cache.estimatedSize());When we add a value, the size increases significantly:
cache.get("A"); assertEquals(1, cache.estimatedSize());We can add the second value to the cache, which causes the first value to be deleted:
cache.get("B"); cache.cleanUp(); assertEquals(1, cache.estimatedSize());It is worth mentioning that before getting the cache size, we call the cleanUp method. This is because cache recycling is executed asynchronously, and this approach helps wait for the recycling to complete.
We can also pass a weigher function to get the cache size:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumWeight(10) .weighter((k,v) -> 5) .build(k -> DataObject.get("Data for " + k)); assertEquals(0, cache.estimatedSize()); cache.get("A"); assertEquals(1, cache.estimatedSize()); cache.get("B"); assertEquals(2, cache.estimatedSize());When weight exceeds 10, the value is deleted from the cache:
cache.get("C"); cache.cleanUp(); assertEquals(2, cache.estimatedSize());4.2. Based on time recovery
This recycling strategy is based on the expiration time of the entry, and there are three types:
Let's configure the post-access expiration policy using the expireAfterAccess method:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .expireAfterAccess(5, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));To configure the post-write expiration policy, we use the expireAfterWrite method:
cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get("Data for " + k));To initialize a custom policy, we need to implement the Expiry interface:
cache = Caffeine.newBuilder().expireAfter(new Expiry<String, DataObject>() { @Override public long expireAfterCreate( String key, DataObject value, long currentTime) { return value.getData().length() * 1000; } @Override public long expireAfterUpdate( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; } @Override public long expireAfterRead( String key, DataObject value, long currentTime, long currentDuration) { return currentDuration; }}).build(k -> DataObject.get("Data for " + k));4.3. Recycling based on reference
We can configure the cache to enable garbage collection of cached key values. To do this, we configure key and value as weak references, and we can configure only soft references for garbage collection.
When there are no strong references to the object, using WeakRefence can enable garbage collection of objects. SoftReference allows objects to garbage collect based on the JVM's global Least-Recently-Used policy. For more details on Java citations, see here.
We should enable each option using Caffeine.weakKeys(), Caffeine.weakValues(), and Caffeine.softValues():
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .weakKeys() .weakValues() .build(k -> DataObject.get("Data for " + k)); cache = Caffeine.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS) .softValues() .build(k -> DataObject.get("Data for " + k));5. Refresh
The cache can be configured to automatically refresh the entry after a defined period of time. Let's see how to use the refreshAfterWrite method:
Caffeine.newBuilder() .refreshAfterWrite(1, TimeUnit.MINUTES) .build(k -> DataObject.get("Data for " + k));Here we should understand the difference between expireAfter and refreshAfter. When an expired entry is requested, execution will block until the build Function calculates the new value.
However, if the entry can be refreshed, the cache returns an old value and reloads the value asynchronously.
6. Statistics
Caffeine has a way to record cache usage:
LoadingCache<String, DataObject> cache = Caffeine.newBuilder() .maximumSize(100) .recordStats() .build(k -> DataObject.get("Data for " + k));cache.get("A");cache.get("A"); assertEquals(1, cache.stats().hitCount()); assertEquals(1, cache.stats().missCount());We may also pass in recordStats supplier to create an implementation of StatsCounter. This object is pushed every statistic-related change.
7. Conclusion
In this article, we are familiar with Java's Caffeine cache library. We saw how to configure and fill the cache, and how to choose the appropriate expiration or refresh policy based on our needs.
The source code for the examples in this article can be found on Github.
The above is all the content of this article. I hope it will be helpful to everyone's learning and I hope everyone will support Wulin.com more.