Welcome to the final lesson in our module on Java Concurrency. In our previous session, we explored how to get results back from asynchronous tasks using Callable and Future. This was a crucial step, but it leads to a new challenge: what happens when multiple concurrent tasks need to access and modify the same piece of shared data?
Simply allowing multiple threads to access a standard HashMap or ArrayList can lead to unpredictable behavior, data corruption (race conditions), and crashes. Today, we'll tackle this problem head-on. You will learn how to use ConcurrentHashMap, a high-performance, thread-safe collection that is a cornerstone of concurrent Java programming. Mastering this class is essential for building robust and scalable backend systems, directly supporting your goal of enterprise Java development.
1. The Perils of Sharing: Why HashMap Isn't Enough
As you know from your data structures training, a HashMap is incredibly efficient for single-threaded lookups. However, it was not designed for a multithreaded world. When multiple threads try to read from and write to the same HashMap simultaneously, chaos can ensue.
There are two primary problems you will encounter:
- Race Conditions: If two threads try to modify the map at the same time, one thread's update might be lost, leading to incorrect data. The final state of the map becomes dependent on the unpredictable timing of thread execution.
ConcurrentModificationException: If one thread tries to modify the map (e.g., add or remove an element) while another thread is iterating over it, the JVM will throw aConcurrentModificationExceptionto prevent further corruption. The iterator becomes invalid.
Let's watch a clear demonstration of these two issues.
Internal working of Concurrent HashMap & Interview Questions - JAVA | Concurrent Collections
The video "Internal working of Concurrent HashMap" by Code With Ease - By Varsha provides excellent code demos of these exact problems.
First, watch the section demonstrating a race condition from the code. Notice how five threads, each trying to add 100 items to a HashMap, fail to produce a final size of 500. Then, see how simply switching to a ConcurrentHashMap solves the problem. Next, watch the segment on ConcurrentModificationException from another code example. This shows how modifying a HashMap during iteration throws an exception, while a ConcurrentHashMap handles it gracefully.
These examples make it clear that for concurrent applications, HashMap is not a safe choice.
2. A Tale of Two Locks: synchronizedMap vs. ConcurrentHashMap
The simplest way to make a HashMap thread-safe is to wrap it using the Collections.synchronizedMap() utility method.
Map<String, String> synchronizedMap = Collections.synchronizedMap(new HashMap<>());
This creates a Map where every single method (put, get, remove, etc.) is wrapped in a synchronized block. This approach works, but it comes at a high cost. It uses a coarse-grained lock, meaning only one thread can access the map at any given time for any operation, whether it's reading or writing. All other threads are blocked, waiting their turn. This can severely limit the concurrency and performance of your application, creating a major bottleneck.

ConcurrentHashMap offers a much more sophisticated and performant solution based on a principle called fine-grained locking (also known as lock striping). Instead of a single lock for the entire map, ConcurrentHashMap internally divides its data structure into segments or buckets, each with its own lock.

When a thread wants to write to the map, it only needs to lock the specific segment where the data resides. Other threads that need to access different segments can do so without being blocked. This strategy allows for a high degree of concurrency.
Let's solidify this comparison.
Internal working of Concurrent HashMap & Interview Questions - JAVA | Concurrent Collections
The same video from Code With Ease - By Varsha also provides a great comparison.
Watch the section comparing ConcurrentHashMap and Collections.synchronizedMap from the next topic. Pay close attention to the key differences highlighted: concurrency level, performance, and locking granularity.
3. Under the Hood: How ConcurrentHashMap Achieves High Throughput
The design of ConcurrentHashMap leads to two very important behaviors that you must understand to use it effectively:
- Reads are non-blocking and fully concurrent. Reading a value with
get()typically does not require any locking. Multiple threads can read from the map at the same time as other threads are writing to it. The trade-off is that a read operation might retrieve a value that has just been updated by another thread, a concept known as eventual consistency. For most applications, this is perfectly acceptable and a worthwhile price for the massive performance gain. - Writes are concurrent, but block at the entry/bucket level. When you write to the map,
ConcurrentHashMaplocks only the specific portion of the map being modified. This allows multiple threads to write to different parts of the map simultaneously. If two threads happen to write to the same part, one will block and wait for the other to finish, ensuring data integrity.
The Baeldung article below provides excellent code examples that demonstrate these read/write behaviors using an ExecutorService, just like we did in the last lesson.
Reading and Writing With a ConcurrentHashMap
This article from Baeldung is perfect for understanding the practical implications of ConcurrentHashMap's design.
First, read the brief "Why Use ConcurrentHashMap" section, which starts here, to reinforce the concept of entry-level locking. Then, carefully study the "Reading" section starting here. The code example shows a read happening while a write is in progress, illustrating eventual consistency. Finally, read the "Writing" section starting here. The two code examples are key: the first shows that two threads writing to the same key will block each other, while the second shows that two threads writing to different keys can proceed in parallel without blocking.
4. Atomic Operations for Compound Actions
A common pattern in concurrent programming is the "check-then-act" sequence: you check if a key exists, and if not, you add it. With a traditional map, this requires at least two separate operations (containsKey and put), creating a window where another thread could interfere.
ConcurrentHashMap provides atomic methods for these compound actions. One of the most useful is computeIfAbsent. This method takes a key and a mapping function. It checks for the key; if it's present, it does nothing. If it's absent, it uses your function to compute a value, inserts it into the map, and returns it—all as a single, indivisible (atomic) operation.
Java Concurrent Collections Tutorial
This tutorial from in28minutes has a fantastic demonstration of why atomic operations are needed and how to use computeIfAbsent.
Watch from welcome back. The video first shows a typical (and non-thread-safe) way to implement a character counter. It then refactors the code to use ConcurrentHashMap and computeIfAbsent, reducing multiple lines of unsafe code to a single, elegant, and thread-safe line. It also introduces LongAdder, a class optimized for high-contention counters, which we will use in our exercise.
5. Coding Exercise: Concurrent Event Counter
Let's apply these concepts to a practical problem. Imagine you are building a backend service that processes user activity streams from multiple sources. You need to maintain a real-time count of different event types (LOGIN, ADD_TO_CART, CHECKOUT, etc.).
Goal: Write a program that uses ConcurrentHashMap and an ExecutorService to concurrently process and count events.
-
Setup:
- Create a
ConcurrentHashMap<String, LongAdder>. We'll useLongAdderas it's highly efficient for concurrent counting. - Create a
FixedThreadPoolusingExecutors.
- Create a
-
The Task:
- Create a
Runnableclass calledEventProcessor. Its constructor should accept aList<String>of events. - In the
run()method, loop through the list of events. For each event string:- Use the
computeIfAbsentmethod on your map. The key is the event string. The mapping function should create anew LongAdder(). - Chain the
increment()method call directly onto thecomputeIfAbsentcall. The full line should look like this:eventCounts.computeIfAbsent(event, k -> new LongAdder()).increment();
- Use the
- Create a
-
Execution:
- In your
mainmethod, create severalList<String>objects containing sample events (e.g., "LOGIN", "VIEW_ITEM", "ADD_TO_CART", "LOGIN", etc.). - Create and submit an
EventProcessortask for each list to yourExecutorService. - Properly shut down the executor service and wait for tasks to complete.
- Finally, print the contents of the
eventCountsmap to see the final tally. You'll see the aggregated counts from all concurrent tasks.
- In your
This exercise synthesizes what you've learned about thread pools, concurrent collections, and atomic operations into a single, practical example.
Conclusion
You have now reached the end of our module on Java Concurrency. By learning to use ConcurrentHashMap, you've added one of the most powerful and essential tools for concurrent programming to your arsenal.
Key Takeaways:
- Standard collections like
HashMapare not thread-safe and can lead to race conditions andConcurrentModificationException. Collections.synchronizedMapprovides a simple but low-performance solution by locking the entire map for every operation (coarse-grained locking).ConcurrentHashMapprovides superior performance and concurrency by using multiple locks for different parts of the map (fine-grained locking).- In a
ConcurrentHashMap, reads are non-blocking, and writes only lock the specific segment being modified, allowing for high throughput. - Atomic methods like
computeIfAbsentallow you to perform complex "check-then-act" operations safely and efficiently in a single step.
You have now built a solid foundation in core Java, from OOP principles to collections and concurrency. In our next module, we will pivot from the language itself to the ecosystem, beginning your journey into enterprise development with the Spring Framework. We'll start by exploring its fundamental principles: Inversion of Control (IoC) and Dependency Injection (DI), which form the bedrock of modern Java applications.