Mastering ConcurrentHashMap: Build High‑Performance Caches in Spring Boot 3
This article explains why ConcurrentHashMap is ideal for high‑concurrency caching, outlines its key features, and provides step‑by‑step Java examples—including a basic in‑memory cache, an expiring cache, and an auto‑loading cache—plus a recommendation to use Caffeine for production workloads.
1. Introduction
In Java, Map is a collection interface for storing key‑value pairs. For high‑concurrency scenarios, ConcurrentHashMap offers thread‑safe operations without explicit synchronization, making it a popular choice for cache implementations.
1.1 Why choose ConcurrentHashMap?
Thread safety: Built‑in mechanisms ensure safe concurrent access.
Performance: Optimized for high concurrency using techniques such as lock striping.
Ease of use: Simple API integrates seamlessly into existing Java applications.
2. Practical Cases
2.1 Basic In‑Memory Cache
A straightforward cache that stores key‑value pairs in a ConcurrentHashMap and provides basic CRUD methods.
<code>public class BasicCache<K, V> {<br> private final ConcurrentMap<K, V> cache = new ConcurrentHashMap<>();<br><br> /** Retrieve a value from the cache */<br> public V get(K key) {<br> return cache.get(key);<br> }<br><br> /** Insert a value if the key is absent */<br> public V putIfAbsent(K key, V value) {<br> return cache.putIfAbsent(key, value);<br> }<br><br> /** Remove a specific key/value pair */<br> public boolean remove(K key, V value) {<br> return cache.remove(key, value);<br> }<br><br> /** Replace an existing value */<br> public boolean replace(K key, V oldValue, V newValue) {<br> return cache.replace(key, oldValue, newValue);<br> }<br>}</code>This implementation lacks expiration and size limits, which can lead to memory leaks.
2.2 Adding Expiration Mechanism
To prevent stale entries, a scheduled task periodically removes expired items based on timestamps.
<code>public class ExpiringCache<K, V> {<br> private final ConcurrentMap<K, V> cache = new ConcurrentHashMap<>();<br> private final ConcurrentMap<K, Long> timestamps = new ConcurrentHashMap<>();<br> private final ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);<br> private final long expirationDuration;<br> private final TimeUnit timeUnit;<br><br> public ExpiringCache(long expirationDuration, TimeUnit timeUnit) {<br> this.expirationDuration = expirationDuration;<br> this.timeUnit = timeUnit;<br> scheduler.scheduleAtFixedRate(this::removeExpiredEntries, expirationDuration, expirationDuration, timeUnit);<br> }<br><br> public V get(K key) {<br> V v = cache.get(key);<br> if (v != null) {<br> long expirationThreshold = System.nanoTime() - timeUnit.toNanos(expirationDuration);<br> if (timestamps.get(key) < expirationThreshold) {<br> clearKey(key);<br> return null;<br> }<br> timestamps.put(key, System.nanoTime());<br> }<br> return v;<br> }<br><br> public V put(K key, V value) {<br> timestamps.put(key, System.nanoTime());<br> return cache.put(key, value);<br> }<br><br> private void removeExpiredEntries() {<br> long expirationThreshold = System.nanoTime() - timeUnit.toNanos(expirationDuration);<br> for (K key : timestamps.keySet()) {<br> if (timestamps.get(key) < expirationThreshold) {<br> clearKey(key);<br> }<br> }<br> }<br><br> private void clearKey(K key) {<br> timestamps.remove(key);<br> cache.remove(key);<br> }<br><br> public void shutdown() {<br> scheduler.shutdown();<br> }<br>}</code>This adds automatic expiration; size limits would require a queue‑based eviction strategy.
2.3 Automatic Loading Cache
When a key is missing, the cache can load the value using a provided Function and store it.
<code>public class LoadingCache<K, V> {<br> private final ConcurrentMap<K, V> cache = new ConcurrentHashMap<>();<br> private final Function<K, V> loader;<br><br> public LoadingCache(Function<K, V> loader) {<br> this.loader = loader;<br> }<br><br> public V get(K key) {<br> return cache.computeIfAbsent(key, loader);<br> }<br><br> public V putIfAbsent(K key, V value) {<br> return cache.putIfAbsent(key, value);<br> }<br><br> public boolean remove(K key, V value) {<br> return cache.remove(key, value);<br> }<br><br> public boolean replace(K key, V oldValue, V newValue) {<br> return cache.replace(key, oldValue, newValue);<br> }<br><br> public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction) {<br> return cache.computeIfAbsent(key, mappingFunction);<br> }<br><br> public V merge(K key, V value, BiFunction<? super V, ? super V, ? extends V> remappingFunction) {<br> return cache.merge(key, value, remappingFunction);<br> }<br>}</code>For production‑grade performance, the article recommends using the Caffeine library, which offers advanced eviction policies and higher throughput.
Spring Full-Stack Practical Cases
Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.
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.