Java atomic classes
Java provides several built-in synchronization primitives to manage concurrent access to shared resources, including locks, semaphores, and atomic classes. Atomic classes are a unique type of synchronization primitive that allows for lightweight concurrent access to shared resources without the need for locks or semaphores.
In this article, we will delve into the concept of atomic classes in Java, their
implementations, and best practices for using them effectively.
First thing first : What are Atomic Classes in Java?
Atomic classes are built-in synchronization primitives in Java that allow multiple threads to access a shared resource concurrently without the need for locks or semaphores. They provide a lightweight and efficient way to manage concurrent access to shared resources, making them ideal for high-contention scenarios. In this article we will not go into the pitfalls of multi-threading while using non-thread-safe variable.
Atomic classes can be used to implement a variety of synchronization primitives,
including
Atomic variables
Atomic variables are simple variables that can be accessed by multiple threads simultaneously without the need for locks or semaphores.
They provide a convenient way to share data between threads without worrying about race conditions. Here is an example of how atomic
variables can be used:
public static class AtomicVariableExample {
/**
* Atomic variable to track how many time the operation has been * completed
*/
private static final AtomicLong completed = new AtomicLong(0);
private static Long nonThreadSafeVariable = 0L;
public static void main(String[] args) throws InterruptedException{
// Using Atomic Variable
Runnable atomicTask = () -> {
completed.incrementAndGet();
System.out.println("Incremented atomic variable " + completed.get());
};
// Using synchronized block
Runnable synchronizedTask = () -> {
synchronized (AtomicVariableExample.class) {
nonThreadSafeVariable++;
System.out.println("Incremented non thread safe variable "+ nonThreadSafeVariable);
}
};
var executor = Executors.newVirtualThreadPerTaskExecutor();
for (var i = 0; i < 10; i++) {
executor.submit(atomicTask);
executor.submit(synchronizedTask);
}
executor.awaitTermination(5, TimeUnit.SECONDS);
}
}Atomic operations
Atomic operations are methods that perform a single operation on an atomic variable, such as incrementing or decrementing its value. These methods ensure that the operation is executed atomically, meaning that either the entire operation is completed, or it is rolled back and tried again if there is a failure. One might assume that incrementing or decrementing a variable is a single operation therefor there is no need for such complexity. However incrementing a variable myVariable++ is actually two operations first you read the current value, then add one to it, and finally you assign that computed value to the variable myVariable= myVariable + 1.
Now that we got this out the way, here's an example of how atomic operations can be used:
Compare And Swap
package multithreading;
import java.util.Random;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
public class CompareAndSwapOperation {
public static AtomicInteger atomicInteger = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
final var gen = new Random();
final var executor = Executors.newVirtualThreadPerTaskExecutor();
/**
* CompareAndSwap (method named comparedAndSet)
* this method compares the current value of the variable
* to an expected value, they are the same it set a new value and returns true
* if the are not the same, it doesn't set the value and returns false
*/
Runnable task = () -> {
var guessedValue = gen.nextInt(0, 10);
var randomNewValue = gen.nextInt(0, 10);
var correctGuess = atomicInteger.compareAndSet(guessedValue, randomNewValue);
if (correctGuess) {
System.out.println("We guess that the value was " + guessedValue + " and that was " + correctGuess
+ " new value will be " + randomNewValue);
}
};
for (var i = 0; i < 1000; i++) {
executor.submit(task);
}
executor.awaitTermination(10, TimeUnit.SECONDS);
}
}
There are many others atomic operations that we have not covered but I think this ones are the most important and the most used.
Best Practices
- Use atomic classes sparingly:
Atomic classes should be used sparingly and only when necessary to avoid unnecessary overhead. They are best suited for situations where
a lightweight synchronization mechanism is required. - Use locks or semaphores in addition to atomic classes:
In some cases, you may want to use locks or semaphores in conjunction with atomic classes to provide an additional layer of
synchronization. This can help ensure that your code is both correct and efficient. - Test your code extensively:
Test your code thoroughly to ensure that it behaves correctly under different scenarios, including high-contention situations. This will
help identify any issues or bugs that may arise due to the use of atomic classes.
Conclusion
In conclusion, atomic classes are a powerful tool in Java for managing concurrent access to shared resources without the need for locks
or semaphores. By understanding their implementations and best practices, developers can create efficient and scalable multi-threaded
applications. Whether you're working on a simple web application or a complex enterprise system, mastering the art of atomic classes can
help you build robust and reliable software.