Backend Development 11 min read

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.

<code>public class BasicCache&lt;K, V&gt; {<br>  private final ConcurrentMap&lt;K, V&gt; cache = new ConcurrentHashMap&lt;&gt;();<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&lt;K, V&gt; {<br>  private final ConcurrentMap&lt;K, V&gt; cache = new ConcurrentHashMap&lt;&gt;();<br>  private final ConcurrentMap&lt;K, Long&gt; timestamps = new ConcurrentHashMap&lt;&gt;();<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&lt;K, V&gt; {<br>  private final ConcurrentMap&lt;K, V&gt; cache = new ConcurrentHashMap&lt;&gt;();<br>  private final Function&lt;K, V&gt; loader;<br><br>  public LoadingCache(Function&lt;K, V&gt; 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&lt;? super K, ? extends V&gt; mappingFunction) {<br>    return cache.computeIfAbsent(key, mappingFunction);<br>  }<br><br>  public V merge(K key, V value, BiFunction&lt;? super V, ? super V, ? extends V&gt; 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.

Javabackend 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

login 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.