Skip to main content

Command Palette

Search for a command to run...

Understanding Concurrency in Java/Kotlin: Visibility vs Atomicity

Published
4 min read
Understanding Concurrency in Java/Kotlin: Visibility vs Atomicity
S

Hey everyone,

I'm Shamitha, working as a programmer analyst at amazon. and also i teach DSA and AWS in a practical way. look me up at teacheron!

When writing multithreaded programs, especially in Java or Kotlin, it’s easy to run into subtle and hard-to-reproduce concurrency bugs.

Thread safety failures can be divided into a few categories. Two of the most important are Atomicity and Visibility.


1. What is Atomicity?

Atomicity refers to whether a thread sees a complete and indivisible operation. A classic case is incrementing a counter:

int counter = 0;

void increment() {
    counter++; // Not atomic: it's read + modify + write
}

What might happen here:

volatile int counter = 0;

Thread 1: counter++; // Read 0 → Add 1 → Write 1 Thread 2: counter++; // Read 0 → Add 1 → Write 1

This results in lost updates due to a race condition.

✅ Fixing Atomicity:

To ensure atomicity:

  • Use synchronized to protect the entire read-modify-write block

  • Use AtomicInteger which uses CAS (Compare-And-Swap) internally

AtomicInteger counter = new AtomicInteger(0);

void increment() {
    counter.incrementAndGet(); // Atomic and thread-safe
}

2. What is Visibility?

Visibility refers to whether a thread can see the most recent value written to a shared variable by another thread.

Modern CPUs and JVMs perform many optimizations:

  • Threads may keep copies of variables in CPU registers or local caches rather than reading/writing from main memory (RAM) every time.

  • So, Thread A might change a variable, but Thread B might still see an old, stale value — because it’s reading from its own cached version, not from main memory.

🔍 Example of Visibility Problem:
var shouldStop = false

fun runner() {
    while (!shouldStop) {
        doSomething()
    }
}

fun stopper() {
    shouldStop = true
}

In this example, the thread running runner() might never stop — even though stopper() sets shouldStop to true. This is because the value of shouldStop might be cached in a CPU register or local thread cache and never re-read from main memory.

✅ Fixing Visibility:

To ensure visibility:

  • Use @Volatile (Kotlin) or volatile (Java)

  • Use @Synchronized (Kotlin) or synchronized blocks (Java)

  • Use atomic classes like AtomicBoolean, AtomicInteger, etc.

When writing multithreaded code, you usually need both atomicity and visibility to ensure correctness:

  • Atomicity prevents race conditions

  • Visibility ensures threads see the latest values

🧠 Important Insight:

  • Using synchronized ensures both atomicity and visibility.

  • Using volatile ensures visibility only — it does not make compound operations atomic.

volatile int counter = 0;

public void increment() {
    counter++; // Still not atomic, even though volatile gives visibility!
}

Here:

  • Threads see the latest value (visibility is fine)

  • But the increment is not atomic, so threads interfere and updates get lost (race condition)

✅ Fixing it with synchronized:

synchronized void increment() {
    counter++;
}

This:

  • Makes counter++ atomic (only one thread enters at a time)

  • Guarantees visibility within the synchronized block

🟡 Key Point:

Fixing atomicity often fixes visibility too, but fixing visibility alone (e.g., using volatile) does not fix atomicity.


3. How AtomicInteger Works Internally

AtomicInteger is part of the java.util.concurrent.atomic package and provides:

  • Atomic operations like incrementAndGet(), getAndAdd(), compareAndSet(), etc.

  • These are lock-free, high-performance alternatives to using synchronized.

Under the hood, AtomicInteger uses a low-level mechanism called CAS (Compare-And-Swap) via the Unsafe class, allowing threads to update shared variables without blocking.


4. The Developer–Compiler Contract

The JVM and CPU are allowed to reorder, cache, or optimize your code unless explicitly instructed not to.

Synchronization tools (like synchronized, volatile, and atomic classes) tell the compiler and CPU:

“Multiple threads are accessing this variable — do not apply optimizations that would break shared memory semantics.”

This is why even reads must be synchronized if any thread writes to the variable.


🧠 The Golden Rule (from Java Concurrency in Practice):

Whenever more than one thread accesses a given state variable, and one of them might write to it, they all must coordinate their access to it using synchronization.