Skip to content

Atomicity

Atomicity is a fundamental concept in multithreading and concurrency that ensures operations are executed entirely or not at all, with no intermediate states visible to other threads. In Java, atomicity plays a crucial role in maintaining data consistency in concurrent environments.

This deep dive covers everything about atomic operations, issues with atomicity, atomic classes in Java, and best practices to ensure atomic behavior in your code.


What is Atomicity?

In a multithreaded program, atomicity guarantees that operations are executed as a single, indivisible unit. When an operation is atomic, it ensures that: 1. No other thread can see the intermediate state of the operation. 2. The operation either completes fully or fails without any effect.


Why is Atomicity Important?

Without atomic operations, multiple threads could interfere with each other, leading to race conditions and data inconsistencies. For example, if two threads try to increment a shared counter simultaneously, the result may not reflect both increments due to interleaving of operations.


Atomicity Problems in Java

  1. Non-Atomic Operations on Primitive Data Types

Consider the following code that increments a counter:

class Counter {
    private int count = 0;

    public void increment() {
        count++;  // Not atomic
    }

    public int getCount() {
        return count;
    }
}
- Problem: The statement count++ is not atomic. It consists of three operations: 1. Read the value of count. 2. Increment the value. 3. Write the new value back to count.

If two threads execute count++ simultaneously, one increment might be lost due to race conditions.


How to Ensure Atomicity?

Java provides several ways to ensure atomicity, including: 1. synchronized blocks and methods. 2. Explicit locks using ReentrantLock. 3. Atomic classes from the java.util.concurrent.atomic package (recommended for simple atomic operations).


Java’s Atomic Classes: Introduction

The java.util.concurrent.atomic package offers classes that support lock-free, thread-safe operations on single variables. These classes rely on low-level atomic operations (like CAS — Compare-And-Swap) provided by the underlying hardware.

Common Atomic Classes in Java

  1. AtomicInteger – Atomic operations on integers.
  2. AtomicLong – Atomic operations on long values.
  3. AtomicBoolean – Atomic operations on boolean values.
  4. AtomicReference<V> – Atomic operations on reference variables.
  5. AtomicStampedReference<V> – Supports versioned references to prevent ABA problems.

1. AtomicInteger Example: Solving the Increment Problem

import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
    private final AtomicInteger count = new AtomicInteger(0);

    public void increment() {
        count.incrementAndGet();  // Atomic increment
    }

    public int getCount() {
        return count.get();
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        AtomicCounter counter = new AtomicCounter();

        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println("Final Count: " + counter.getCount());  // Output: 2000
    }
}

Explanation:

  • The incrementAndGet() method ensures atomicity without using locks.
  • This solution is faster and more scalable than using synchronized or ReentrantLock.

2. AtomicBoolean Example: Managing Flags Safely

import java.util.concurrent.atomic.AtomicBoolean;

class FlagManager {
    private final AtomicBoolean isActive = new AtomicBoolean(false);

    public void activate() {
        if (isActive.compareAndSet(false, true)) {
            System.out.println("Flag activated.");
        } else {
            System.out.println("Flag already active.");
        }
    }

    public void deactivate() {
        if (isActive.compareAndSet(true, false)) {
            System.out.println("Flag deactivated.");
        } else {
            System.out.println("Flag already inactive.");
        }
    }
}

public class Main {
    public static void main(String[] args) {
        FlagManager manager = new FlagManager();

        Thread t1 = new Thread(manager::activate);
        Thread t2 = new Thread(manager::activate);

        t1.start();
        t2.start();
    }
}

Explanation:

  • compareAndSet() changes the flag only if it matches the expected value, ensuring thread safety.

3. AtomicReference Example: Atomic Operations on Objects

import java.util.concurrent.atomic.AtomicReference;

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }
}

public class AtomicReferenceExample {
    public static void main(String[] args) {
        AtomicReference<Person> personRef = new AtomicReference<>(new Person("Alice"));

        // Atomic update of the reference
        personRef.set(new Person("Bob"));
        System.out.println("Updated Person: " + personRef.get().name);
    }
}

Use Case:

  • Use AtomicReference when you need atomic operations on object references.

4. AtomicStampedReference Example: Solving the ABA Problem

The ABA problem occurs when a value changes from A to B and then back to A. AtomicStampedReference solves this by associating a version (stamp) with the value.

import java.util.concurrent.atomic.AtomicStampedReference;

public class AtomicStampedReferenceExample {
    public static void main(String[] args) {
        AtomicStampedReference<Integer> ref = new AtomicStampedReference<>(1, 0);

        int[] stamp = new int[1];
        Integer value = ref.get(stamp);
        System.out.println("Initial Value: " + value + ", Stamp: " + stamp[0]);

        boolean success = ref.compareAndSet(1, 2, stamp[0], stamp[0] + 1);
        System.out.println("CAS Success: " + success + ", New Value: " + ref.get(stamp) + ", New Stamp: " + stamp[0]);
    }
}

Explanation:

  • AtomicStampedReference ensures that the same value change does not go undetected by tracking the version.

Performance Benefits of Atomic Classes

  1. No Locks: Atomic operations are non-blocking and do not require heavy locks, improving throughput.
  2. Scalability: They perform well in highly concurrent environments.
  3. Low Overhead: CAS operations are supported natively by hardware, providing low-latency operations.

When to Use Atomic Classes

  1. Simple Counters or Flags:
  2. Use AtomicInteger or AtomicBoolean instead of synchronized methods.

  3. Non-blocking Algorithms:

  4. Use AtomicReference for lock-free algorithms.

  5. Preventing ABA Problems:

  6. Use AtomicStampedReference for versioned updates.

Limitations of Atomic Classes

  1. Limited to Single Variables:
  2. Atomic classes only work with single variables. For multiple variables, use synchronized or ReentrantLock.

  3. Not Suitable for Complex Transactions:

  4. For complex operations involving multiple state changes, atomic classes are insufficient.

  5. Still Prone to Livelocks:

  6. Atomic operations avoid deadlocks but can still suffer from livelocks (threads continuously retrying without progress).

Best Practices with Atomic Classes

  1. Use atomic classes for simple state management (e.g., counters, flags).
  2. Avoid overuse of atomic classes—for complex operations, use synchronized or ReentrantLock.
  3. Monitor performance—while atomic operations are faster, they might not suit every situation (e.g., write-heavy workloads).
  4. Avoid busy-waiting with atomic classes to prevent CPU wastage.

Summary

The atomic classes in Java’s java.util.concurrent.atomic package offer lock-free, thread-safe operations that are ideal for simple state management. By ensuring atomicity, these classes help avoid race conditions and improve the performance and scalability of multithreaded applications. However, they are best suited for single-variable updates—for more complex operations, locks or transactional mechanisms may still be necessary.