Java Concurrency: Replacing Synchronized with Atomic Operations

Thread Safety Challenges with Simple Operations

Consider a scenario where multiple threads need to increment a shared counter. A common implementation might use a standard integer field. However, even a simple operation like count++ is not atomic. It consists of three distinct steps: reading the current value, incrementing it, and writing the new value back to memory.

public class RaceConditionDemo {
    private int counter = 0;

    public void increment() {
        counter++; // Not thread-safe
    }

    public int getValue() {
        return counter;
    }

    public static void main(String[] args) throws InterruptedException {
        RaceConditionDemo demo = new RaceConditionDemo();
        ExecutorService executor = Executors.newFixedThreadPool(50);

        for (int i = 0; i < 100; i++) {
            executor.execute(demo::increment);
        }

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);

        System.out.println("Final count: " + demo.getValue());
    }
}

If you run this code, the result is often less than 100. This inconsistency occurs because threads can overwrite each other's changes during the read-modify-write cycle.

The Traditional Synchronized Approach

The most straightforward fix is to use the synchronized keyword to ensure mutual exclusion.

public synchronized void increment() {
    counter++;
}

While this guarantees correctness, it creates a performance bottleneck. Synchronized relies on OS-level locking mechanisms, causing threads to block and wait if the lock is held by another thread. This context switching and waiting can be expensive in high-concurrency environments.

Atomic Variables and CAS

A more efficient alternative is found in the java.util.concurrent.atomic package. Classes like AtomicInteger and AtomicLong use hardware-level Compare-And-Swap (CAS) operations.

CAS is an optimistic strategy that avoids locking. It operates on three operands:

  • The memory location (V)
  • The expected old value (A)
  • The new value to set (B)

The processor updates V to B only if V currently matches A. If another thread has modified V in the interim, the operation fails. The thread then re-reads the value and retries (spinning). This approach allows threads to remain active rather than blocking.

Implementing AtomicInteger

Replacing the integer with an AtomicInteger solves the race condition without explicit locks.

import java.util.concurrent.atomic.AtomicInteger;

public class SafeCounter {
    private final AtomicInteger sequence = new AtomicInteger(0);

    public void increment() {
        sequence.incrementAndGet();
    }

    public int getValue() {
        return sequence.get();
    }
}

The incrementAndGet() method atomically increments the value and returns the result, ensuring thread safety with minimal overhead.

Addressing the ABA Problem

A potential drawback of standard CAS is the "ABA problem." This occurs if a variable changes from A to B and back to A between the time a thread reads it and attempts to update it. The CAS operation succeeds because the value appears unchanged, but the state history has actually been modified.

To resolve this, Java provides AtomicStampedReference. It encapsulates both a reference and an integer "stamp" (version number). The CAS operation checks both the reference and the stamp, detecting intermediate changes.

import java.util.concurrent.atomic.AtomicStampedReference;

public class StampedUpdate {
    private AtomicStampedReference<Integer> ref;

    public StampedUpdate(int initial) {
        ref = new AtomicStampedReference<>(initial, 0);
    }

    public void update(int expected, int newValue) {
        int[] stampHolder = new int[1];
        Integer current = ref.get(stampHolder);
        // Only updates if the reference and stamp match expectations
        ref.compareAndSet(expected, newValue, stampHolder[0], stampHolder[0] + 1);
    }
}

Performance Optimization with LongAdder

For applications with extremely high write contention, Java 8 introduced LongAdder. While AtomicLong relies on a single value and causes threads to spin on one hotspot, LongAdder maintains a set of variables internally. Different threads update different cells, and the final value is computed by summing these cells.

This approach significantly reduces contention and improves throughput in scenarios where the exact read operation is less frequent than writes.

Tags: java Concurrency multithreading Atomic CAS

Posted on Tue, 20 Oct 2026 16:43:10 +0000 by madolia