Java ConcurrentHashMap Tutorial with Examples

In this tutorial, we will learn everything about the Java ConcurrentHashMap class with examples. We will also see the real-world example of ConcurrentHashMap.

The ConcurrentHashMap in Java is a part of the Java Collection Framework which provides a concurrent version of the standard HashMap. The main advantage of ConcurrentHashMap over HashMap is that it allows for simultaneous reads and writes via multiple threads without external synchronization.

Key Points about Java ConcurrentHashMap

Thread Safety: Unlike HashMap, ConcurrentHashMap is thread-safe and can be used without external synchronization. 

Segmentation: Internally, it divides the map into segments. Each segment can be locked independently, allowing high concurrency. 

Null Restrictions: Unlike HashMap, neither keys nor values can be null. Attempting to insert a null key or value will throw a NullPointerException. 

Performance: It provides better performance than Hashtable and synchronized HashMap due to the granular locking mechanism. 

Concurrent Retrievals: Retrievals (get operations) are generally lock-free and do not affect the concurrency level. 

Methods for Concurrent Use: It provides methods specifically designed for concurrent use like putIfAbsent, remove, and replace. 

Adjustable Concurrency: The constructor allows you to specify the concurrency level, which hints at the number of concurrently updating threads the map should accommodate. 

Fail-Safe Iterators: Its iterators are designed to be fail-safe, which means they won't throw a ConcurrentModificationException if the map is modified while being iterated. 

Enhanced java.util.stream: With Java 8 and above, ConcurrentHashMap was enhanced with aggregate operations based on the newly introduced Streams. 

Computational Operations: Methods like compute, computeIfAbsent, and computeIfPresent were added, which are useful for atomically updating the key-value pairs. 

Memory Consistency: Actions in a thread prior to placing an object into a ConcurrentHashMap as a key or value happen before actions subsequent to the access or removal of that object in another thread. 

Scalability: ConcurrentHashMap scales well with the number of threads. More threads can read and write without blocking each other, thanks to its segment-level locking mechanism.

Java ConcurrentHashMap - Creating and Adding Key-Value Pairs

Using ConcurrentHashMap, we can store key-value pairs in a thread-safe manner, ensuring safe concurrent access and updates.
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap
        ConcurrentHashMap<String, Integer> studentGrades = new ConcurrentHashMap<>();

        // Adding key-value pairs
        studentGrades.put("Amit", 85);
        studentGrades.put("Rohan", 90);
        studentGrades.put("Priya", 88);

        System.out.println("ConcurrentHashMap content: " + studentGrades);
    }
}

Output:

ConcurrentHashMap content: {Amit=85, Rohan=90, Priya=88}

Explanation:

1. ConcurrentHashMap: It's a part of the java.util.concurrent package. Unlike HashMap, it is thread-safe and allows multiple threads to perform retrieval operations. However, the default concurrency level, which defines the number of concurrently updating threads, is set to 16.

2. put: This method is used to add a key-value pair to the ConcurrentHashMap. It works similarly to the put method in a regular HashMap but is designed to be thread-safe.

Creating a ConcurrentHashMap from Other Maps

In essence, this example demonstrates how to easily convert a regular HashMap or any other Map implementation into a ConcurrentHashMap.
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapFromOtherMaps {
    public static void main(String[] args) {
        // Create a HashMap and populate it with some data
        Map<String, Integer> hashMap = new HashMap<>();
        hashMap.put("One", 1);
        hashMap.put("Two", 2);
        hashMap.put("Three", 3);

        // Create a ConcurrentHashMap from the HashMap
        ConcurrentHashMap<String, Integer> concurrentMap = new ConcurrentHashMap<>(hashMap);

        // Print the contents of the ConcurrentHashMap
        concurrentMap.forEach((key, value) -> System.out.println(key + " -> " + value));
    }
}

Output:

One -> 1
Two -> 2
Three -> 3

Explanation:

1. We first create a HashMap named hashMap and populate it with some key-value pairs.

2. We then use the constructor of ConcurrentHashMap that accepts a Map as its argument to create a ConcurrentHashMap named concurrentMap.

3. The forEach(BiConsumer<? super K, ? super V> action) method is used to iterate over the concurrentMap and print its contents.

Java ConcurrentHashMap - Accessing Keys and Values

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapAccess {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap and adding key-value pairs
        ConcurrentHashMap<String, Integer> studentGrades = new ConcurrentHashMap<>();
        studentGrades.put("Amit", 85);
        studentGrades.put("Rohan", 90);
        studentGrades.put("Priya", 88);

        // Accessing keys
        for (String student : studentGrades.keySet()) {
            System.out.println("Student name: " + student);
        }

        // Accessing values
        for (Integer grade : studentGrades.values()) {
            System.out.println("Grade: " + grade);
        }
    }
}

Output:

Student name: Amit
Student name: Rohan
Student name: Priya
Grade: 85
Grade: 90
Grade: 88

Explanation:

1. keySet(): This method retrieves a Set of all the keys contained in the ConcurrentHashMap. It's worth noting that the set is a view on the keys, meaning changes in the map are reflected in the set and vice-versa.

2. values(): This method returns a Collection of all the values stored in the ConcurrentHashMap. Similar to keySet(), changes in the map are mirrored in the returned collection.

The iteration over the keySet() and values() allows us to access individual keys and values in the ConcurrentHashMap. As the map is thread-safe, these operations can be safely executed even in concurrent environments.

Java ConcurrentHashMap - Removing Keys

By using the remove method, we can easily and safely remove key-value pairs from a ConcurrentHashMap even in concurrent environments.
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapRemove {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap and adding key-value pairs
        ConcurrentHashMap<String, Integer> studentGrades = new ConcurrentHashMap<>();
        studentGrades.put("Amit", 85);
        studentGrades.put("Rohan", 90);
        studentGrades.put("Priya", 88);

        System.out.println("Before Removal: " + studentGrades);

        // Removing a key-value pair using the key
        studentGrades.remove("Amit");

        System.out.println("After Removal of Amit: " + studentGrades);
    }
}

Output:

Before Removal: {Amit=85, Rohan=90, Priya=88}
After Removal of Amit: {Rohan=90, Priya=88}

Explanation:

remove(Object key): This method is utilized to remove the key-value pair associated with the specified key from the ConcurrentHashMap. If the key doesn't exist in the map, the method has no effect.

Java ConcurrentHashMap - Obtaining entrySet, keySet, and Values

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map.Entry;

public class ConcurrentHashMapSetsValues {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap and adding key-value pairs
        ConcurrentHashMap<String, Integer> studentGrades = new ConcurrentHashMap<>();
        studentGrades.put("Amit", 85);
        studentGrades.put("Rohan", 90);
        studentGrades.put("Priya", 88);

        // Obtaining and displaying entrySet
        for (Entry<String, Integer> entry : studentGrades.entrySet()) {
            System.out.println("Entry: " + entry.getKey() + " - " + entry.getValue());
        }

        // Obtaining and displaying keySet
        for (String key : studentGrades.keySet()) {
            System.out.println("Key: " + key);
        }

        // Obtaining and displaying values
        for (Integer value : studentGrades.values()) {
            System.out.println("Value: " + value);
        }
    }
}

Output:

Entry: Amit - 85
Entry: Rohan - 90
Entry: Priya - 88
Key: Amit
Key: Rohan
Key: Priya
Value: 85
Value: 90
Value: 88

Explanation:

1. entrySet(): This method retrieves a Set view of the mappings contained in the ConcurrentHashMap. Each element in the set is a map entry (Entry<K,V>).

2. keySet(): This method provides a Set view of the keys present in the ConcurrentHashMap.

3. values(): It offers a Collection view of the values stored in the ConcurrentHashMap.

All the views (entrySet, keySet, and values) reflect the changes made in the original map, ensuring consistency between them and the actual ConcurrentHashMap.

Iterating Over Java's ConcurrentHashMap in Different Ways

import java.util.concurrent.ConcurrentHashMap;
import java.util.Map.Entry;
import java.util.Iterator;

public class ConcurrentHashMapIteration {
    public static void main(String[] args) {
        // Creating a ConcurrentHashMap and adding key-value pairs
        ConcurrentHashMap<String, Integer> studentGrades = new ConcurrentHashMap<>();
        studentGrades.put("Amit", 85);
        studentGrades.put("Rohan", 90);
        studentGrades.put("Priya", 88);

        // 1. Using forEach and lambda expression
        System.out.println("Using forEach and lambda:");
        studentGrades.forEach((key, value) -> System.out.println(key + " -> " + value));

        // 2. Iterating using entrySet and for-each loop
        System.out.println("\nUsing entrySet:");
        for (Entry<String, Integer> entry : studentGrades.entrySet()) {
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }

        // 3. Iterating using keySet and for-each loop
        System.out.println("\nUsing keySet:");
        for (String key : studentGrades.keySet()) {
            System.out.println(key + " -> " + studentGrades.get(key));
        }

        // 4. Iterating using iterators
        System.out.println("\nUsing iterators:");
        Iterator<Entry<String, Integer>> iterator = studentGrades.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<String, Integer> entry = iterator.next();
            System.out.println(entry.getKey() + " -> " + entry.getValue());
        }
    }
}

Output:

Using forEach and lambda:
Amit -> 85
Rohan -> 90
Priya -> 88

Using entrySet:
Amit -> 85
Rohan -> 90
Priya -> 88

Using keySet:
Amit -> 85
Rohan -> 90
Priya -> 88

Using iterators:
Amit -> 85
Rohan -> 90
Priya -> 88

Explanation:

1. forEach(BiConsumer<? super K, ? super V> action): Executes the given action for each key-value pair in the map.

2. entrySet(): Provides a Set view of the mappings contained in the map. Can be used with a for-each loop to iterate over the entries.

3. keySet(): Supplies a Set view of the keys. Used with a for-each loop to iterate over keys and then fetch the corresponding values.

4. iterator(): Returns an iterator over the entry set of the map, which can be used to traverse the map's entries.

These are common ways to iterate over a ConcurrentHashMap, providing flexibility depending on the use case.

Using ConcurrentHashMap with User-Defined Objects: Student

The example demonstrates the use of a user-defined object (Student) as the key in a ConcurrentHashMap and how to effectively store and retrieve values using custom object keys.
import java.util.concurrent.ConcurrentHashMap;
import java.util.Objects;

class Student {
    private String name;
    private int rollNo;

    // Constructor
    public Student(String name, int rollNo) {
        this.name = name;
        this.rollNo = rollNo;
    }

    // Getters and setters omitted for brevity

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return rollNo == student.rollNo &&
               Objects.equals(name, student.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, rollNo);
    }

    @Override
    public String toString() {
        return "Student{" +
               "name='" + name + '\'' +
               ", rollNo=" + rollNo +
               '}';
    }
}

public class ConcurrentHashMapWithCustomObject {
    public static void main(String[] args) {
        // Create a ConcurrentHashMap with Student as the key and String as the value (e.g., grade of the student)
        ConcurrentHashMap<Student, String> studentGrades = new ConcurrentHashMap<>();

        Student amit = new Student("Amit", 101);
        Student rohan = new Student("Rohan", 102);

        // Add students to the map
        studentGrades.put(amit, "A");
        studentGrades.put(rohan, "B");

        // Print the students and their grades
        studentGrades.forEach((student, grade) -> System.out.println(student + " -> " + grade));
    }
}

Output:

Student{name='Amit', rollNo=101} -> A
Student{name='Rohan', rollNo=102} -> B

Explanation:

1. Student class: Represents a student with name and rollNo. For the Student class to work correctly as a key in ConcurrentHashMap, it needs to override equals() and hashCode() methods. These methods ensure that different instances of Student with the same properties are treated as equal when looking them up in the map.

2. studentGrades: This is a ConcurrentHashMap where a key is a Student object, and the value is a String representing the student's grade. We add two students, Amit and Rohan, and their respective grades to this map.

3. forEach(BiConsumer<? super K, ? super V> action): This method is used to iterate over the studentGrades map and print each student along with their grade.

Real-World Use Case: Website Visitor Counter Using ConcurrentHashMap

This example showcases how ConcurrentHashMap can be used in a real-world scenario to atomically count occurrences in a multithreaded environment (like tracking website visitors).

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;

public class WebsiteVisitorCounter {

    // ConcurrentHashMap to store website names as keys and their visitor count as values
    private static final ConcurrentHashMap<String, AtomicInteger> visitorCounter = new ConcurrentHashMap<>();

    public static void main(String[] args) {

        // Simulate visitors for different websites
        visitWebsite("example.com");
        visitWebsite("example.com");
        visitWebsite("example.org");
        visitWebsite("example.net");
        visitWebsite("example.net");
        visitWebsite("example.net");

        // Display the visitor counts for each website
        visitorCounter.forEach((website, count) -> {
            System.out.println(website + " has " + count + " visitors.");
        });
    }

    // Method to simulate a visit to a website
    public static void visitWebsite(String website) {
        visitorCounter.compute(website, (key, value) -> {
            if (value == null) {
                return new AtomicInteger(1);
            } else {
                value.incrementAndGet();
                return value;
            }
        });
    }
}

Output:

example.com has 2 visitors.
example.org has 1 visitors.
example.net has 3 visitors.

Explanation:

1. We use a ConcurrentHashMap named visitorCounter with website names (String) as the keys and their visitor counts (AtomicInteger) as values.

2. The visitWebsite method simulates a visitor to a website. It uses the compute method of ConcurrentHashMap to atomically compute each website's visitor count.

3. If the website has not been visited yet, an AtomicInteger with a value of 1 is created. If the website has been visited previously, the existing count is incremented.

4. The main method simulates website visits and then prints the visitor counts for each website.

Comments