October 12, 2022

Java ConcurrentHashMap With Examples

ConcurrentHashMap in Java is a thread safe Map implementation which provides another alternative to be used in a multithreaded environment apart from HashTable or explicitly synchronizing HashMap. ConcurrentHashMap is part of the java.util.concurrent package.

How is ConcurrentHashMap a better option

Other thread safe implementations like HashTable or explicit synchronization of HashMap synchronize all the methods on a single lock and all the methods are synchronized, even if methods are for retrieving elements. That makes these options very slow-

  1. Since whole collection is locked so only a single thread can access it at a given time.
  2. Since all the methods are synchronized so read operations are also slow.

ConcurrentHashMap in Java tries to address these issues-

  1. By not locking the collection for retrieval operations like get(). Concurrent read operations are permitted only the write operations are synchronized.
  2. Even for write operations whole collection is not locked but only the part of the table where the elements has to be put as per the calculated hashcode.

Internal implementation of ConcurrentHashMap in Java

For storing its element ConcurrentHashMap internally uses an array named table of type Node.

transient volatile Node<K,V>[] table;

Here Node class represents the key-value entry and defined as a static class with in the ConcurrentHashMap implementation. Node class has fields for storing key and value and also next field for holding the reference to the next node.

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;
  final K key;
  volatile V val;
  volatile Node<K,V> next; 
  .....

Here note that in initial implementation of ConcurrentHashMap in Java 5 there was array Segment which was used and that provided concurrency level of 16 by default i.e. 16 threads could access 16 elements stored in different indexes of the array because each segment could be locked independently. But Java 8 onward internal implementation of ConcurrentHashMap is modified and now array named table is used and it mainly uses Compare-And-Swap (CAS) operations for concurrency during write operations.

Each array index in the table can still be independently locked by synchronizing the first node of that particular bucket.

internal implementation ConcurrentHashMap in Java

Java ConcurrentHashMap constructors

  • ConcurrentHashMap()- Creates a new, empty map with the default initial table size (16).
  • ConcurrentHashMap(int initialCapacity)- Creates a new, empty map with an initial table size accommodating the specified number of elements without the need to dynamically resize.
  • ConcurrentHashMap(int initialCapacity, float loadFactor)- Creates a new, empty map with an initial table size based on the given number of elements (initialCapacity) and initial table density (loadFactor).
  • ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel)- Creates a new, empty map with an initial table size based on the given number of elements (initialCapacity), table density (loadFactor), and number of concurrently updating threads (concurrencyLevel).
  • ConcurrentHashMap(Map<? extends K,? extends V> m)- Creates a new map with the same mappings as the given map.

Java example creating a ConcurrentHashMap

In this example a ConcurrentHashMap is created and (key, value) pair added to it which are later displayed.

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

public class CHMExample {
  public static void main(String[] args) {
    // Creating ConcurrentHashMap
    Map<String, String> carMap = new ConcurrentHashMap<String, String>();
    // Storing elements
    carMap.put("1", "Audi");
    carMap.put("2", "BMW");
    carMap.put("3", "Jaguar");
    carMap.put("4", "Mini Cooper");
    
    for (Map.Entry<String, String> entry : carMap.entrySet()) {
      System.out.println("key- " + entry.getKey() + " value- " + entry.getValue());
    }
  }
}
Output
key- 1 value- Audi
key- 2 value- BMW
key- 3 value- Jaguar
key- 4 value- Mini Cooper

Null not allowed in Java ConcurrentHashMap

ConcurrentHashMap does not allow insertion of null as either key or value. So both of the following statements result in NullPointerException.

carMap.put(null, "Audi");
Exception in thread "main" java.lang.NullPointerException
carMap.put("1", null);
Exception in thread "main" java.lang.NullPointerException

ConcurrentHashMap in Java is thread safe

ConcurrentHashMap in Java is safe to use in multi-threaded environment. Let’s see an example where we’ll first try to insert 400 elements in a HashMap (which is not thread safe) using 4 threads with each thread inserting 100 elements. Expected size of the HashMap is 400 after the execution.

import java.util.HashMap;
import java.util.Map;

public class MapSynchro implements Runnable{
  private Map<String, String> testMap;
  public MapSynchro(Map<String, String> testMap){
    this.testMap = testMap;
  }

  public static void main(String[] args) {
    Map<String, String> testMap = new HashMap<String, String>();
    /// 4 threads
    Thread t1 = new Thread(new MapSynchro(testMap));
    Thread t2 = new Thread(new MapSynchro(testMap));
    Thread t3 = new Thread(new MapSynchro(testMap));
    Thread t4 = new Thread(new MapSynchro(testMap));
        
    t1.start();
    t2.start();
    t3.start();
    t4.start();
        
    try {
      t1.join();
      t2.join();
      t3.join();
      t4.join();
    } catch (InterruptedException e) {    
      e.printStackTrace();
    }
    System.out.println("Size of Map is " + testMap.size());
  }
  @Override
  public void run() {
    System.out.println("in run method" + Thread.currentThread().getName());
    String str = Thread.currentThread().getName();
    for(int i = 0; i < 100; i++){
      // adding thread name to make element unique
      testMap.put(str+i, str+i);
      try {
        // delay to verify thread interference
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}
Output
in run methodThread-3
in run methodThread-0
in run methodThread-1
in run methodThread-2
Size of Map is 394

As you can see size is 394 in one run because of the thread interference.

Using ConcurrentHashMap eliminates such inconsistencies. You just need to change the following line in the code.

Map<String, String> testMap = new ConcurrentHashMap<String, String>();

Now the size is always 400.

Java ConcurretHashMap returns a fail-safe iterator

Iterator returned by ConcurrentHashMap is fail-safe and does not throw ConcurrentModificationException if the map is structurally modified at any time after the iterator is created.

public class FailSafeDemo {
  public static void main(String[] args) {
    Map<String, String> carMap = new ConcurrentHashMap<String, String>();
    carMap.put("1", "Audi");
    carMap.put("2", "BMW");
    carMap.put("3", "Jaguar");
    carMap.put("4", "Mini Cooper");
    // iterating map
    Iterator<Map.Entry<String, String>> itr = carMap.entrySet().iterator();
    while(itr.hasNext()) {
      Map.Entry<String, String&gt; entry = itr.next();
      System.out.println("Key is " + entry.getKey() + " Value is " + entry.getValue());
      carMap.put("5", "Mercedes");
    }
    System.out.println("Size- " + carMap.size());
  }
}
Output
Key is 1 Value is Audi
Key is 2 Value is BMW
Key is 3 Value is Jaguar
Key is 4 Value is Mini Cooper
Key is 5 Value is Mercedes
Size- 5

In the code, while iterating the ConcurrentHashMap a new element is added to it that does not result in ConcurrentModificationException being thrown.

Atomic operations in ConcurrentHashMap

Even though Java ConcurrentHashMap is thread safe but atomic operations may still give inconsistent result in multi-threaded environment. For example a scenario as follows.

Integer oldVal = CHMMap.get(key); 
Integer newVal = (oldVal== null) ? 1 : oldVal + 1;
// newValue stored by another thread
CHMMap.put(key, newValue);

Here if an executing thread is preemepted by another thread after the execution of this line- Integer newVal = (oldVal== null) ? 1 : oldVal + 1; then the value put back in the ConcurrentHashMap may not be correct. In such scenarios it is better to use atomic operations. Some of the atomic operations in the ConcurrentHashMap class are-

  • putIfAbsent(K key, V value)- If the specified key is not already associated with a value, associates it with the given value.
  • remove(Object key, Object value)- Removes the entry for a key only if currently mapped to a given value.
  • computeIfAbsent(K key, Function<? super K,? extends V> mappingFunction)- If the specified key is not already associated with a value, attempts to compute its value using the given mapping function and enters it into this map unless null.
  • computeIfPresent(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)- If the value for the specified key is present, attempts to compute a new mapping given the key and its current mapped value.
  • compute(K key, BiFunction<? super K,? super V,? extends V> remappingFunction)- Attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current mapping).
  • merge(K key, V value, BiFunction<? super V,? super V,? extends V> remappingFunction)- If the specified key is not already associated with a (non-null) value, associates it with the given value.
Using the atomic operation compute(), scenario as listed above can be written as follows-
CHMMap.compute(key, (k,v)-> v == null ? 1 : v + 1);

Advantages and disadvantages of ConcurrentHashMap

  1. ConcurrentHashMap in Java performs better if there are more reads than writes in a multi-threaded environment as the concurrent read operations are permitted. Since retrieval operations are non-blocking, so may overlap with update operations (including put and remove). Thus the concurrent retrievals may or may not reflect insertion or removal of some entries.
  2. If there are more writes and updates in the ConcurrentHashMap and HashCode implementation is not proper then many elements may have the same hashcode. In that scenario most of the threads will need to access the same table index where the elements with the same hashcode are to be stored resulting in degraded performance.
  3. Iterators in ConcurrentHashMap are designed to be used by only one thread at a time.

That's all for the topic Java ConcurrentHashMap With Examples. If something is missing or you have something to share about the topic please write a comment.


You may also like

No comments:

Post a Comment