Site Search:

Chapter 16: The Java Memory Model

Back> 

Chapter 16: The Java Memory Model

Multithreaded programming in Java requires more than just using synchronized or volatile. To reason correctly about visibility, ordering, and atomicity between threads, you need to understand the Java Memory Model (JMM). Chapter 16 of Java Concurrency in Practice dives into this essential topic. In this post, we’ll break it down by examining what really happens under the hood with a deceptively simple line of code:

aVariable = 3;

What Really Happens Under the Hood

On the surface, this looks like a direct write to memory. But on modern systems with CPU caches, compiler optimizations, and multiple threads, it involves:

  • Compiler reordering (for optimization)
  • CPU instruction reordering and store buffers
  • Cache coherence between CPU cores
  • Memory barriers or fences (explicit or implied)
  • Delayed visibility to other threads

In the absence of synchronization, a thread may see a stale or inconsistent value of aVariable. The Java Memory Model was designed to handle these complexities in a way that gives developers rules to reason about concurrency across platforms.

Key Concepts of the Java Memory Model

  • Visibility: Whether one thread sees the results of another thread’s changes
  • Ordering: The order in which reads and writes happen with respect to program and hardware
  • Atomicity: Whether operations are indivisible (safe from race conditions)
  • Happens-Before: A key relationship that ensures visibility and ordering between operations

Happens-Before Examples

  • Thread A writing to a volatile variable happens-before Thread B reading it
  • Unlocking a synchronized block happens-before another thread locks the same monitor
  • Starting a thread happens-before its run() method

Safe Initialization Patterns

1. Thread-Safe Lazy Initialization (Broken Version)


private SomeObject instance;

public SomeObject getInstance() {
    if (instance == null) {
        instance = new SomeObject(); // not thread-safe!
    }
    return instance;
}

The above version is not thread-safe. Without proper synchronization, one thread could see a partially constructed object or a stale null value.

2. Eager Initialization (Always Safe)


private final SomeObject instance = new SomeObject();

public SomeObject getInstance() {
    return instance;
}

This is always safe because final fields initialized during object construction are guaranteed to be visible correctly to all threads due to the JMM's initialization safety.

3. Synchronized Lazy Initialization


private SomeObject instance;

public synchronized SomeObject getInstance() {
    if (instance == null) {
        instance = new SomeObject();
    }
    return instance;
}

This approach is thread-safe, but synchronized adds overhead even after the instance is initialized.

4. Double-Checked Locking with Volatile


private volatile SomeObject instance;

public SomeObject getInstance() {
    if (instance == null) {
        synchronized(this) {
            if (instance == null) {
                instance = new SomeObject();
            }
        }
    }
    return instance;
}

This idiom is safe starting from Java 5 and onward due to volatile guaranteeing visibility and preventing instruction reordering.

5. Initialization-on-Demand Holder Class Idiom (Best Practice)


public class Holder {
    private static class LazyHolder {
        static final SomeObject INSTANCE = new SomeObject();
    }

    public static SomeObject getInstance() {
        return LazyHolder.INSTANCE;
    }
}

This idiom is both thread-safe and highly performant. The class loader guarantees that INSTANCE is created only when accessed and ensures proper synchronization under the JMM.

Initialization Safety for Immutable Objects

Immutable objects, such as those using final fields, have special support from the Java Memory Model. When an object is safely published (e.g., stored in a volatile field or inside a synchronized block), all threads will see the initialized state of its final fields correctly.


public class ImmutableRGB {
    private final int red;
    private final int green;
    private final int blue;

    public ImmutableRGB(int r, int g, int b) {
        this.red = r;
        this.green = g;
        this.blue = b;
    }

    public int getRGB() {
        return ((red << 16) | (green << 8) | blue);
    }
}

Once an instance of ImmutableRGB is constructed and published, all threads will observe its fully initialized state—even without synchronization.

Conclusion

The Java Memory Model provides the foundation for writing safe and performant concurrent code. Understanding how operations like aVariable = 3 translate through CPU caches, compilers, and memory barriers will help you reason more clearly about visibility and correctness. Using volatile, synchronized, or atomic classes from java.util.concurrent ensures your concurrent objects behave reliably across all platforms.

No comments:

Post a Comment