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.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Mastering ConcurrentHashMap: Build High‑Performance Caches in Spring Boot 3

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

backend-developmentcachingSpring BootConcurrentHashMap
Spring Full-Stack Practical Cases
Written by

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.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.