DZone
Thanks for visiting DZone today,
Edit Profile
  • Manage Email Subscriptions
  • How to Post to DZone
  • Article Submission Guidelines
Sign Out View Profile
  • Post an Article
  • Manage My Drafts
Over 2 million developers have joined DZone.
Log In / Join
Please enter at least three characters to search
Refcards Trend Reports
Events Video Library
Refcards
Trend Reports

Events

View Events Video Library

Zones

Culture and Methodologies Agile Career Development Methodologies Team Management
Data Engineering AI/ML Big Data Data Databases IoT
Software Design and Architecture Cloud Architecture Containers Integration Microservices Performance Security
Coding Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks
Culture and Methodologies
Agile Career Development Methodologies Team Management
Data Engineering
AI/ML Big Data Data Databases IoT
Software Design and Architecture
Cloud Architecture Containers Integration Microservices Performance Security
Coding
Frameworks Java JavaScript Languages Tools
Testing, Deployment, and Maintenance
Deployment DevOps and CI/CD Maintenance Monitoring and Observability Testing, Tools, and Frameworks

Modernize your data layer. Learn how to design cloud-native database architectures to meet the evolving demands of AI and GenAI workkloads.

Secure your stack and shape the future! Help dev teams across the globe navigate their software supply chain security challenges.

Releasing software shouldn't be stressful or risky. Learn how to leverage progressive delivery techniques to ensure safer deployments.

Avoid machine learning mistakes and boost model performance! Discover key ML patterns, anti-patterns, data strategies, and more.

Related

  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Trending

  • Stateless vs Stateful Stream Processing With Kafka Streams and Apache Flink
  • Understanding and Mitigating IP Spoofing Attacks
  • Performance Optimization Techniques for Snowflake on AWS
  • Build an MCP Server Using Go to Connect AI Agents With Databases
  1. DZone
  2. Coding
  3. Java
  4. Java 8: ConcurrentHashMap Atomic Updates

Java 8: ConcurrentHashMap Atomic Updates

Here's a great look at what Java 8 brought to ConcurrentHashMaps, including close looks at the methods at your disposal and their performance impacts.

By 
Tony Tapper user avatar
Tony Tapper
·
Updated Apr. 23, 21 · Tutorial
Likes (16)
Comment
Save
Tweet
Share
74.7K Views

Join the DZone community and get the full member experience.

Join For Free

Whilst doing some refactoring on updates to ConcurrentHashMap values, I came across these great articles ...

  • https://dzone.com/articles/concurrenthashmap-isnt-always-enough?fromrel=true
  • https://dzone.com/articles/how-concurrenthashmap-works-internally-in-java?fromrel=true

... and was inspired to try to develop the theme a bit further.

Pre-Java 8, we had various ways to try to perform atomic operations on the values of Concurrent collections as described by Dima.

For example, a simple counter:

Java
 




x


 
1
// Incrementing a count of the occurrences of a currency symbol
2
// (In reality we would have used an atomic variable even pre Java 8)
3
ConcurrentHashMap <String Integer> map = new ConcurrentHashMap <>();
4
String key = "USD/JPY";
5
Double oldValue; Double newValue; double increment = 1.0;
6
do {
7
    oldValue = results.get(key);
8
    newValue = oldValue == null? increment: oldValue + increment;
9
} while (!results.replace(key, oldValue, newValue));



Improved Methods in Java 8

  • computeIfAbsent(): If the value is threadsafe and can be safely updated outside the method, or you intend to synchronize on the value whilst updating it, or if you just want to be certain of getting a new or existing value without having to check for null.
  • compute(): If the value is not threadsafeand must be updated inside the method with a remapping function to ensure the entire operation is atomic. This gives you the most control over the computation, but also the responsibility to handle the possibility that there is no existing value inside your remapping function.
  • merge(): Like compute(), you provide a remapping function to be performed on the existing value — if any. You also supply an initial value to be used if there is no existing value. This is a convenience because, with compute(), you have to, in the remapping function, handle the possibility of there being no existing value. Here, though, you do not have access to the key in the remapping function, unlike with compute().

The Method Signatures

Java
 




xxxxxxxxxx
1
17


 
1
public V putIfAbsent(K key, V value)
2

          
3
public boolean replace(K key, V oldValue, V newValue)
4

          
5
// The argument to the mapping function is the key and the result is the 
6
// existing value (if found) or a new value that has been computed
7
public V computeIfAbsent(K key, Function<? super K, ? extends V> mappingFunction)
8

          
9
// The arguments to the remapping bifunction are the key and the existing value 
10
// (if found) and the result is a value that has been computed
11
public V compute(K key, BiFunction<? super K, ? super V, ? extends V> remappingFunction)
12

          
13
// The arguments to the remapping bifunction are the existing value (if found)
14
// and the initial value which was passed to the method in position 2 and the 
15
// result is a value that has been computed, possibly by combining them
16
public V merge(K key, V value,
17
               BiFunction<? super V,? super V,? extends V> remappingFunction)



Unlike the existing methods putIfAbsent() and replace(), instead of taking objects for the values, the arguments represent a Function or BiFunction, respectively, which can be implemented by a lambda or a method reference or even an inner class. This could be useful, as it means you don't have to instantiate an object that may well not be needed. You pass in some code that will only be executed if needed.

Atomicity

All three methods (plus computeIfPresent) are guaranteed to be atomic in ConcurrentHashMap. The way that works is that the method synchronizes on the new or existing Entry in the HashTable whilst executing.

It's important to notice that with computeIfAbsent(), a compound operation is not necessarily atomic because updates are performed outside of the method.

I try to think of them like, say, 'volatile' and AtomicInteger. Volatile guarantees visibility, but not atomicity. You won't get data races but could still get a race condition. If atomicity is required, then you use AtomicInteger, etc. to avoid race conditions

Another consideration is — are the values truly independent, or do they depend on an existing value or an external factor like 'order of arrival'?

This warning comes in the Oracle documentation:

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.

Examples

computeIfAbsent() takes a key and a mapping function and returns a value:

Java
 




xxxxxxxxxx
1


 
1
// This operation is atomic because updates on DoubleAdder are atomic 
2
private final Map<Integer, DoubleAdder> doubleAdderResults = new ConcurrentHashMap<>();
3
doubleAdderResults.computeIfAbsent(1, (k) -> new DoubleAdder()).add(1.0);
4

          
5
// This is not threadsafe or atomic
6
// The update of the mutable non-threadsafe value takes place outside the computeIfAbsent() method
7
pointResults.computeIfAbsent(mapKeyZero, (k) -> new MutablePoint(0, 0)).move(10, 40);



computeIfAbsent() is simplest to use.

compute() takes a key and a value and a remapping function and returns a value:

Java
 




xxxxxxxxxx
1
11


 
1
private final Map<Integer, MutablePoint> pointResults = new ConcurrentHashMap<>();
2

          
3
// Here, updates to the mutable value object are performed inside the compute() 
4
// method which, itself, synchronizes on the Entry, internally
5
for (int i : data) {
6
    pointResults.compute( i, (key, value) -> {
7
        value = (value == null? new MutablePoint(0, 0): value);
8
        value.move(5, 20);
9
        return value;
10
    });
11
}



compute() is more flexible because you have access to the key and the existing value in the remapping function and can update the existing value — or create a new one — inside the method atomically.

merge() takes a key and an initialize value to be returned if the key is not present and a remapping function.

For example:

Java
 




xxxxxxxxxx
1


 
1
// increments a counter or initialises it with the increment if it is not found
2
map.merge("GBP/CHF", 1, (existingValue, newValue) -> existingValue + newValue); 
3
// This could be simplified to 
4
map.merge("GBP/CHF", 1, Int::sum); 



Here, the initial value is combined with the existing value to calculate the new value.

Notes

All three methods should return either the existing or a new value to the calling code —therefore, you don't have to check for null. It's important to remember that ConcurrentHashMap is actually a HashTable, not a HashMap. Therefore, your function cannot return a null value to the map.

That would throw a NullPointerException.

merge() is a bit tricky in this respect. If the remapping function is executed (because the key exists) but returns null, then the existing value is removed and null returned to the caller. So, it shouldn't throw NullPointerException unless the key does not exist and the initialize value is also null

You don't have to be intending to actually do any computation. Here, I just want to make sure I get an Exceutor on which to stream market data snapshots for dollar/loonie whether it already exists or not and without having to check for null values. (This assumes mappings are never removed by my code.):

Java
 




xxxxxxxxxx
1


 
1
private final Map<String, ExecutorService> serviceMap = new ConcurrentHashMap<>();
2
ExecutorService exec3 = 
3
  serviceMap.computeIfAbsent("USD/CAD", (k) -> Executors.newSingleThreadExecutor());



Testing

Here is a sample of the results running five different versions of an update operation through a Monte Carlo simulator for 1 million trials. We are simulating the tossing of a fair coin. One would expect the results to be almost exactly 50:50.

Versions tested:

  1. computeIfAbsent().add() with a DoubleAdder
  2. compute() with a Double
  3. computeIfAbsent() to get a Double, increment it and put it back in the map
  4. computeIfAbsent() to get a Double, increment it and replace it in the map - in a loop
  5. merge()

Look at the huge difference in the variance between updates one, two, four, and five (similar) and update three. To me, that suggests that there has been interleaving of threads in update three and the operation was not atomic. Also, the probabilities no longer add up to unity.

Performance

There doesn't seem to be any significant difference in the performance regarding the time taken to perform the updates, but I wondered about memory usage. Remember, one of the advantages of passing lambdas or method references to a method whose arguments are functional interfaces is that you are passing code, not objects. Therefore, if the code is not required, then no object needs to be instantiated.

I couldn't see any real difference in memory consumption or garbage collection between merge() and compute() in VisualVM, but then Doubles are pretty small objects. There did seem to be significantly more Doubles being created using merge(), though. Perhaps 25% more.

Java
 




xxxxxxxxxx
1
26


 
1
Number of trials = 1 million
2

          
3
compute with a Double 
4
Concurrent Stream. Total time: nanoseconds 2359164784 or seconds = 2.359164784
5
Key: tails, Value: 0.49963900, Expected: 0.5, Difference: 0.072200000 percent
6
Key: heads, Value: 0.50036100, Expected: 0.5, Difference: 0.072148000 percent
7

          
8
computeIfAbsent with a DoubleAdder
9
Concurrent Stream. Total time: nanoseconds 2229822699 or seconds = 2.229822699
10
Key: tails, Value: 0.50033000, Expected: 0.5, Difference: 0.065956000 percent
11
Key: heads, Value: 0.49967000, Expected: 0.5, Difference: 0.066000000 percent
12

          
13
computeIfAbsent with a Double incrementing the value 
14
Concurrent Stream. Total time: nanoseconds 2313169217 or seconds = 2.313169217
15
Key: tails, Value: 0.46560200, Expected: 0.5, Difference: 6.8796000 percent
16
Key: heads, Value: 0.46416100, Expected: 0.5, Difference: 7.1678000 percent
17

          
18
computeIfAbsent with a Double replacing the value                                                                                                                                                        
19
Concurrent Stream. Total time: nanoseconds 2237265799 or seconds = 2.237265799
20
Key: tails, Value: 0.49958500, Expected: 0.5, Difference: 0.083000000 percent
21
Key: heads, Value: 0.50041500, Expected: 0.5, Difference: 0.082931000 percent
22

          
23
merge with a Double
24
Concurrent Stream. Total time: nanoseconds 1.9923561E+9 or seconds = 1.9923561
25
Key: tails, Value: 0.49968200, Expected: 0.5, Difference: 0.063600000 percent
26
Key: heads, Value: 0.50031800, Expected: 0.5, Difference: 0.063560000 percent



Java (programming language)

Opinions expressed by DZone contributors are their own.

Related

  • How to Convert XLS to XLSX in Java
  • Recurrent Workflows With Cloud Native Dapr Jobs
  • Java Virtual Threads and Scaling
  • Java’s Next Act: Native Speed for a Cloud-Native World

Partner Resources

×

Comments
Oops! Something Went Wrong

The likes didn't load as expected. Please refresh the page and try again.

ABOUT US

  • About DZone
  • Support and feedback
  • Community research
  • Sitemap

ADVERTISE

  • Advertise with DZone

CONTRIBUTE ON DZONE

  • Article Submission Guidelines
  • Become a Contributor
  • Core Program
  • Visit the Writers' Zone

LEGAL

  • Terms of Service
  • Privacy Policy

CONTACT US

  • 3343 Perimeter Hill Drive
  • Suite 100
  • Nashville, TN 37211
  • support@dzone.com

Let's be friends:

Likes
There are no likes...yet! 👀
Be the first to like this post!
It looks like you're not logged in.
Sign in to see who liked this post!