Java - Atomic Variables and ConcurrentMap
This article will guide you to another Synchronization topic in the Java programming language, Atomic variables and Concurrent Maps. Both have been greatly improved in Java 8 with the introduction of Lambdas.
Most of the programming examples in this article follow Java 8 syntax and functionalities but they should run just fine on older versions as well.
For the simplicity of this article, we make use of two helper methods as defined by below ConcurrentUtils class:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
public class ConcurrentUtils {
public static void stop(ExecutorService executor) {
try {
executor.shutdown();
executor.awaitTermination(60, TimeUnit.SECONDS);
}
catch (InterruptedException e) {
System.err.println("termination interrupted");
}
finally {
if (!executor.isTerminated()) {
System.err.println("killing non-finished tasks");
}
executor.shutdownNow();
}
}
public static void sleep(int seconds) {
try {
TimeUnit.SECONDS.sleep(seconds);
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
Atomic Integer
When a data (typically a variable) can be accessed by several threads, you must synchronize the access to the data to ensure visibility and correctness.
>>> Atomic classes do not serve as replacements for java.lang.Integer and related classes.
The AtomicInteger class provides us with an int variable which can be read and written atomically, and which also contains advanced atomic methods like compareAndSet(). The AtomicInteger class is located in the package java.util.concurrent.atomic, so the fully qualified class name is java.util.concurrent.atomic.AtomicInteger.
Creating an AtomicInteger
Let’s create an AtomicInteger:
AtomicInteger atomicInteger = new AtomicInteger();
Above snippet creates an AtomicInteger with the initial value of 0.
If you want to create an AtomicInteger with some value, you can do so as follows:
AtomicInteger atomicInteger = new AtomicInteger(234);
This snippet passes a value of 234 as parameter to the AtomicInteger constructor, which sets the initial value of the AtomicInteger instance to 234.
Getting the AtomicInteger Value
Let’s get the value of an AtomicInteger instance via the get() method. Here is an AtomicInteger.get() snippet:
AtomicInteger atomicInteger = new AtomicInteger(234);
int theValue = atomicInteger.get();
Setting the AtomicInteger Value
Let’s now set the value of an AtomicInteger instance via the set() method. Here is an AtomicInteger.set() snippet:
AtomicInteger atomicInteger = new AtomicInteger(234);
atomicInteger.set(123);
This example creates an AtomicInteger example with an initial value of 234, and then sets its value to 123 in the next line.
Compare and Set the AtomicInteger Value
The AtomicInteger class also has an atomic compareAndSet() method. This method compares the current value of the AtomicInteger instance to an expected value, and if the two values are equal, sets a new value for the AtomicInteger instance. Here is an AtomicInteger.compareAndSet() snippet:
AtomicInteger atomicInteger = new AtomicInteger(234);
int expectedValue = 123;
int newValue = 111;
atomicInteger.compareAndSet(expectedValue, newValue);
This snippet first creates an AtomicInteger instance with an initial value of 234 . Then it compares the value of the AtomicInteger to the expected value 123 and if they are equal the new value of the AtomicInteger becomes 111;
Adding to the AtomicInteger Value
The AtomicInteger class contains a few methods you can use to add a value to the AtomicInteger and get its value returned. These methods are:
- addAndGet()
- getAndAdd()
- getAndIncrement()
- incrementAndGet()
The first method, addAndGet() adds a number to the AtomicInteger and returns its value after the addition. The second method, getAndAdd() also adds a number to the AtomicInteger but this time, returns the value to the AtomicInteger had before the value was added. Which of these two methods we should use depends on a use case. Here are two snippets:
AtomicInteger atomicInteger = new AtomicInteger();
System.out.println(atomicInteger.getAndAdd(10));
System.out.println(atomicInteger.addAndGet(10));
This example will print out the values 0 and 20. First the example gets the value of the AtomicIntegerbefore adding 10. Its value before addition is 0. Then the example adds 10 to the AtomicInteger and gets the value after the addition. The value is now 20.
We can also add negative numbers to the AtomicInteger via these two methods. The result is effectively a subtraction.
The methods getAndIncrement() and incrementAndGet() works like getAndAdd() and addAndGet() but just add 1 to the value of the AtomicInteger.
Subtracting From the AtomicInteger Value
The AtomicInteger class also contains a few methods for subtracting values from the AtomicIntegervalue atomically. These methods are:
- decrementAndGet()
- getAndDecrement()
The decrementAndGet() subtracts 1 from the AtomicInteger value and returns its value after the subtraction. The getAndDecrement() also subtracts 1 from the AtomicInteger value but returns the value the AtomicInteger had before the subtraction.
Use of AtomicInteger
Two main uses of AtomicInteger are:
- As an atomic counter (incrementAndGet(), etc) that can be used by many threads concurrently
- As a primitive that supports compare-and-swap instruction (compareAndSet()) to implement non-blocking algorithms.
While you can almost always achieve the same synchronization guarantees with ints and appropriate synchronized declarations, the beauty of AtomicInteger is that the thread-safety is built into the actual object itself, rather than you needing to worry about the possible interleavings, and monitors held, of every method that happens to access the int value. It's much harder to accidentally violate thread safety when calling getAndIncrement() than when returning i++ and remembering (or not) to acquire the correct set of monitors beforehand.
ConcurrentMap
The interface ConcurrentMap extends the Map interface and defines one of the most useful concurrent collection types. Java 8 introduces functional programming by adding new methods to this interface.
In the coming code examples, we use the following sample map to demonstrate those new methods:
ConcurrentMap<String, String> map = new ConcurrentHashMap<>();
map.put("foo", "bar");
map.put("han", "solo");
map.put("r2", "d2");
map.put("c3", "p0");
The method forEach() accepts a lambda expression of type BiConsumer with both the key and value of the map passed as parameters. It can be used as a replacement to for-each loops to iterate over the entries of the concurrent map. The iteration is performed sequentially on the current thread.
map.forEach((key, value) -> System.out.printf("%s = %s\n", key, value));
The method putIfAbsent() puts a new value into the map only if no value exists for the given key. At least for the ConcurrentHashMap implementation of this method is thread-safe just like put() so we don't have to synchronize when accessing the map concurrently from different threads:
String value = map.putIfAbsent("c3", "p1");
System.out.println(value); // p0
The method getOrDefault() returns the value for the given key. In case no entry exists for this key the passed default value is returned:
String value = map.getOrDefault("hi", "there");
System.out.println(value); // there
The method replaceAll() accepts a lambda expression of type BiFunction. BiFunctions take 2 parameters and return a single value. In this case the function is called with the key and the value of each map entry and returns a new value to be assigned to the current key:
map.replaceAll((key, value) -> "r2".equals(key) ? "d3" : value);
System.out.println(map.get("r2")); // d3
Instead of replacing all values of the map compute() let us transform a single entry. The method accepts both the key to be computed and a bi-function to specify the transformation of the value.
map.compute("foo", (key, value) -> value + value);
System.out.println(map.get("foo")); // barbar
In addition to compute() two variants exist: computeIfAbsent() and computeIfPresent(). The functional parameters of these methods only get called if the key is absent or present respectively.
Finally, the method merge() can be utilized to unify a new value with an existing value in the map. Merge accepts a key, the new value to be merged into the existing entry and a bi-function to specify the merging behavior of both values:
map.merge("foo", "boo", (oldVal, newVal) -> newVal + " was " + oldVal);
System.out.println(map.get("foo")); // boo was foo
ConcurrentHashMap
All those methods above are part of the ConcurrentMap interface, thereby available to all implementations of that interface. In addition the most important implementation, ConcurrentHashMap has been further enhanced with a couple of new methods to perform concurrent operations on the map.
Those methods use a special ForkJoinPool available via ForkJoinPool.commonPool() in Java 8. This pool uses a preset parallelism which depends on the number of available cores. 4 CPU cores are available on my machine which results in a parallelism of three:
System.out.println(ForkJoinPool.getCommonPoolParallelism()); // 3
This value can be decreased or increased by setting the following JVM parameter:
-Djava.util.concurrent.ForkJoinPool.common.parallelism=5
We use the same example map for demonstrating purposes but this time we work upon the concrete implementation ConcurrentHashMap instead of the interface ConcurrentMap, so we can access all public methods from this class:
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<>();
map.put("foo", "bar");
map.put("han", "solo");
map.put("r2", "d2");
map.put("c3", "p0");
Java 8 introduces three kinds of parallel operations: forEach, search and reduce. Each of those operations are available in four forms accepting functions with keys, values, entries and key-value pair arguments.
All of those methods use a common first argument called parallelism Threshold. This threshold indicates the minimum collection size when the operation should be executed in parallel.
E.g. if you pass a threshold of 500 and the actual size of the map is 499 the operation will be performed sequentially on a single thread. In the next examples we use a threshold of one to always force parallel execution for demonstrating purposes.
Conclusion
This article guided you to another Synchronization topic in Java programming language, Atomic variables and Concurrent Maps. Both have been greatly improved in Java 8 with the introduction of Lambdas. I hope you enjoyed it !
Recent Stories
Top DiscoverSDK Experts
Compare Products
Select up to three two products to compare by clicking on the compare icon () of each product.
{{compareToolModel.Error}}
{{CommentsModel.TotalCount}} Comments
Your Comment