Original: www.baeldung.com/java-cachin…

Author: baeldung

From stackGC

1, the introduction

In this article, I introduce Caffeine, a high-performance Java cache library.

One fundamental difference between a cache and a Map is that the cache can reclaim stored items.

The recycle policy is what objects are deleted at a specified time. This policy directly affects cache hit ratio, an important feature of cache libraries.

Caffeine, which uses Windows TinyLfu recycling strategy, provides a near-optimal hit ratio.

2, rely on

We need to add caffeine dependency to pom.xml:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>2.5.5</version>
</dependency>
Copy the code

You can find the latest version of Caffeine at Maven Central.

3. Populate the cache

Let’s take a look at Caffeine’s three cache padding strategies: manual, synchronous and asynchronous loading.

First, we write a class for the type of value we want to store 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 newDataObject(data); }}Copy the code

3.1. Manual filling

In this strategy, we manually put values into the cache and then retrieve them.

Initialize the cache:

Cache<String, DataObject> cache = Caffeine.newBuilder()
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .maximumSize(100)
  .build();
Copy the code

Now we can use the getIfPresent method to get the value from the cache. If the specified value does not exist in the cache, the method returns NULL:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);
 
assertNull(dataObject);
Copy the code

We can manually populate the cache using the put method:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);
 
assertNotNull(dataObject);
Copy the code

We can also get the value using the get method, which passes in a Function that takes key as an argument. If the key does not exist in the cache, this function is used to provide a default 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());
Copy the code

The GET method can perform calculations atomically. This means you only do the calculation once — even if there are multiple threads requesting the value at the same time. That’s why using GET is better than getIfPresent.

Sometimes we need to manually trigger some cached value invalidation:

cache.invalidate(key);
dataObject = cache.getIfPresent(key);
 
assertNull(dataObject);
Copy the code

3.2. Synchronous loading

This way of loading the cache uses a get method similar to the manual strategy used to initialize functions of 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));
Copy the code

Now we can use the get method to retrieve values:

DataObject dataObject = cache.get(key);
 
assertNotNull(dataObject);
assertEquals("Data for " + key, dataObject.getData());
Copy the code

Of course, you 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());
Copy the code

Retrieves values from the initialization function passed to the build method, which makes it possible to use the cache as the primary Facade for accessing values.

3.3. Asynchronous loading

This policy does the same thing as before, but performs the operation asynchronously and returns a CompletableFuture with a value:

AsyncLoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(100)
  .expireAfterWrite(1, TimeUnit.MINUTES)
  .buildAsync(k -> DataObject.get("Data for " + k));
Copy the code

We can use the get and getAll methods in the same way, taking into account that they return 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()));
Copy the code

CompletableFuture has a lot of useful apis, and you can learn more about them in this article.

4. Value collection

Caffeine has three value collection strategies: size based, time based and reference based.

4.1. Collection based on size

This collection approach assumes that a collection will occur when the cache size exceeds the configured size limit. There are two ways to get the size: count objects in the cache, or get weights.

Let’s look at how to calculate objects in the cache. When the cache is initialized, its size equals zero:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumSize(1)
  .build(k -> DataObject.get("Data for " + k));
 
assertEquals(0, cache.estimatedSize());
Copy the code

When we add a value, the size increases significantly:

cache.get("A");
 
assertEquals(1, cache.estimatedSize());
Copy the code

We can add the second value to the cache, which will cause the first value to be deleted:

cache.get("B");
cache.cleanUp();
 
assertEquals(1, cache.estimatedSize());
Copy the code

It is worth mentioning that before getting the cache size, we call the cleanUp method. This is because cache reclamation is performed asynchronously, which helps to wait for the collection to complete.

We can also pass a weigher Function to get the size of the cache:

LoadingCache<String, DataObject> cache = Caffeine.newBuilder()
  .maximumWeight(10)
  .weigher((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());
Copy the code

When weight exceeds 10, the value is removed from the cache:

cache.get("C");
cache.cleanUp();
 
assertEquals(2, cache.estimatedSize());
Copy the code

4.2 Time-based collection

This collection strategy is based on the expiration date of an item and comes in three types:

  • Expire after access – An entry expires after the last read or write occurred.
  • Write expiration – An entry expires after the last write occurred
  • Custom policy – The Expiry date is calculated solely by the Expiry implementation

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));
Copy the code

To configure a write expiration policy, we use the expireAfterWrite method:

cache = Caffeine.newBuilder()
  .expireAfterWrite(10, TimeUnit.SECONDS)
  .weakKeys()
  .weakValues()
  .build(k -> DataObject.get("Data for " + k));
Copy the code

Expiry interface to initialize the custom policy, we need to implement 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));
Copy the code

4.3 reference based reclamation

We can configure the cache to enable garbage collection based on 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 is no strong reference to an object, using WeakRefence enables garbage collection of the object. SoftReference allows an object to collect garbage based on the GLOBAL least-recently-used policy of the JVM. For more details on Java references, see here.

We should use Caffeine.Weakkeys (), Caffeine.WeakValues () and Caffeine.softValues() to enable each option:

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));
Copy the code

5, refresh

The cache can be configured to automatically refresh entries after a specified 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));
Copy the code

Here we need to understand the difference between expireAfter and refreshAfter. When an expired item is requested, execution blocks until build Function evaluates the new value.

However, if the entry can be refreshed, the cache returns an old value and asynchronously reloads the value.

6, statistics

Caffeine has the statistics of recorded 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());
Copy the code

We can also pass in recordStats supplier to create an implementation of StatsCounter. This object is pushed each time a statistics-related change is made.

7, the conclusion

In this article, you familiarized yourself with Java’s Caffeine cache library, learned how to configure and populate the cache, and how to choose the appropriate expiration or refresh strategy for your needs.

The source code for the examples in this article is available on Github.