Skip to Content
Course content

87: Concurrent Collections: ConcurrentHashMap

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this exact bug pop up in production more times than I care to admit. A developer builds a feature that works perfectly in their local environment with a single user, but the second it hits a multi-threaded server environment, the data starts drifting or the application just crashes with a cryptic exception. Let's look at a scenario where we're trying to track page views for a small web server.

public class PageTracker {
    private final Map<String, Integer> pageViews = new HashMap<>();

    public void recordView(String page) {
        int currentViews = pageViews.getOrDefault(page, 0);
        pageViews.put(page, currentViews + 1);
    }

    public void printReport() {
        for (Map.Entry<String, Integer> entry : pageViews.entrySet()) {
            System.out.println(entry.getKey() + ": " + entry.getValue());
        }
    }
}

The Mystery of the Vanishing Page Hits

On the surface, recordView looks fine. But if you run this with ten threads hammering it simultaneously, you'll notice two things. First, your final counts will be lower than the actual number of calls—some increments just disappear. Second, if you call printReport while the other threads are still recording views, you'll likely get a ConcurrentModificationException. This happens because HashMap is not thread-safe; it doesn't expect the underlying structure to change while you're iterating over it.

Why the HashMap is Lying to You

The "vanishing hits" are caused by a race condition. The line pageViews.put(page, currentViews + 1) isn't a single atomic action. It's a "read-modify-write" sequence. Imagine Thread A and Thread B both read the current count for "/home" as 10. Both calculate 11. Thread A writes 11, then Thread B writes 11. We just had two views, but the count only went up by one. This is a classic synchronization failure.

You might be tempted to just wrap the HashMap in Collections.synchronizedMap(). I wouldn't recommend it for this. That approach locks the entire map for every single operation, which becomes a massive bottleneck as your traffic grows. It's like having a giant library where only one person is allowed inside at a time, regardless of which book they are looking for.

Making Updates Atomic with ConcurrentHashMap

This is where ConcurrentHashMap comes in. It doesn't lock the whole map; it uses a sophisticated striping mechanism (and CAS operations) to allow multiple threads to read and write to different parts of the map simultaneously. However, simply changing the type to ConcurrentHashMap won't fix the race condition in recordView. You're still doing a separate get and put.

To fix this, we use atomic methods like merge or compute. Here is how I would actually write this in a professional codebase:

public class PageTracker {
    // Use ConcurrentHashMap for thread-safe access
    private final Map<String, Integer> pageViews = new ConcurrentHashMap<>();

    public void recordView(String page) {
        // merge is atomic: it handles the "get, increment, put" in one go
        pageViews.merge(page, 1, Integer::sum);
    }

    public void printReport() {
        // ConcurrentHashMap's iterators are weakly consistent; 
        // they won't throw ConcurrentModificationException
        pageViews.forEach((page, count) -> {
            System.out.println(page + ": " + count);
        });
    }
}

The merge method is the secret sauce here. It says: "If the key page isn't there, put 1. If it is there, use the provided function (Integer::sum) to combine the old value with 1." Because ConcurrentHashMap guarantees this operation is atomic per key, no updates are lost, and we don't need to manually synchronize the whole method. Also, notice that forEach (and the entry set iterator) in a ConcurrentHashMap is designed to be used while the map is being modified, so those crashes disappear.




📋 Practical Task

Build a Thread-Safe User Session Cache

You are building a session management system where multiple request-handling threads update the "last activity" timestamp for users. A standard HashMap is causing crashes and data loss under load.

Your Task:

  • Create a class SessionCache that uses a ConcurrentHashMap to store userId (String) and lastActiveTimestamp (Long).
  • Implement a method updateActivity(String userId)` that updates the timestamp to System.currentTimeMillis().
  • Implement a method getSessionDuration(String userId, long currentTimestamp)` that returns the difference between the current timestamp and the stored timestamp. If the user isn't in the cache, return -1.
  • Ensure that the updateActivity method handles the insertion and update of the timestamp atomically.
  • Write a small main method that spawns 5 threads, each updating the same user ID 1,000 times, and verify that the program completes without throwing any ConcurrentModificationException while you simultaneously print the cache contents in a separate thread.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.