1, the introduction of

In this article, we’ll look at Caffeine, a high-performance caching library for Java.

One fundamental difference between a cache and a Map is that the cache cleans up the stored items.

A cleanup policy that determines which objects should be deleted at a given time directly affects the cache hit ratio, a key feature of the cache library.

Caffeine uses the Window Tinylfu cleanup strategy, which provides a close to optimal hit ratio.

2, rely on

We need to add the Caffeine dependency to our pom.xml:

< the dependency > < groupId > com. Making. Ben - manes. Caffeine < / groupId > < artifactId > caffeine < / artifactId > < version > 2.5.5 < / version >  </dependency>

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

3. Write to the cache

Let’s focus on Caffeine’s three cache write strategies: manual, synchronous, and asynchronous.

First, let’s write a class as the type of the value 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 writing

In this strategy, we manually write the values to the cache and read them later.

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 the value does not exist in the cache, this method returns null:

String key = "A";
DataObject dataObject = cache.getIfPresent(key);

assertNull(dataObject);

We can manually write to the cache using the put method:

cache.put(key, dataObject);
dataObject = cache.getIfPresent(key);

assertNotNull(dataObject);

We can also get the value using the get method, which takes a function and a key as arguments. If the key does not exist in the cache, then this function will be used to provide a backstory value, which will be written to the cache after execution:

dataObject = cache
  .get(key, k -> DataObject.get("Data for A"));

assertNotNull(dataObject);
assertEquals("Data for A", dataObject.getData());

The GET method execution is atomic. This means that even if multiple threads request the value at the same time, the execution takes place only once. That’s 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 the cache requires a Function to initialize the write value, similar to the get method of the manual write policy, so let’s see how to use it.

First, we need to initialize our 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 read 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());

The value is read from the underlying back-end initialization Function passed to the build method, so you can use the cache as the primary entry point to access the value.

3.3. Asynchronous loading

This policy works the same as the previous one, but executes the action asynchronously and returns a completableFuture to hold the actual 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, considering that they return 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()));

CompletableFuture has a number of useful APIs that you can read more about in this article.

4. Cleaning up cache values

Caffeine has three cleanup strategies for cached values: size based, time based, and reference based.

4.1 Size-based Cleaning

This type of cleanup is designed to occur when the size limit configured for the cache is exceeded. There are two ways to get the size — count the number of objects in the cache, or get their weights.

Let’s look at how to count the number of objects in the cache. When the cache is initialized, its size is 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 will cause the first value to be deleted:

cache.get("B");
cache.cleanUp();

assertEquals(1, cache.estimatedSize());

It’s worth mentioning that we called the cleanUp method before getting the cache size. This is because cache cleanup is performed asynchronously, and this method helps to wait for the cleanup to complete.

We can also pass in a weigher Function to define the cache size fetch:

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());

When the weight exceeds 10, these values are removed from the cache:

cache.get("C");
cache.cleanUp();

assertEquals(2, cache.estimatedSize());

4.2 Time-based cleaning

This cleanup strategy, based on the expiration time of an entry, falls into three categories:

  • Expiration after access – An entry expires after a certain amount of time has elapsed since it was last read or written
  • Expiration after write – An entry has expired after a certain amount of time has elapsed since the last write
  • Custom policy – byExpiryTo calculate the expiry time for each item separately

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 the 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 Reference-Based Cleaning

We can configure our cache to allow garbage collection of cached keys or values or both. To do this, we need to configure the use of WeakReference for keys and values, and we can configure the SoftreReference for garbage collection of values only.

The use of WeakReference allows the object to be garbage collected without any strong reference to the object. SoftreReference allows object garbage collection based on the JVM’s global LRU (least-recently used) policy. You can find more detailed information about references in Java here.

We use Caffin.weakKeys (), Caffin.weakValues () and Caffin.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));

5. Cache refresh

Caches can be configured to automatically refresh entries after a defined period of time. Let’s see how to do this using the refreshAfterWrite method:

Caffeine.newBuilder()
  .refreshAfterWrite(1, TimeUnit.MINUTES)
  .build(k -> DataObject.get("Data for " + k));

Here we should understand one difference between expireAfter and refreshAfter: when requesting an expired item, the execution blocks until the build function calculates the new value. But if the entry meets the refresh criteria, the cache returns an old value and asynchronously reloads the value.

6, statistics

Caffeine provides a way to track cache usage statistics:

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 can also create an implementation of StatsCounter as a parameter to pass RecordStats. This implementation object will be called every time a statistics-related change is made.

7, the conclusion

In this article, you learned about the Java Caffeine cache library. We’ve seen how to configure and cache, and how to choose the appropriate expiration or refresh policy as needed.

Original: https://www.baeldung.com/java…

Code farmers panda

For more technical products, please visit my personal website https://pinmost.com, or pay attention to the public account [Code Panda].