Java > Java Collections Framework > List, Set, and Map Interfaces > Concurrent Collections

ConcurrentHashMap Example

This example demonstrates the use of `ConcurrentHashMap` for thread-safe operations on a map. It showcases how multiple threads can concurrently read and write to a map without data corruption or race conditions. The `ConcurrentHashMap` is designed for high concurrency and provides better performance compared to synchronizing access to a regular `HashMap`.

Core Concepts

The `ConcurrentHashMap` is part of the `java.util.concurrent` package and implements the `ConcurrentMap` interface. It achieves thread safety through a combination of techniques including segmentation (dividing the map into multiple segments), lock striping (each segment has its own lock), and copy-on-write strategies for certain operations. This allows multiple threads to access different parts of the map concurrently. Unlike using `Collections.synchronizedMap(new HashMap())`, `ConcurrentHashMap` doesn't lock the entire map for every operation, resulting in improved performance under heavy load.

Code Snippet: ConcurrentHashMap Usage

This code creates a `ConcurrentHashMap` and uses an `ExecutorService` to simulate multiple threads concurrently accessing and modifying the map. The `compute` method is used to atomically update the values associated with keys. The third task demonstrates that reads are non-blocking, allowing the map's size to be accessed concurrently while other threads are writing to it. The `ExecutorService` is shut down gracefully after all tasks are submitted, and `awaitTermination` ensures that all threads complete before the program exits.

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

public class ConcurrentHashMapExample {

    public static void main(String[] args) throws InterruptedException {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

        ExecutorService executor = Executors.newFixedThreadPool(3);

        // Task 1: Increment counts for keys A and B
        executor.submit(() -> {
            for (int i = 0; i < 1000; i++) {
                map.compute("A", (key, val) -> (val == null) ? 1 : val + 1);
                map.compute("B", (key, val) -> (val == null) ? 1 : val + 1);
            }
        });

        // Task 2: Increment counts for keys C and D
        executor.submit(() -> {
            for (int i = 0; i < 1000; i++) {
                map.compute("C", (key, val) -> (val == null) ? 1 : val + 1);
                map.compute("D", (key, val) -> (val == null) ? 1 : val + 1);
            }
        });

        // Task 3: Read and print the size of the map periodically
        executor.submit(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(200);
                    System.out.println("Map size: " + map.size());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);

        System.out.println("Final Map Size: " + map.size());
        System.out.println("Map Contents: " + map);
    }
}

Explanation of Key Methods

  • `ConcurrentHashMap`: The declaration of a `ConcurrentHashMap` with key type `K` and value type `V`.
  • `compute(K key, BiFunction remappingFunction)`: Atomically computes a new value for the given key. The `BiFunction` receives the key and the current value (if any) and returns the new value to be associated with the key. If the function returns `null`, the key is removed from the map. This provides a thread-safe way to update values based on their current state.

Real-Life Use Case

A common use case for `ConcurrentHashMap` is in caching scenarios where multiple threads need to access and update cached data concurrently. For example, in a web application, you might use a `ConcurrentHashMap` to store session data or frequently accessed database queries. Another scenario is in distributed systems where multiple nodes need to share state information.

Best Practices

  • Choose appropriate initial capacity and load factor: The initial capacity and load factor can impact the performance of the `ConcurrentHashMap`. A larger initial capacity can reduce the need for resizing, while the load factor determines when the map will be resized.
  • Use immutable keys and values: Using immutable objects as keys and values can improve the thread safety of the map and prevent unintended modifications.
  • Consider the trade-offs between concurrency and consistency: `ConcurrentHashMap` provides a high level of concurrency, but it does not guarantee strict consistency. In some cases, you may need to use a more strongly consistent data structure if data integrity is paramount.

Interview Tip

When asked about concurrent collections, be prepared to discuss the differences between `ConcurrentHashMap` and `Collections.synchronizedMap(new HashMap())`. Highlight the performance advantages of `ConcurrentHashMap` due to its fine-grained locking mechanism. Also, mention the atomic operations provided by `ConcurrentHashMap`, such as `compute` and `putIfAbsent`.

When to Use `ConcurrentHashMap`

Use `ConcurrentHashMap` when you need a thread-safe map that provides high concurrency and good performance. It is suitable for scenarios where multiple threads will be accessing and modifying the map concurrently, and where strict consistency is not a requirement.

Memory Footprint

The memory footprint of `ConcurrentHashMap` can be higher than that of a regular `HashMap` due to the additional overhead associated with its internal data structures and locking mechanisms. However, the improved concurrency often outweighs the increased memory usage in many real-world applications.

Alternatives

  • `Collections.synchronizedMap(new HashMap())`: Provides thread safety by wrapping a regular `HashMap` with synchronization. However, it uses a single lock for the entire map, which can lead to contention and reduced performance under high load.
  • `Hashtable`: A legacy class that is thread-safe but uses a single lock for the entire map, similar to `Collections.synchronizedMap`.
  • `CopyOnWriteArrayList`: For thread-safe list implementations where reads are more frequent than writes. Not a direct alternative to maps, but relevant when considering concurrent collections.

Pros

  • High concurrency and good performance
  • Thread-safe without requiring external synchronization
  • Atomic operations for updating values

Cons

  • Higher memory footprint compared to `HashMap`
  • Does not guarantee strict consistency

FAQ

  • What is the difference between ConcurrentHashMap and Collections.synchronizedMap?

    ConcurrentHashMap provides a higher level of concurrency because it uses fine-grained locking (segment locking) instead of locking the entire map like Collections.synchronizedMap. This allows multiple threads to access different parts of the map concurrently, improving performance under heavy load.
  • Does ConcurrentHashMap guarantee strict consistency?

    No, ConcurrentHashMap does not guarantee strict consistency. Updates made by one thread may not be immediately visible to other threads. However, it provides a high level of concurrency while maintaining reasonable consistency.
  • How do I iterate over a ConcurrentHashMap in a thread-safe manner?

    Iterators returned by ConcurrentHashMap are weakly consistent, meaning they reflect the state of the map at some point at or since the creation of the iterator. They are also fail-safe, meaning they don't throw ConcurrentModificationException if the map is modified while iterating.