CodeGym /Courses /JAVA 25 SELF /Java Memory Model (JMM)

Java Memory Model (JMM)

JAVA 25 SELF
Level 58 , Lesson 4
Available

1. Introduction to the Java Memory Model (JMM)

The visibility and ordering problem

In a single-threaded program, things are simple: you write a value to a variable — and you can read it right away. In the multithreaded world, it works differently. CPUs cache values, the compiler and the JVM sometimes reorder instructions, and one thread may see an “old” value even if another thread has just changed it.

The Java Memory Model (JMM) describes how threads communicate through memory: when the changes of one thread become visible to others and in what order operations occur. If you ignore this, a program can behave unpredictably, even though at first glance everything looks fine.

Understanding the JMM helps explain why a thread sometimes doesn’t see fresh data, how to correctly use volatile, synchronized, and the atomic classes, and why bugs in concurrent code can show up only in production. Put simply, the JMM is the rulebook of memory, and if you ignore it, even the most careful code can work against you.

Analogy

Imagine two people (threads) who write and read notes (variables) on a board (memory). Sometimes one writes, and the other still doesn’t see the new note — because they are looking at their own copy of the board (a cache). The JMM defines when and how these notes become visible to everyone.

2. happens-before: the foundation of the JMM

What is happens-before?

happens-before is a relation between two actions in a program: if action A happens-before action B, then all changes made in A are guaranteed to be visible in B.

Important: happens-before is not just “occurred earlier,” but specifically “guaranteed to be visible.”

Core happens-before rules

1. Within a single thread

Everything within one thread is ordered: if you write to a variable and then read it, you will see your own change.

2. Synchronized blocks/monitors

Everything that happens before exiting a synchronized block (synchronized) becomes visible to a thread that subsequently enters that block.

synchronized(lock) {
    sharedVar = 42; // write
}
// ...
synchronized(lock) {
    System.out.println(sharedVar); // we are guaranteed to see 42
}

3. volatile write/read

A write to a volatile field happens-before any subsequent read of that field by another thread.

volatile boolean ready = false;

// Thread 1
data = 123;
ready = true; // volatile write

// Thread 2
if (ready) { // volatile read
    System.out.println(data); // we are guaranteed to see data = 123
}

4. Starting and finishing threads

  • Calling Thread.start() happens-before the thread begins executing.
  • Thread termination happens-before return from Thread.join().

5. Task completion in an Executor

If you submit a task to an Executor and wait for it to complete (Future.get()), all changes made by the task are visible after get().

6. Final fields

Initializing fields with the final modifier in a constructor happens-before publishing the reference to the object. This is important for immutable objects.

3. Safe publication of objects

Problem: a “stale” object

If one thread creates an object and passes it to another thread without synchronization, the other thread may see “raw” field values (for example, uninitialized or old values).

class Holder {
    int value;
    Holder() { value = 42; }
}

Holder holder = null;

// Thread 1
holder = new Holder(); // create object

// Thread 2
if (holder != null) {
    System.out.println(holder.value); // may see 0, not 42!
}

How to publish objects correctly?

1. Via final fields

If all fields of an object are final and are initialized in the constructor, the object can be safely published without additional synchronization.

class SafeHolder {
    final int value;
    SafeHolder() { value = 42; }
}

2. Via a volatile reference

If the reference to the object is declared as volatile, then after assignment the object is guaranteed to be visible to other threads.

volatile Holder holder;

// Thread 1
holder = new Holder();

// Thread 2
if (holder != null) {
    System.out.println(holder.value); // we are guaranteed to see 42
}

3. Via single-threaded initialization before publication

If the object is created and initialized before the reference to it becomes accessible to other threads, it is safe.

Holder holder = new Holder(); // only in one thread
// ... then holder becomes accessible to other threads

4. Via locking

If an object is created inside a synchronized block, and the reference to it is read only inside the same kind of block, it is safe.

Holder holder;

synchronized(lock) {
    if (holder == null) {
        holder = new Holder();
    }
}

// ... in another thread
synchronized(lock) {
    if (holder != null) {
        // safe
    }
}

4. Double-checked locking and volatile

What is double-checked locking?

It is a pattern for lazy initialization of a singleton object:

class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) { // first check
            synchronized (Singleton.class) {
                if (instance == null) { // second check
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

Problem: Without a volatile reference for instance, this code is not correct! A thread may see a partially initialized object.

Why is it bad without volatile?

The JVM may reorder instructions so that the reference to the object is assigned before the constructor finishes. Another thread will see an uninitialized object.

How to do it right?

Declare instance as volatile:

private static volatile Singleton instance;

Now double-checked locking works correctly: volatile guarantees a happens-before relation between writing and reading the reference.

Alternative: static initialization

The simplest and safest way to implement a singleton is to use static initialization:

class Singleton {
    private static final Singleton INSTANCE = new Singleton();
    public static Singleton getInstance() { return INSTANCE; }
}

Here, the JVM itself guarantees proper initialization.

5. VarHandle: modern low-level access

What is VarHandle?

VarHandle is a modern API (Java 9+) that allows low-level work with variables: reading, writing, performing atomic operations, and controlling visibility and instruction ordering.

Why use VarHandle if atomic classes exist?
VarHandle lets you work with arbitrary fields (not only int/long/Reference).
— It lets you explicitly choose access semantics: volatile, acquire/release, opaque.
— It is used to implement high-performance data structures.

Access semantics

  • Volatile: full happens-before guarantee (same as a volatile field).
  • Acquire/Release: a weaker guarantee, but faster (used for lock-free structures).
  • Opaque: minimal visibility guarantees, but maximum performance.

Example usage of VarHandle

import java.lang.invoke.MethodHandles;
import java.lang.invoke.VarHandle;

class Counter {
    int value;
    static final VarHandle VALUE_HANDLE;

    static {
        try {
            VALUE_HANDLE = MethodHandles.lookup().findVarHandle(Counter.class, "value", int.class);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
}

Counter counter = new Counter();
Counter.VALUE_HANDLE.setVolatile(counter, 42);
int v = (int) Counter.VALUE_HANDLE.getVolatile(counter);

When to use VarHandle?

  • To implement your own lock-free data structures.
  • When you need maximum performance and control over instruction ordering.
  • In typical applications, the atomic classes and synchronized are usually sufficient.

6. False sharing and cache alignment

False sharing is a situation where two threads work with different variables, but those variables lie in the same CPU cache line. As a result, the threads interfere with each other because changing one variable invalidates the cache for the other.

Analogy: Two people sit at the same desk (a cache line), but each writes on their own half. If one changes something, the other has to “re-read” the whole sheet.

Why is this bad?

  • Performance drops sharply: CPUs spend time synchronizing caches.
  • It is especially critical for “hot” variables that are frequently modified by different threads.

How to avoid it?

Separate “hot” fields into different objects or use special annotations/structures for padding (for example, @Contended). In modern JVMs, you can enable the -XX:-RestrictContended option and use @sun.misc.Contended (Java 8+) to pad fields.

Example:

@sun.misc.Contended
public volatile long value1;

@sun.misc.Contended
public volatile long value2;

NB: The @Contended annotation is not part of the standard API, but it is used in the JDK to optimize the atomic classes.

7. Practice: fixing a singleton and JMH microbenchmarks

Fixing a broken singleton

Bad (without volatile):

class BrokenSingleton {
    private static BrokenSingleton instance;
    public static BrokenSingleton getInstance() {
        if (instance == null) {
            synchronized (BrokenSingleton.class) {
                if (instance == null) {
                    instance = new BrokenSingleton();
                }
            }
        }
        return instance;
    }
}

Good (with volatile):

class SafeSingleton {
    private static volatile SafeSingleton instance;
    public static SafeSingleton getInstance() {
        if (instance == null) {
            synchronized (SafeSingleton.class) {
                if (instance == null) {
                    instance = new SafeSingleton();
                }
            }
        }
        return instance;
    }
}

Best — static initialization:

class StaticSingleton {
    private static final StaticSingleton INSTANCE = new StaticSingleton();
    public static StaticSingleton getInstance() { return INSTANCE; }
}

JMH microbenchmarks: visibility and atomicity

Note: JMH is a special framework for Java microbenchmarks. Do not draw performance conclusions without JMH — results can be misleading!

Example: checking volatile visibility

public class VolatileVisibility {
    volatile boolean flag = false;

    public void writer() {
        flag = true;
    }

    public void reader() {
        while (!flag) {
            // spin until we see true
        }
        // change observed
    }
}

Example: non-atomic volatile

public class VolatileNotAtomic {
    volatile int counter = 0;

    public void increment() {
        counter++; // not atomic!
    }
}

Despite volatile, when multiple threads increment concurrently, the final value will be less than expected (the counter++ operation breaks down into a read, compute, and write).

8. Common mistakes when working with the JMM, volatile, and publication

Mistake No. 1: Expecting atomicity from volatile.
volatile guarantees only visibility of changes, not atomicity of operations. The operation counter++ does not become atomic just because the variable is volatile.

Mistake No. 2: Publishing objects without synchronization.
If you create an object in one thread and hand it to another without volatile, synchronized, or final fields, the other thread may see “raw” values.

Mistake No. 3: Double-checked locking without volatile.
Without a volatile reference to the singleton, you can get an uninitialized object in another thread.

Mistake No. 4: Using legacy locks with virtual threads.
Some legacy synchronization mechanisms (native monitor) can prevent the JVM from efficiently managing virtual threads.

Mistake No. 5: Ignoring false sharing.
If “hot” variables lie close together in memory, threads will interfere with each other due to cache lines.

Mistake No. 6: Drawing performance conclusions without JMH.
Microbenchmarks without JMH often yield misleading results due to JVM optimizations and CPU caches.

1
Task
JAVA 25 SELF, level 58, lesson 4
Locked
Smart Home: Safe Publication of Configuration 🏡
Smart Home: Safe Publication of Configuration 🏡
1
Task
JAVA 25 SELF, level 58, lesson 4
Locked
Resource Manager: The Only One in the Universe 🌌
Resource Manager: The Only One in the Universe 🌌
1
Survey/quiz
Diving deeper into multithreading, level 58, lesson 4
Unavailable
Diving deeper into multithreading
Diving deeper into multithreading
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION