Mastering Caffeine Cache: High‑Performance Java Caching Techniques
This guide introduces Caffeine, a high‑performance Java caching library, explains its core features, configuration options, loading strategies—including manual, automatic, asynchronous, and async loading caches—eviction policies, removal listeners, and statistics collection, providing code examples for each use case.
1. Understanding Caffeine
Caffeine is a high‑performance caching library built on Java 8 that offers near‑optimal hit rates and is the default local cache implementation in Spring Boot.
It provides a flexible builder to create caches with features such as automatic loading (optionally asynchronous), size‑based eviction, time‑based expiration (from last access or write), asynchronous refresh, weak/soft references for keys and values, eviction notifications, and access statistics.
Core Classes and Parameters
Caffeine: the base builder for creating high‑performance caches.
Key parameters:
maximumSize: maximum number of entries.
maximumWeight: maximum total weight (cannot be used with maximumSize).
initialCapacity: initial cache capacity.
expireAfterWriteNanos: expire after a given number of nanoseconds since write.
expireAfterAccessNanos: expire after a given number of nanoseconds since last access.
refreshAfterWriteNanos: refresh after a given number of nanoseconds since write.
2. Data Loading
Caffeine supports four loading strategies.
Manual loading
<code>public static void demo() {</code>
<code> Cache<String, String> cache = Caffeine.newBuilder()</code>
<code> .expireAfterAccess(Duration.ofMinutes(1))</code>
<code> .maximumSize(100)</code>
<code> .recordStats()</code>
<code> .build();</code>
<code> // put data</code>
<code> cache.put("a", "a");</code>
<code> // get if present</code>
<code> String a = cache.getIfPresent("a");</code>
<code> System.out.println(a);</code>
<code> // get or load</code>
<code> String b = cache.get("b", k -> {</code>
<code> System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code> try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code> System.out.println("end query ...");</code>
<code> return UUID.randomUUID().toString();</code>
<code> });</code>
<code> System.out.println(b);</code>
<code> // invalidate</code>
<code> cache.invalidate("a");</code>
<code>}</code>Automatic loading
<code>public static void demo() {</code>
<code> LoadingCache<String, String> loadingCache = Caffeine.newBuilder()</code>
<code> .maximumSize(100)</code>
<code> .expireAfterWrite(10, TimeUnit.MINUTES)</code>
<code> .build(new CacheLoader<String, String>() {</code>
<code> @Nullable @Override</code>
<code> public String load(@NonNull String key) {</code>
<code> return createExpensiveValue();</code>
<code> }</code>
<code> @Override</code>
<code> public @NonNull Map<String, String> loadAll(@NonNull Iterable<? extends String> keys) {</code>
<code> if (keys == null) { return Collections.emptyMap(); }</code>
<code> Map<String, String> map = new HashMap<>();</code>
<code> for (String key : keys) { map.put(key, createExpensiveValue()); }</code>
<code> return map;</code>
<code> }</code>
<code> });</code>
<code> String a = loadingCache.get("a");</code>
<code> System.out.println(a);</code>
<code> Set<String> keys = new HashSet<>();</code>
<code> keys.add("a"); keys.add("b");</code>
<code> Map<String, String> allValues = loadingCache.getAll(keys);</code>
<code> System.out.println(allValues);</code>
<code>}</code>
<code>private static String createExpensiveValue() {</code>
<code> System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code> try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code> System.out.println("end query ...");</code>
<code> return UUID.randomUUID().toString();</code>
<code>}</code>A
LoadingCacheadds a
CacheLoaderto a regular cache.
The
getAllmethod invokes
CacheLoader.loadfor each missing key, and you can override
loadAllfor batch efficiency.
Manual asynchronous loading
<code>public static void demo() throws ExecutionException, InterruptedException {</code>
<code> AsyncCache<String, String> asyncCache = Caffeine.newBuilder()</code>
<code> .maximumSize(100)</code>
<code> .buildAsync();</code>
<code> asyncCache.put("a", CompletableFuture.completedFuture("a"));</code>
<code> CompletableFuture<String> a = asyncCache.getIfPresent("a");</code>
<code> System.out.println(a.get());</code>
<code> CompletableFuture<String> cf = asyncCache.get("b", k -> createExpensiveValue("b"));</code>
<code> System.out.println(cf.get());</code>
<code> asyncCache.synchronous().invalidate("a");</code>
<code> System.out.println(asyncCache.getIfPresent("a"));</code>
<code>}</code>
<code>private static String createExpensiveValue(String key) {</code>
<code> System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code> try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code> System.out.println("end query ...");</code>
<code> return UUID.randomUUID().toString();</code>
<code>}</code>An
AsyncCachegenerates cache entries on an
Executorand returns
CompletableFuture, allowing integration with reactive programming models. The
synchronous()method blocks until the asynchronous computation completes.
The default thread pool is
ForkJoinPool.commonPool(), but you can supply a custom executor via
Caffeine.executor(Executor).
Automatic asynchronous loading
<code>public static void demo() throws ExecutionException, InterruptedException {</code>
<code> AsyncLoadingCache<String, String> cache = Caffeine.newBuilder()</code>
<code> .maximumSize(10_000)</code>
<code> .expireAfterWrite(10, TimeUnit.MINUTES)</code>
<code> .buildAsync((key, executor) -> createExpensiveValueAsync(key, executor));</code>
<code> CompletableFuture<String> a = cache.get("a");</code>
<code> System.out.println(a.get());</code>
<code> Set<String> keys = new HashSet<>();</code>
<code> keys.add("a"); keys.add("b");</code>
<code> CompletableFuture<Map<String, String>> values = cache.getAll(keys);</code>
<code> System.out.println(values.get());</code>
<code>}</code>
<code>private static String createExpensiveValue(String key) {</code>
<code> System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code> try { Thread.sleep(1000); } catch (InterruptedException e) {}</code>
<code> System.out.println("end query ...");</code>
<code> return UUID.randomUUID().toString();</code>
<code>}</code>
<code>private static CompletableFuture<String> createExpensiveValueAsync(String key, Executor executor) {</code>
<code> System.out.println("begin query ..." + Thread.currentThread().getName());</code>
<code> try { Thread.sleep(1000); executor.execute(() -> System.out.println("async create value....")); } catch (InterruptedException e) {}</code>
<code> System.out.println("end query ...");</code>
<code> return CompletableFuture.completedFuture(UUID.randomUUID().toString());</code>
<code>}</code>An
AsyncLoadingCachecombines
AsyncCachewith an
AsyncCacheLoader, suitable for asynchronous value generation.
3. Eviction Strategies
Caffeine offers three eviction strategies: size‑based, time‑based, and reference‑based, plus manual removal and listeners.
Size‑based eviction
<code>Cache<String, String> cache = Caffeine.newBuilder()</code>
<code> .maximumSize(100)</code>
<code> .recordStats()</code>
<code> .build();</code>
<code>AsyncCache<String, String> asyncCache = Caffeine.newBuilder()</code>
<code> .maximumWeight(10)</code>
<code> .buildAsync();</code>Time‑based eviction
<code>// Fixed time after last access</code>
<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code> .expireAfterAccess(Duration.ofMinutes(1))</code>
<code> .recordStats()</code>
<code> .build();</code>
<code>// Fixed time after write</code>
<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code> .expireAfterWrite(Duration.ofMinutes(1))</code>
<code> .recordStats()</code>
<code> .build();</code>
<code>// Custom expiry</code>
<code>Cache<String, String> expire = Caffeine.newBuilder()</code>
<code> .expireAfter(new Expiry<String, String>() {</code>
<code> @Override public long expireAfterCreate(...){ return LocalDateTime.now().plusMinutes(5).getSecond(); }</code>
<code> @Override public long expireAfterUpdate(...){ return currentDuration; }</code>
<code> @Override public long expireAfterRead(...){ return currentDuration; }</code>
<code> })</code>
<code> .recordStats()</code>
<code> .build();</code>Three time‑based methods:
expireAfterAccess(long, TimeUnit): evicts when a value has not been accessed for the specified duration.
expireAfterWrite(long, TimeUnit): evicts when a value has not been written or updated for the specified duration.
expireAfter(Expiry): evicts based on a custom
Expiryimplementation.
Reference‑based eviction
Reference types:
<code>// Weak keys and values</code>
<code>LoadingCache<Object, Object> weak = Caffeine.newBuilder()</code>
<code> .weakKeys()</code>
<code> .weakValues()</code>
<code> .build(k -> createExpensiveValue());</code>
<code>// Soft values</code>
<code>LoadingCache<Object, Object> soft = Caffeine.newBuilder()</code>
<code> .softValues()</code>
<code> .build(k -> createExpensiveValue());</code> weakKeysand
weakValuesallow garbage collection when no strong references exist;
softValuesare reclaimed under memory pressure.
Manual removal
<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code> .expireAfterWrite(Duration.ofMinutes(1))</code>
<code> .recordStats()</code>
<code> .build();</code>
<code>// single</code>
<code>cache.invalidate("a");</code>
<code>// bulk</code>
<code>Set<String> keys = new HashSet<>(); keys.add("a"); keys.add("b");</code>
<code>cache.invalidateAll(keys);</code>
<code>// all</code>
<code>cache.invalidateAll();</code>Removal listeners
<code>Cache<Object, Object> cache = Caffeine.newBuilder()</code>
<code> .expireAfterWrite(Duration.ofMinutes(1))</code>
<code> .recordStats()</code>
<code> .evictionListener((key, value, cause) -> System.out.println("evict cause " + cause))</code>
<code> .removalListener((key, value, cause) -> System.out.println("removed cause " + cause))</code>
<code> .build();</code>Listeners run asynchronously using the default
ForkJoinPool.commonPool(), but a custom executor can be supplied via
Caffeine.executor(Executor).
Common eviction causes:
EXPLICIT – manual removal.
REPLACED – entry replaced by a new put.
COLLECTED – garbage collection of weak/soft references.
EXPIRED – time‑based expiration.
SIZE – size‑based eviction.
4. Cache Statistics
Calling
Caffeine.recordStats()enables collection of metrics such as hit count, hit rate, miss count, miss rate, load success/failure counts, total load time, eviction count, eviction weight, request count, and average load penalty.
<code>CacheStats stats = cache.stats();</code>
<code>System.out.println("stats.hitCount():" + stats.hitCount());</code>
<code>System.out.println("stats.hitRate():" + stats.hitRate());</code>
<code>System.out.println("stats.missCount():" + stats.missCount());</code>
<code>System.out.println("stats.missRate():" + stats.missRate());</code>
<code>System.out.println("stats.loadSuccessCount():" + stats.loadSuccessCount());</code>
<code>System.out.println("stats.loadFailureCount():" + stats.loadFailureCount());</code>
<code>System.out.println("stats.loadFailureRate():" + stats.loadFailureRate());</code>
<code>System.out.println("stats.totalLoadTime():" + stats.totalLoadTime());</code>
<code>System.out.println("stats.evictionCount():" + stats.evictionCount());</code>
<code>System.out.println("stats.evictionWeight():" + stats.evictionWeight());</code>
<code>System.out.println("stats.requestCount():" + stats.requestCount());</code>
<code>System.out.println("stats.averageLoadPenalty():" + stats.averageLoadPenalty());</code>JD Cloud Developers
JD Cloud Developers (Developer of JD Technology) is a JD Technology Group platform offering technical sharing and communication for AI, cloud computing, IoT and related developers. It publishes JD product technical information, industry content, and tech event news. Embrace technology and partner with developers to envision the future.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.