Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Java 8: ConcurrentHashMap Atomic Updates

DZone's Guide to

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.

· Java Zone ·
Free Resource

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

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

... 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:

// Incrementing a count of the occurrences of a currency symbol
// (In reality we would have used an atomic variable even pre Java 8)
ConcurrentHashMap <String Integer> map = new ConcurrentHashMap <>();
String key = "USD/JPY";
Double oldValue; Double newValue; double increment = 1.0;
do {
    oldValue = results.get(key);
    newValue = oldValue == null? increment: oldValue + increment;
} 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

public V putIfAbsent(K key, V value)

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

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

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

// The arguments to the remapping bifunction are the existing value (if found)
// and the initial value which was passed to the method in position 2 and the 
// result is a value that has been computed, possibly by combining them
public V merge(K key, V value,
               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:

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

// This is not threadsafe or atomic
// The update of the mutable non-threadsafe value takes place outside the computeIfAbsent() method
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:

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

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


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:

// increments a counter or initialises it with the increment if it is not found
map.merge("GBP/CHF", 1, (existingValue, newValue) -> existingValue + newValue); 
// This could be simplified to 
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. Hopefully, the test code makes it more clear. 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.):

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


Testing

Having asserted that update operations may not be atomic if care is not taken, I suppose it's up to me to try and prove that!

Appendix 1 (seen farther down the page) shows 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.

Appendix 2 (also below) is a JUnit test harness to check that various simple examples compile and run accurately under single-threaded access

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.

If you look at the method signatures again, merge() requires an extra object to be instantiated and passed to it — the initialize value.

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.

Appendix 1: Results of Update Operations (Simulating Coin Tosses) Through Monte Carlo

/**
 * The Parallel and the Concurrent Monte Carlo simulator code is taken from the following work
 * and adapted to try to make it more general so that a function to implement the required probabilities
 * can be passed to it (for example, tossing a coin instead of throwing two dice)
 * Any subsequent errors introduced are, of course, mine
 * Java 8 Lamdas. Copyright 2014 Richard Warburton. O'Reilly Media. ISBN 978-1-449-37077-0
 * Monte Carlo Simulation of Dice rolling. Chapter 6 pg 85,86
*/

public static final int NUMTRIALS = 1000000;

private static void computeDouble(ExecutorService service, 
    Map<String, Double> results, Function<ThreadLocalRandom, String> function, 
                                  CountDownLatch latch, Double increment ) {
    service.execute( () -> {
        final ThreadLocalRandom random = ThreadLocalRandom.current();
        final String key = function.apply(random);
        results.compute( key, (k, v) -> {
            Double counter = (v == null? new Double(0): v);
            counter += increment;
            return counter;
        });
        latch.countDown();
    });
}
Concurrent Stream. Total time: nanoseconds 2359164784 or seconds = 2.359164784
Key: tails, Value: 0.49963900, Expected: 0.5, Difference: 0.072200000 percent
Key: heads, Value: 0.50036100, Expected: 0.5, Difference: 0.072148000 percent

private static void computeIfAbsentDoubleAdder(ExecutorService service, 
    Map<String, DoubleAdder> results, 
         Function<ThreadLocalRandom, String> function, 
             CountDownLatch latch, Double increment) {
    service.execute( () -> {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        final String key = function.apply(random);
        results.computeIfAbsent(key, (k) -> new DoubleAdder()).add(increment);
        latch.countDown();
   });
}
Concurrent Stream. Total time: nanoseconds 2229822699 or seconds = 2.229822699
Key: tails, Value: 0.50033000, Expected: 0.5, Difference: 0.065956000 percent
Key: heads, Value: 0.49967000, Expected: 0.5, Difference: 0.066000000 percent

private static void computeIfAbsentDouble(ExecutorService service, 
    Map<String, Double> results, Function<ThreadLocalRandom, String> function, 
                                    CountDownLatch latch, Double increment) {
    service.execute( () -> {
        ThreadLocalRandom random = ThreadLocalRandom.current();
        final String key = function.apply(random);
        results.put(key, results.computeIfAbsent(key, (k) -> 0.0) + increment);
        latch.countDown();
    });
}
Concurrent Stream. Total time: nanoseconds 2313169217 or seconds = 2.313169217
Key: tails, Value: 0.46560200, Expected: 0.5, Difference: 6.8796000 percent
Key: heads, Value: 0.46416100, Expected: 0.5, Difference: 7.1678000 percent

private static void computeIfAbsentDoubleAtomic(ExecutorService service, Map<String, Double> results,                           
    Function<ThreadLocalRandom, String> function, CountDownLatch latch, double increment) 
    service.execute( () -> {                                                                                                    
        ThreadLocalRandom random = ThreadLocalRandom.current();                                                                 
        final String key = function.apply(random);                                                                              
        Double oldValue; Double newValue;                                                                                       
        do {                                                                                                                    
            oldValue = results.computeIfAbsent(key, (k) -> 0.0);                                                                
            newValue = oldValue + increment;                                                                                    
        } while (!results.replace(key, oldValue, newValue));                                                                    

        latch.countDown();                                                                                                                              
    });                                                                                                                         
}                                                                                                                               
Concurrent Stream. Total time: nanoseconds 2237265799 or seconds = 2.237265799
Key: tails, Value: 0.49958500, Expected: 0.5, Difference: 0.083000000 percent
Key: heads, Value: 0.50041500, Expected: 0.5, Difference: 0.082931000 percent

private static void mergeDouble(ExecutorService service, Map<String, Double> results,
    Function<ThreadLocalRandom, String> function, CountDownLatch latch, Double increment ) {
    service.execute( () -> {
        final ThreadLocalRandom random = ThreadLocalRandom.current();
        final String key = function.apply(random);
        results.merge( key, increment, Double::sum);
        latch.countDown();
    });
}
Concurrent Stream. Total time: nanoseconds 1.9923561E+9 or seconds = 1.9923561
Key: tails, Value: 0.49968200, Expected: 0.5, Difference: 0.063600000 percent
Key: heads, Value: 0.50031800, Expected: 0.5, Difference: 0.063560000 percent


Appendix 2: Test Harness for Updating Values in a ConcurrentHashMap

package com.microliquidity.javalamdas;

import junit.framework.TestCase;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runners.MethodSorters;

import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.DoubleAdder;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;

/** Sorts tests  and executes by method name */
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class TestCompute {

    private final Map<Integer, DoubleAdder> doubleAdderResults = new ConcurrentHashMap<>();
    private final Map<Integer, Double> doubleResults = new ConcurrentHashMap<>();
    private final Map<Integer, MutablePoint> pointResults = new ConcurrentHashMap<>();
    private final Integer mapKeyZero = 0, mapKeyOne = 1, mapKeyTwo = 2, mapKeyThree = 3,
            mapKeyFour = 4, mapKeyFive = 5, mapKeyTen = 10;
    private Double increment = 1.0;
    private static final DoubleAdder defaultValue = new DoubleAdder();
    private final Map<String, ExecutorService> serviceMap = new ConcurrentHashMap<>();

    @BeforeClass
    public static void setup() {
        defaultValue.add(1);
    }

    class MutablePoint {

        private int y;
        private int x;

        MutablePoint(int argy, int argx) {
            y = argy;
            x = argx;
        }

        public final int getY() { return y; } public final void setY(int argy) { y = argy; }
        public final int getX() { return x; } public final void setX(int argx) { x = argx; }
        public final void move(int argy, int argx) { y += argy; x += argx;}

        public String toString() {
            return "Y = " + y + " X = " + x;
        }
    }

    class ImmutableCompassBearing {

        private final String desc;

        ImmutableCompassBearing( String desc )
        {
            this.desc = desc;
        }

        public String getDesc() {return desc;}
    }


    @Test
    public void testIndependentImmutableValue() {

        int[] data = {mapKeyZero, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyTwo,
                mapKeyTwo, mapKeyTwo, mapKeyThree, mapKeyThree, mapKeyZero, mapKeyThree, mapKeyThree};
        doubleResults.clear();
        // The Double is immutable and the new value does not depend on the old value or anything else
        for (int i : data) {
            doubleResults.computeIfAbsent(i, (key) ->
                ThreadLocalRandom.current().nextDouble(1, 100)
            );
        }

        for(Map.Entry entry: doubleResults.entrySet()) {
            System.out.println("Double Results. Key: " + entry.getKey() + " value: " +
                    entry.getValue());
        }

        assertTrue( "Value: ",  doubleResults.containsKey(mapKeyZero) );
        assertTrue( "Value: ",  doubleResults.containsKey(mapKeyOne) );
        assertTrue( "Value: ",  doubleResults.containsKey(mapKeyTwo) );
        assertTrue( "Value: ",  doubleResults.containsKey(mapKeyThree) );
    }

    @Test
    public void testDependentImmutableValueComputeLamda() {

        int [] data =  {mapKeyZero, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyTwo,
                mapKeyTwo, mapKeyTwo, mapKeyThree, mapKeyThree, mapKeyZero, mapKeyThree, mapKeyThree  };
        doubleResults.clear();
        double initialValue = 1.0;
        // The Double is immutable but the new value depends on the old value
        for (int i : data) {
            doubleResults.compute( i, (key, value) -> {
                value = (value == null? initialValue: value + increment);
                return value;
                // The key is not used
            });
        }

        for(Map.Entry entry: doubleResults.entrySet()) {
            System.out.println("Double Results. Key: " + entry.getKey() + " value: " +
                    entry.getValue());
        }
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyZero)),
                doubleResults.get(mapKeyZero) == 2.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyTwo)),
                doubleResults.get(mapKeyTwo) == 3.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyThree)),
                doubleResults.get(mapKeyThree) == 4.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyOne)),
                doubleResults.get(mapKeyOne) == 5.0 );
    }

    @Test
    public void testDependentImmutableValueComputeMethodRef() {

        int [] data =  {mapKeyZero, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyTwo,
                mapKeyTwo, mapKeyTwo, mapKeyThree, mapKeyThree, mapKeyZero, mapKeyThree, mapKeyThree  };
        doubleResults.clear();
        double initialValue = 1.0;
        // The Double is immutable but the new value depends on the old value
        for (int i : data) {
            // In order to prefer to pass a method reference instead of a lamda as per EJ 3 Item 43 we have
            // to write a custom method that implements BiFunction
            doubleResults.compute( i, this::sumDoublesHandlingNullValues);
        }

        for(Map.Entry entry: doubleResults.entrySet()) {
            System.out.println("Double Results. Key: " + entry.getKey() + " value: " +
                    entry.getValue());
        }
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyZero)),
                doubleResults.get(mapKeyZero) == 2.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyTwo)),
                doubleResults.get(mapKeyTwo) == 3.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyThree)),
                doubleResults.get(mapKeyThree) == 4.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyOne)),
                doubleResults.get(mapKeyOne) == 5.0 );
    }
    private double sumDoublesHandlingNullValues(int key, Double value) {
        // The key is not used
        double initialValue = 1.0;
        return (value == null? initialValue: value + increment);
    }

    @Test
    public void testDependentImmutableValueMerge() {

        int [] data =  {mapKeyZero, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyTwo,
                mapKeyTwo, mapKeyTwo, mapKeyThree, mapKeyThree, mapKeyZero, mapKeyThree, mapKeyThree  };
        doubleResults.clear();
        double initialValue = 1.0;
        // The Double is immutable but the new value depends on the old value
        for (int i : data) {
            // Unlike with compute() we don't have to handle the possibility of the old value being null in
            // our mapping function
            doubleResults.merge( i, initialValue, (oldValue, newValue) -> oldValue + newValue);
            // Or: passing a method reference instead of a lamda as per EJ 3 Item 43
            // doubleResults.merge( i, initialValue, Double::sum);
        }

        for(Map.Entry entry: doubleResults.entrySet()) {
            System.out.println("Double Results. Key: " + entry.getKey() + " value: " + entry.getValue());
        }
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyZero)),
                doubleResults.get(mapKeyZero) == 2.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyTwo)),
                doubleResults.get(mapKeyTwo) == 3.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyThree)),
                doubleResults.get(mapKeyThree) == 4.0 );
        assertTrue( "Value: " + Double.toString(doubleResults.get(mapKeyOne)),
                doubleResults.get(mapKeyOne) == 5.0 );
    }

    @Test
    public void testDependentMutableNotThreadsafeScalar() {

        int [] data =  {mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyTwo, mapKeyTwo,
                mapKeyTwo, mapKeyThree, mapKeyThree, mapKeyThree, mapKeyZero, mapKeyZero, mapKeyThree  };
        // The MutablePoint is not threadsafe and the new value depends on the old value
        for (int i : data) {
            pointResults.compute( i, (key, value) -> {
                value = (value == null? new MutablePoint(0, 0): value);
                value.move(5, 20);
                return value;
            });
        }
        // This is not atomic.
        // The update of the mutable non-threadsafe value takes place outside the computeIfAbsent() method
        pointResults.computeIfAbsent(mapKeyZero, (k) -> new MutablePoint(0, 0)).move(10, 40);

        for(Map.Entry entry: pointResults.entrySet()) {
            System.out.println("Mutable Point Results. Key: " + entry.getKey() + " value: " + entry.getValue());
        }
        assertTrue( "Value: " + pointResults.get(mapKeyZero).toString(),
                pointResults.get(mapKeyZero).getX() == 80 && pointResults.get(mapKeyZero).getY()== 20);
        assertTrue( "Value: " + pointResults.get(mapKeyOne).toString(),
                pointResults.get(mapKeyOne).getX() == 100 && pointResults.get(mapKeyOne).getY()== 25);
        assertTrue( "Value: " + pointResults.get(mapKeyOne).toString(),
                pointResults.get(mapKeyTwo).getX() == 60 && pointResults.get(mapKeyTwo).getY()== 15);
        assertTrue( "Value: " + pointResults.get(mapKeyThree).toString(),
                pointResults.get(mapKeyThree).getX() == 80 && pointResults.get(mapKeyThree).getY()== 20);

    }

    @Test
    public void testDependentMutableThreadsafe() {

        int [] data =  {mapKeyTwo, mapKeyTwo, mapKeyTwo, mapKeyThree, mapKeyThree, mapKeyThree, mapKeyThree,
                mapKeyFour, mapKeyFour, mapKeyFour, mapKeyFour, mapKeyFour, mapKeyFive, mapKeyFive, mapKeyFive};
        // Updates to the mutable value object are performed outside the computeIfAbsent() method so the
        // updates would not be atomic if the DoubleAdder, itself, was not threadsafe
        for (int i : data) {
            doubleAdderResults.computeIfAbsent(i, (k) -> new DoubleAdder()).add(increment);
        }

        for(Map.Entry entry: doubleAdderResults.entrySet()) {
            System.out.println("DoubleAdder Results. Key: " + entry.getKey() + " isShutdown: " + entry.getValue());
        }

        assertTrue( "Value: " + Double.toString(doubleAdderResults.get(mapKeyTwo).doubleValue()),
                doubleAdderResults.get(mapKeyTwo).longValue() == 3.0 );
        assertTrue( "Value: " + Double.toString(doubleAdderResults.get(mapKeyThree).doubleValue()),
                doubleAdderResults.get(mapKeyThree).longValue() == 4.0 );
        assertTrue( "Value: " + Double.toString(doubleAdderResults.get(mapKeyFour).doubleValue()),
                doubleAdderResults.get(mapKeyFour).longValue() == 5.0 );
        assertTrue( "Value: " + Double.toString(doubleAdderResults.get(mapKeyFive).doubleValue()),
                doubleAdderResults.get(mapKeyFive).longValue() == 3.0 );
    }

    @Test
    public void testIndependentMutableThreadsafe() {

        // Java 8, computeIfAbsent() returns either the existing value or the new value so we don't have to
        // check for null. The ExecutorService will not be instantiated if a new one is not required
        ExecutorService exec1 =
                serviceMap.computeIfAbsent("EUR/USD", (k) -> Executors.newSingleThreadExecutor());
        System.out.println("Executor " + "EUR/USD" + ": Terminated = " + exec1.isTerminated());

        ExecutorService exec2 =
                serviceMap.computeIfAbsent("GBP/JPY", (k) -> Executors.newSingleThreadExecutor());
        System.out.println("Executor " + "GBP/JPY" + ": Terminated = " + exec2.isTerminated());

        ExecutorService exec3 =
                serviceMap.computeIfAbsent("USD/CAD", (k) -> Executors.newSingleThreadExecutor());
        System.out.println("Executor " + "USD/CAD" + ": Terminated = " + exec3.isTerminated());

        // It's entirely unnecessary to check if an Executor is already shutdown because .shutdown() is idempotent
        // This is just to illustrate that you can atomically perform any valid operation on the map value
        ExecutorService exec4 = serviceMap.compute("GBP/CHF", (k, v) -> {
            v = (v == null? Executors.newSingleThreadExecutor(): v);
            if (!v.isShutdown()) {
                v.shutdown();
            }
            return v;
        });
        System.out.println("Executor " + "GBP/CHF" + ": Terminated = " + exec4.isTerminated());

        // Not computing anything here either. Taking advantage of the fact that we get a valid value and don't have
        // to check for null. We put an Executor in the map and shut it down
        // Unlike the exec4 example the shutdown() is not performed atomically because it is outside computeIfAbsent()
        serviceMap.computeIfAbsent("AUS/NZD", (k) -> Executors.newSingleThreadExecutor()).shutdown();
        System.out.println("Executor " + "AUS/NZD" + ": Terminated = " +  serviceMap.get("AUS/NZD").isTerminated());

        for(String mapkey: serviceMap.keySet()) {
            System.out.println("Executor Results. Key: " + mapkey + " isShutdown: " +
                    serviceMap.get(mapkey).isShutdown() );
        }
        assertTrue( "No of Executors: " + serviceMap.size(), serviceMap.size() == 5);
        assertFalse( "EUR/USD Shutdown : ", serviceMap.get("EUR/USD").isShutdown());
        assertFalse( "GBP/JPY  Shutdown : ", serviceMap.get("GBP/JPY").isShutdown());
        assertFalse( "USD/CAD Shutdown : ", serviceMap.get("USD/CAD").isShutdown());
        assertTrue( "AUS/NZD Shutdown : ", serviceMap.get("AUS/NZD").isShutdown());
        assertTrue( "GBP/CHF Shutdown : ", serviceMap.get("GBP/CHF").isShutdown());
    }

    @Test
    public <T> void  testDependentMutableNotThreadsafeCollection() throws
            ClassCastException, NullPointerException, IllegalArgumentException, UnsupportedOperationException {

        final String NORTH = "NORTH", SOUTH = "SOUTH", EAST = "EAST", WEST = "WEST";
        // Even though the map is concurrent and the compass bearings are immutable - the list is NOT threadsafe
        Map<String, List<ImmutableCompassBearing>> bearings = new ConcurrentHashMap<>();
        String keyOne = "1", keyTwo = "2";

        // We can assign the result to something of the same type as the value. In this case a List of ImmutableCompassBearing
        // The operation is executed within the compute() method so is atomic
        // Here we know the compass bearings that will be added to the list at compile time so we can do it all in one statement
        List<ImmutableCompassBearing> returnedOne = bearings.compute(keyOne, (k, v) -> {
            v = (v == null ? new ArrayList<>() : v);
            v.add(new ImmutableCompassBearing(NORTH));
            v.add(new ImmutableCompassBearing(SOUTH));
            v.add(new ImmutableCompassBearing(EAST));
            return v;
        });
        for (ImmutableCompassBearing b : returnedOne) {
            System.out.println("Key: " + keyOne + " Value : " + b.getDesc());
        }

        // We cannot assign the result to something of the same type as the value because the result of List.add() is a boolean
        // so we need to retrieve it from the map again or separate the assignment of the List from the add() operation
        // This compiles and runs just fine in this test BUT the add() to the List is not atomic because the List is not
        // threadsafe or an atomic artifact and the operation occurs outside of the computeIfAbsent() method
        String[] data1 = {NORTH, SOUTH, EAST, WEST};
        for (String s : data1) {
            bearings.computeIfAbsent(keyTwo, (k) -> new ArrayList<>()).add(new ImmutableCompassBearing(s));
        }
        for (ImmutableCompassBearing b : bearings.get(keyTwo)) {
            System.out.println("Key: " + keyTwo + " Value : " + b.getDesc());
        }

        // The operation is executed within the compute() method so is atomic
        // Here we don't know the compass bearings that will be added to the list at compile time so we have to do it in a loop
        String[] data2 = {SOUTH, WEST, EAST};
        for (String s : data2) {
            bearings.compute(keyTwo, (k, v) -> {
                v = (v == null ? new ArrayList<>() : v);
                v.add(new ImmutableCompassBearing(s));
                return v;
            });
        }
        for (ImmutableCompassBearing b : bearings.get(keyTwo)) {
            System.out.println("Key: " + keyTwo + " Value : " + b.getDesc());
        }

        TestCase.assertTrue("Compass bearing: ", bearings.get(keyOne).get(1).getDesc().equals(SOUTH));
        TestCase.assertTrue("Compass bearing: " + bearings.get(keyTwo).get(0).getDesc(), bearings.get(keyTwo).get(0).getDesc().equals(NORTH));
        TestCase.assertTrue("Compass bearing: ", bearings.get(keyTwo).get(6).getDesc().equals(EAST));
    }


    @Test
    public void testComputeIfAbsentMappingFunctionReturnsNull() {
        int[] data = {mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne};
        int yMoveValue = 5, xMoveValue = 20, yFinalValue = 0, xFinalValue = 0, zero = 0;

        for (int i : data) {
            pointResults.compute(i, (key, value) -> {
                value = (value == null ? new MutablePoint(zero, zero) : value);
                value.move(yMoveValue, xMoveValue);
                return value;
            });
            yFinalValue += yMoveValue;
            xFinalValue += xMoveValue;
        }

        Exception thrown = null;
        assertTrue("mapKeyOne exists", pointResults.get(mapKeyOne) != null &&
                pointResults.get(mapKeyOne).getX() == xFinalValue && pointResults.get(mapKeyOne).getY() == yFinalValue);
        try {
            // The key exists therefore the Function is not executed. The existing value is returned to the calling code
            MutablePoint m = pointResults.computeIfAbsent(mapKeyOne, (key) -> {
                MutablePoint mp = null;
                return null;
            });
            System.out.printf("computeIfAbsent(): Key %1$s exists. Value is not null. x = %2$s, y = %3$s" +
                    System.lineSeparator(), mapKeyOne, m.getX(), m.getY());
        } catch (Exception npe) {
            System.out.println("computeIfAbsent(): Key exists. NullPointerException thrown. message = " + npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was thrown", thrown == null);
        assertTrue("mapKeyOne exists", pointResults.get(mapKeyOne) != null &&
                pointResults.get(mapKeyOne).getX() == xFinalValue && pointResults.get(mapKeyOne).getY() == yFinalValue);


        thrown = null;
        assertTrue("mapKeyTen exists", pointResults.get(mapKeyTen) == null);
        try {
            // The key does not exist therefore the Function is executed. Null is returned to the map causing a NullPointerException
            MutablePoint m = pointResults.computeIfAbsent(mapKeyTen, (key) -> {
                MutablePoint mp = null;
                return mp;
            });
            System.out.printf("computeIfAbsent(): Key %1$s exists. Value is not null. x = %2$s, y = %3$s" +
                    System.lineSeparator(), mapKeyOne, m.getX(), m.getY());
        } catch (NullPointerException npe) {
            System.out.println("computeIfAbsent(): Key does not exist. NullPointerException thrown. message = " +
                    npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was not thrown",
                thrown != null && thrown instanceof NullPointerException);
        assertTrue("mapKeyTen exists", pointResults.get(mapKeyTen) == null);

    }


    @Test
    public void testComputeReMappingFunctionReturnsNull() {
        int[] data = {mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne};
        int yMoveValue = 5, xMoveValue = 20, yFinalValue = 0, xFinalValue = 0, zero = 0;

        for (int i : data) {
            pointResults.compute(i, (key, value) -> {
                value = (value == null ? new MutablePoint(zero, zero) : value);
                value.move(yMoveValue, xMoveValue);
                return value;
            });
            yFinalValue += yMoveValue;
            xFinalValue += xMoveValue;
        }

        Exception thrown = null;
        assertTrue("mapKeyOne exists", pointResults.get(mapKeyOne) != null &&
                pointResults.get(mapKeyOne).getX() == xFinalValue && pointResults.get(mapKeyOne).getY() == yFinalValue);
        try {
            // The BiFunction is executed whether the key exists or not. The existing value is found but ignored and
            // null is returned to the map causing a NullPointerException and removing the mapping
            MutablePoint m = pointResults.compute(mapKeyOne, (key, value) -> {
                MutablePoint mp = null;
                return mp;
            });
            System.out.printf("compute(): Key exists. Value is not null. x = %1$s, y = %2$s" +
                    System.lineSeparator(), m.getX(), m.getY());
        } catch (NullPointerException npe) {
            System.out.println("compute(): Key exists but the function computed a null value. " +
                    "NullPointerException thrown. message = " + npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was not thrown",
                thrown != null && thrown instanceof NullPointerException);
        assertTrue("mapKeyOne exists", pointResults.get(mapKeyOne) == null);


        thrown = null;
        assertTrue("mapKeyTen exists", pointResults.get(mapKeyTen) == null);
        try {
            // The BiFunction is executed whether the key exists or not. The existing value is not found so is null and
            // null is returned to the map causing a NullPointerException
            MutablePoint m = pointResults.compute(mapKeyTen, (key, value) -> value);
            System.out.printf("compute(): Key does not exist. Value is not null. x = %1$s, y = %2$s" +
                    System.lineSeparator(), m.getX(), m.getY());
        } catch (NullPointerException npe) {
            System.out.println("compute(): Key does not exist. NullPointerException thrown. message = " + npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was not thrown",
                thrown != null && thrown instanceof NullPointerException);
        assertTrue("mapKeyTen exists", pointResults.get(mapKeyTen) == null);

    }

    @Test
    public void testMergeReMappingFunctionReturnsNull() {
        int[] data = {mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne, mapKeyOne};
        int yMoveValue = 5, xMoveValue = 20, yFinalValue = 0, xFinalValue = 0, zero = 0, one = 1, two = 2, six = 6, seven = 7;

        for (int i : data) {
            pointResults.compute(i, (key, value) -> {
                value = (value == null ? new MutablePoint(zero, zero) : value);
                value.move(yMoveValue, xMoveValue);
                return value;
            });
            yFinalValue += yMoveValue;
            xFinalValue += xMoveValue;
        }

        Exception thrown = null;
        assertTrue("mapKeyOne does not exist", pointResults.get(mapKeyOne) != null &&
                pointResults.get(mapKeyOne).getX() == xFinalValue && pointResults.get(mapKeyOne).getY() == yFinalValue);
        try {
            // The key exists so the remapping function is executed. It tries to return null to the map.
            // This causes the existing value to be removed and null returned to the calling code
            // The initialize value is ignored
            MutablePoint m = pointResults.merge(mapKeyOne, new MutablePoint(one, two),
                    (exists, init) -> {
                        MutablePoint mp = null;
                        return mp;
                    });
            System.out.printf("merge(): Return value was null = %1$s" + System.lineSeparator(), m == null);
        } catch (NullPointerException npe) {
            System.out.println("merge(): Key exists. NullPointerException thrown. message = " + npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was thrown", thrown == null);
        assertTrue("mapKeyOne exists", pointResults.get(mapKeyOne) == null);


        thrown = null;
        assertTrue("mapKeyTen exists", pointResults.get(mapKeyTen) == null);
        try {
            // The key does not exist so the remapping function is not executed.
            // Therefore the initialize value is associated with the key in the map
            MutablePoint m = pointResults.merge(mapKeyTen, new MutablePoint(six, seven),
                    (exists, init) -> {
                        MutablePoint mp = null;
                        return mp;
                    });
            System.out.printf("merge(): Key does not exist. New mapping created. key = %1$s, x = %2$s, y = %3$s" +
                    System.lineSeparator(), mapKeyTen, m.getX(), m.getY());
        } catch (NullPointerException npe) {
            System.out.println("merge(): Key does not exist. NullPointerException thrown. message = " + npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was thrown", thrown == null);
        assertTrue("Value: " + pointResults.get(mapKeyTen).toString(),
                pointResults.get(mapKeyTen) != null &
                        (pointResults.get(mapKeyTen).getX() == seven && pointResults.get(mapKeyTen).getY() == six));


        thrown = null;
        MutablePoint nullInitializeValue = null;
        assertTrue("mapKeyTwo exists", pointResults.get(mapKeyTwo) == null);
        try {
            // The key does not exist so the remapping function is not executed.
            // Therefore the initialize value is associated with the key in the map but the value is null
            // so the map throws a NullPointerException
            MutablePoint m = pointResults.merge(mapKeyTwo, nullInitializeValue,
                    (exists, init) -> {
                        MutablePoint mp = null;
                        return mp;
                    });
            System.out.printf("merge(): Return value was null = %1$s" + System.lineSeparator(), m == null);
        } catch (NullPointerException npe) {
            System.out.println("merge(): Key does not exist. NullPointerException thrown. message = " + npe.getMessage());
            thrown = npe;
        }
        assertTrue("NullPointerException was not thrown", thrown != null);
        assertTrue("mapKeyTwo exists", pointResults.get(mapKeyTwo) == null);

    }
}

How do you break a Monolith into Microservices at Scale? This ebook shows strategies and techniques for building scalable and resilient microservices.

Topics:
concurrenthashmap ,java ,java 8 ,java performance ,atomic updates ,tutorial

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}