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.
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>}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.
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>}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.
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>}For production‑grade performance, the article recommends using the Caffeine library, which offers advanced eviction policies and higher throughput.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
