When building multithreaded applications in Java, ensuring data integrity is crucial. A common challenge arises when multiple threads need to update a shared counter. Without proper synchronization, you can encounter race conditions, leading to incorrect results. This is where AtomicInteger comes to the rescue.

AtomicInteger is a class from the java.util.concurrent.atomic package that provides a thread-safe way to perform atomic operations on an integer value. Unlike traditional locking mechanisms (e.g., synchronized blocks), AtomicInteger uses low-level hardware instructions to achieve thread safety with higher performance.

The Problem: Race Conditions with Standard Integers

In a multithreaded environment, incrementing a simple int variable is not an atomic operation. The seemingly simple counter++ operation involves three distinct steps:

  1. Read: Read the current value of the counter.
  2. Modify: Increment the value.
  3. Write: Write the updated value back to the counter.

If two threads execute this operation simultaneously, they might both read the same initial value, increment it, and write back the same result. This leads to “lost updates” and an incorrect final value.

Non-Atomic Example

The following code demonstrates this problem. We start ten threads, and each thread increments a shared counter 10,000 times. We expect the final value to be 100,000, but the actual result is often lower due to race conditions.

import java.util.concurrent.atomic.AtomicInteger;

public class CounterExample {

    private static int counter = 0;
    private static AtomicInteger atomicCounter = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        nonAtomicExample();
        atomicExample();
    }

    public static void nonAtomicExample() throws InterruptedException {
        counter = 0;
        Runnable task = () -> {
            for (int i = 0; i < 10_000; i++) {
                counter++;
            }
        };

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("Non-Atomic Final Counter: " + counter);
    }

    public static void atomicExample() throws InterruptedException {
        atomicCounter.set(0);
        Runnable task = () -> {
            for (int i = 0; i < 10_000; i++) {
                atomicCounter.incrementAndGet(); // Atomic operation
            }
        };

        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(task);
            threads[i].start();
        }

        for (Thread thread : threads) {
            thread.join();
        }

        System.out.println("Atomic Final Counter: " + atomicCounter.get());
    }
}

The Solution: AtomicInteger

AtomicInteger solves this problem by providing atomic methods for updating the integer value. These methods use a hardware-level mechanism called Compare-and-Swap (CAS).

How Compare-and-Swap (CAS) Works

CAS is an atomic instruction that compares the contents of a memory location with a given value and, only if they are the same, modifies the contents of that memory location to a new given value. This is done as a single atomic operation.

The incrementAndGet() method, for example, uses CAS to ensure that the read-modify-write operation is atomic. It repeatedly tries to update the value until it succeeds, preventing race conditions.

Common AtomicInteger Methods

Here are some of the most commonly used methods in AtomicInteger:

  • get(): Returns the current value.
  • set(int newValue): Sets the value to newValue.
  • incrementAndGet(): Atomically increments the current value by one and returns the new value.
  • getAndIncrement(): Atomically increments the current value by one and returns the old value.
  • decrementAndGet(): Atomically decrements the current value by one and returns the new value.
  • getAndDecrement(): Atomically decrements the current value by one and returns the old value.
  • addAndGet(int delta): Atomically adds the given value to the current value and returns the new value.
  • getAndSet(int newValue): Atomically sets to the given value and returns the old value.
  • compareAndSet(int expect, int update): Atomically sets the value to update if the current value is equal to expect, and returns true if successful.

By using AtomicInteger, you can ensure the correctness of your multithreaded code without the performance overhead of traditional synchronization.