Sunday, November 5, 2023

5 Ways to Update Values in ConcurrentHashMap in Java 8 - Example Tutorial

In last article, I have showed you how to update value in HashMap and today we'll learn about how to update value in a ConcurrentHashMap. Some of you may question, can we use the same technique we used to update a value in HashMap here? well, you can but you need to keep in mind the difference between an HashMap and a ConcurrentHashMap. The HashMap is only meant to be updated by one thread at a time, hence you don't need to pay any attention to thread-safety or concurrency, but ConcurrentHashMap can be updated by multiple threads at the same time, hence you need to pay special attention. 

It's possible that two threads are trying to update the same value at the same time, which means if you are not careful, both thread will see the same old value and update the counter with same value, potentially missing a count. In short, concurrent hash map must be updated atomically to avoid any logical error. 

When ConcurrentHashMap was first added, there were not any method to atomically update the value, which means you need to write code to handle both update and atomicity, Thankfully this has been rectified in Java 8, with new classes like LongAddr and new methods on ConcurrentHashMap e.g. merge() and compute()

In this article, I'll show you how to update a value in ConcurrentHashMap by 5 different ways, you can choose any way you like, but you should be consistent in your coding. 

In Java 8, you can update a ConcurrentHashMap value by following ways:
  • 1) old way to update value in do-while loop using replace() method
  • 2) atomic update using AtomicLong
  • 3) atomic update using LongAddr of Java 8
  • 4) updating value in ConcurrentHashMap using compute() function
  • 5) Using merge to update value atomically in ConcurrentHashMap

But, before we start, let's first revisit, the most common, yet wrong way to update a value in ConcurrentHashMap in Java. 

The wrong way to update value in ConcurrentHashMap

Suppose you have a ConcurrentHashMap, which hold population by cities for United states and many threads are supposed to update population for same as well different cities. How will we write code to handle such updates? 

Here is the first attempt, which has some issue, see if you can find out

ConcurrentHashMap<String, Long> populationByCities 
= new ConcurrentHashMap<>(); 
Long currentValue = populationByCities.get("New York");
Long newValue = currentValue == null ? 1 : currentValue + 1;
populationByCities.put("New York",newValue); 

If you look at this code, it is the same technique, we used to update value in HashMap but unfortunately this will not work in case of ConcurrentHashMap because it's possible that another thread is updating population for same city at the same time.

In which case, both will see the same old value and try to increment it by 1, potentially losing 1 count, which can add into a much higher number with time, potentially leading to incorrect record. 

So the question is how do you fix that? Well, there are many correct ways to update a value in ConcurrentHashMap in Java, particularly from JDK 8, which you will see in this article. 

5 Ways to update the value of ConcurrentHashMap atomically in Java



5 Ways to update the value of ConcurrentHashMap atomically in Java

Here is are 5 ways to safely update a value in a ConcurrentHashMap in Java:

1. Updating value in ConcurrentHashMap using replace()

The ConcurrentHashMap class provides a replace(key, oldValue, newValue) method, which replaces the entry for a key only if the old value is equal to the currently mapped value. This is equivalent to 

if (map.containsKey(key) && Objects.equals(map.get(key), oldValue)) {
    map.put(key, newValue);
    return true;
} else
    return false;
}

except that the action is performed atomically. You can leverage the fact that it return false, if currently mapped value is not equal to the given old value, which means another thread has updated the map while you are computing value. So, you keep try until you succeed. 

This may not seem the ideal way but it works.

So, you can rewrite the first example, as shown below to ensure that a value is correctly updated into ConcurrentHashMap:

do {
    currentValue = populationByCities.get("New York");
    newValue = currentValue == null ? 1 : currentValue + 1;

} while (!populationByCities.replace("New York", currentValue, newValue));

This code will keep try until until it ensures that no thread has updated the same value in the background while it is trying. 

The best part of this code is that it will work in every Java version from Java SE 5 to Java SE 8 and one of the correct way to update values of a concurrent hashmap until Java SE 6,


2. Updating value in ConcurrentHashMap using AtomicInteger

A simpler way to update numeric counters e.g. integer or long in ConcurrentHashMap is by using AtomicInteger and AtomicLong, for example instead of using Long values, you can chose to use an AtomicLong value and then atomically update its value using incrementAndGet()

Here is a simplified version of above code using AtomicLong values:

ConcurrentHashMap<String, AtomicLong> map = new ConcurrentHashMap<>(); 
AtomicLong existingValue = map.get("New York");
AtomicLong newValue = existingValue == null ? 
           new AtomicLong(1) : existingValue.incrementAndGet();
map.put("New York", newValue);

This is safe, because even if another thread gets the value and update it while you are calling the put() method, it's the same object which is update and it is always updated atomically, which means you will not lose any update or counts. 

You also don't need to use do-while loop and replace() method because we are alway updating same object, AtomicLong. 

Btw, you can further simplify the code by using putIfAbsent() method which only put a value, if there is no value exists. Otherwise, it return the old value, which means you can directly update the return value of putifAbsent() method as shown in following example:

map.putIfAbsent("Tokyo", new AtomicLong()).incrementAndGet();

This one liner is the best way to update a value in ConcurrentHashMap if you are not running in Java SE 8. 


3. ConcurrentHashMap Value Update using LongAdder

The LongAdder is another useful class which is added in JDK 8 to efficiently update a long value from multiple threads without any side effect of concurrency. So, instead of using Long or AtomicLong, you can also use LongAdder, if a counter is means to be updated by multiple threads. 

Here is a code example of how to update a value in ConcurrentHashMAp using LongAdder in Java SE 8:

ConcurrentHashMap<String, LongAdder> aMap = new ConcurrentHashMap<>();
aMap.putIfAbsent("Mumbai", new LongAdder()).increment();


The increment() method of LongAdder atomically updates the counter. We have again taken advantage of putIfAbsent() method to perform the update in single line, instead of writing a couple of lines to get the value and then updated it. 

You can also read a good book on Java SE 8, e.g. Java 8 in Action to learn more about benefits of using LongAdder in multithreaded Java application. 


4. The Java 8 way to update value in ConcurrentHashMap using compute()

The true Java way to write code take advantage of all features of Java 8 e.g. new methods, new language enhancements e.g. lambda expression and the new way of writing functional code. The JDK 8 provides a compute() method to atomically update any value of ConcurrentHashMap in Java 8. 

The compute() method is called with a key and a function to compute the new value. That function receives a key and existing value, or null if there is no value for that key and it computes new value atomically. 

Here is the code example to update value using compute() function of Java 8:

ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
map.compute("New York",
(key, value) -> value == null ? 1 : value + 1);

As per Java documentation, the compute() function attempts to compute a mapping for the specified key and its current mapped value (or null if there is no current mapping). The entire method invocation is performed atomically. 

Some attempted update operations on this map by other threads may be blocked while computation is in progress, so the computation should be short and simple, and must not attempt to update any other mappings of this Map. 

It also have variants e.g. computeIfAbsent() and computeIfPresent(), which only compute a new value when there isn't yet one or an old is already present. You can see Java SE 8 for Really Impatient to learn more about their usage. 


5. Using Java 8 merge() function to update value in ConcurrentHashMap

The JDK 8 provided another method called merge() which can be used to update a value in ConcurrentHashMap safely and atomically. The merge method also accept initial value, which makes it more convenient to put a counter when there is no count already e.g. adding mapping for a new city. You can use this function to merge existing value with existing value. 

The function, or lambda expression you supply is used to compute new value, as shown in following example:

ConcurrentHashMap<String, Long> map = new ConcurrentHashMap<>();
map.merge("Houston", 1L, (current, next) -> current + next);

Here 1L is the initial value, we have put L sufix to make it long. You can further simplify above code using method reference as shown below:

map.merge("Chicago", 1L, Long::sum);

The Long::sum method is newly added in Long class to facilitate reduction using stream. 

Though compute() and merge() looks similar, there is a key difference between them, unlike compute(), the function you supply to produce new value doesn't process key. 




Java Program to update value in ConcurrentHashMap

Here is our complete java program to update a value atomically in ConcurrentHashMap. This includes all five ways we have discussed in this tutorial e.g. updating values using replace, compute, merge, AtomicLong, and LongAdder in Java 8. 

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

/*
 * Java Program to update value for a given key in ConcurrentHashMap
 * using Java 8. 
 */
public class Demo {


    public static void main(String[] args) throws Exception {

        // a ConcurrentHashMAp which holds population by cities
        ConcurrentHashMap < String, Long > populationByCities 
                 = new ConcurrentHashMap < > ();

        // how do you atomically update any value in ConcurrentHashMap 
        Long currentValue = populationByCities.get("New York");
        Long newValue = currentValue == null ? 1 : currentValue + 1;
        populationByCities.put("New York", newValue); 
         // Error - this may not replace old value. 

        // Right way to update any value in ConcurrentHashMap

        do {
            currentValue = populationByCities.get("New York");
            newValue = currentValue == null ? 1 : currentValue + 1;

        } while (!populationByCities.replace("New York", currentValue, newValue));


        // another way to update concurrent hash map aromatically is by 
        // using atomic integer

        ConcurrentHashMap < String, AtomicLong > map = new ConcurrentHashMap < > ();

        // updating a value atomically in concurrent map
        map.putIfAbsent("Tokyo", new AtomicLong()).incrementAndGet();


        // in Java 8 - you can also use LongAddr to update counter in ConcurrentHashMap
        ConcurrentHashMap < String, LongAdder > aMap = new ConcurrentHashMap < > ();
        aMap.putIfAbsent("Mumbai", new LongAdder()).increment();


        // in JDK 8 - you can also use compute() and lambda expression to
        // atomically update a value or mapping in ConcurrentHashMap
        populationByCities.compute("New York",
            (key, value) - > value == null ? 1 : value + 1);

        // JDK 8 has another choice, by using merge() method 
        populationByCities.merge("Houston", 1 L, (current, next) - > current + next);

        // or, by using method reference and reduce methods
        populationByCities.merge("Chicago", 1 L, Long::sum);

    }

}


That's all about how to safely and atomically update a value for given key in ConcurrentHashMap. We have seen how to do that in both Java 8 and previous version. The older ways e.g. using replace() function works but it is not particularly convenient, but you can always simply the update logic by using AtomicLong instead of Long. 

From Java 8 onwards, you should use either compute() or merge() to update new values, and if you have to maintain a counter prefer LongAdder, which is especially designed to be updated from multiple threads.

Other Java HashMap tutorials you may like to explore:
  • How does get() method of HashMap work in Java? (answer)
  • Difference between ArrayList and HashMap? (difference)
  • HashMap vs LinkedHashMap in Java? (answer)
  • ArrayList vs HashMap in Java? (answer)
  • Difference between ConcurrentHashMap and HashMap in Java? (answer)
  • How to sort the HashMap on keys and values in Java? (solution)
  • 3 ways to loop over a Map in Java? (example)
  • HashSet vs HashMap in Java? (answer)
  • How HashSet internally works in Java? (answer)
  • How ConcurrentHashMap internally works in Java? (answer)
  • HashMap vs ConcurrentHashMap in Java? (answer)
  • The best way to iterate over HashMap in Java? (answer)
  • Difference between HashMap vs Hashtable in Java? (answer)
  • How to convert Map to List in Java? (solution)

Thanks for reading this article so far. If you like this example then please share with your friends and colleagues. If you have any doubts or questions feel free to ask. 

And now one question for you, what is difference between compute() and merge() of Map class in Java? 

No comments :

Post a Comment