Welcome to our deep dive into Java Atomic Variables! This tutorial is designed to help both beginners and intermediates understand the concept from scratch and delve deeper into its practical applications. 📝
Atomic variables, in the context of Java, are a way to ensure that complex operations on variables are executed in a thread-safe manner. They are part of the java.util.concurrent.atomic package. These variables provide higher-performance alternatives to the synchronization offered by the synchronized keyword.
When multiple threads access and modify shared variables, they can lead to concurrency issues like race conditions and inconsistent states. Atomic variables help mitigate these issues by ensuring that operations on these variables are atomic, meaning they are executed as a single, indivisible operation.
Java provides several types of atomic variables, including:
AtomicInteger for integer valuesAtomicLong for long valuesAtomicReference for objectsLet's look at an example using AtomicInteger.
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicExample {
public static AtomicInteger counter = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(AtomicExample::increment).start();
}
}
public static void increment() {
counter.incrementAndGet(); // This operation is atomic!
System.out.println("Thread " + Thread.currentThread().getId() + " incremented the counter to: " + counter.get());
}
}In this example, we create an AtomicInteger variable counter. The incrementAndGet() method both increments the counter and returns the new value as an integer. When run, this program will output the counter being incremented by each thread, demonstrating the atomicity of the operation.
What does the `incrementAndGet()` method do in the provided example?
In this example, we demonstrate the use of an AtomicReference to maintain a stack in a thread-safe manner.
import java.util.concurrent.atomic.AtomicReference;
import java.util.LinkedList;
public class StackExample {
private static AtomicReference<Node> top = new AtomicReference<>(new Node(null));
static class Node {
int data;
Node next;
public Node(int data) {
this.data = data;
}
}
public static void push(int data) {
Node newNode = new Node(data);
while (true) {
Node currentTop = top.get();
Node newTop = new Node();
newTop.next = currentTop;
newTop.data = data;
if (top.compareAndSet(currentTop, newTop)) break;
}
}
public static int pop() {
Node currentTop;
while ((currentTop = top.get()) != null) {
Node nextNode = currentTop.next;
if (nextNode == null) {
throw new RuntimeException("Stack is empty");
}
if (top.compareAndSet(currentTop, nextNode)) {
return currentTop.data;
}
}
throw new RuntimeException("Error while popping from stack");
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
for (int j = 0; j < 5; j++) {
StackExample.push(j * 10);
}
}).start();
}
new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("Popped: " + StackExample.pop());
}
}).start();
}
}In this example, we create a thread-safe stack using AtomicReference. The push() method adds elements to the stack, while the pop() method removes and returns the top element.
And there you have it! You've learned about Java Atomic Variables, how they help with concurrency issues, and how to use them in practice. Happy coding! 💡