How do I safely share data between threads with ConcurrentHashMap?

When working with a multithreaded application, ConcurrentHashMap is a great choice for safely sharing data between threads. It is a thread-safe version of a HashMap that provides high concurrency for both retrieval and updates. Here are some guidelines to safely use a ConcurrentHashMap in a multithreaded environment:


1. Use Thread-Safe Access Operations

ConcurrentHashMap ensures that operations like put(), get(), remove(), containsKey() are thread-safe. Unlike HashMap, you can safely use these methods concurrently across multiple threads without additional synchronization.

package org.kodejava.util.concurrent;

import java.util.concurrent.ConcurrentHashMap;

public class ExampleConcurrentHashMap {
    public static void main(String[] args) {
        ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
        map.put("key1", 1);

        // Reading and updating the map from multiple threads
        Runnable task = () -> {
            System.out.println(Thread.currentThread().getName());
            Integer value = map.get("key1");
            if (value != null) {
                map.put("key1", value + 1);
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
    }
}

This code works safely across threads because the put() and get() operations are thread-safe.


2. Avoid Compound Operations

While individual operations like put() and get() are thread-safe, compound operations (operations that consist of multiple actions, e.g., check-then-act) are not atomic by default. For example, the following code might fail in a multithreaded scenario:

if (!map.containsKey("key")) {  // Thread 1 might pass this check
    map.put("key", 42);         // Thread 2 might also pass this check before Thread 1 puts the value
}

To perform compound operations atomically, use methods provided by ConcurrentHashMap, such as putIfAbsent(), compute(), or merge().

Example: Use putIfAbsent

map.putIfAbsent("key", 42); // Ensures that "key" is inserted only if it isn't already present

Example: Use compute

map.compute("key", (k, v) -> (v == null) ? 1 : v + 1);
// Safely updates the value of "key" atomically

Example: Use merge

map.merge("key", 1, Integer::sum);
// Combines a new value with the existing value of "key" in a thread-safe manner

3. Leverage Concurrent Iteration

ConcurrentHashMap allows thread-safe iteration over its entries using iterators. However, note that the iterator reflects the state of the map at the moment it was created. Any changes made to the map by other threads after the iterator creation will not throw ConcurrentModificationException, but they may or may not be seen during iteration.

Safe Iteration Example

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("key1", 1);
map.put("key2", 2);

map.forEach((key, value) -> {
    System.out.println(key + ": " + value);
});

Iterating and updating simultaneously can still be done safely through operations like compute() or computeIfPresent() within the iteration.


4. Understand Default Concurrency Level

ConcurrentHashMap partitions the map into segments internally to reduce contention among threads. You can adjust the level of concurrency (number of segments) by specifying it during construction, but the default value is sufficient for most use cases.

Custom Concurrency Level Example:

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(16, 0.75f, 32);
// 32 is the concurrency level (number of threads allowed to modify without contention)

5. Use Bulk Operations for Performance

ConcurrentHashMap includes bulk operations like forEach(), reduce(), and search(). These operations are implemented to efficiently work with large volumes of data in a concurrent environment.

Example: Use forEach

map.forEach(1, (key, value) -> {
    System.out.println(key + ": " + value);
});
// The first parameter is parallelismThreshold (minimum size to make it parallelizable)

Example: Use reduce

Integer sum = map.reduceValues(1, Integer::sum);
System.out.println("Sum of all values: " + sum);

6. Avoid Manual Synchronization

Avoid adding explicit locks like synchronized or ReentrantLock with ConcurrentHashMap, as this can lead to deadlocks or significantly hinder performance. Instead, rely on the built-in atomic methods provided by the class.


7. Be Aware of Null Restrictions

Unlike HashMap, ConcurrentHashMap does not support null keys or null values. If you try to use null, it will throw a NullPointerException. Use valid non-null keys and values at all times.


Conclusion

ConcurrentHashMap is a powerful and flexible tool for managing shared data across multiple threads. To use it safely and efficiently:

  1. Use atomic methods like putIfAbsent, compute, or merge for compound operations.
  2. Avoid manual synchronization.
  3. Leverage bulk operations for large datasets.
  4. Handle data consistently without assuming atomicity for compound actions unless explicitly supported by the API.

By following these guidelines, you can minimize race conditions and improve the safety and performance of your multithreaded application.

Wayan

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.