Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Java Memory Model: Understanding Visibility and Ordering Issues

Tech May 8 3

Java Memory Model: Understanding Visibility and Ordering Issues

Java Memory Model Fundamentals

The Java Memory Model (JMM) defines two logical memory regions:

  • A thread-private stack
  • A heap space shared among threads

The actual physical space for heap and stack consists of CPU registers, caches, and hardware memory.

Variable Storage Locations

Type Location
Primitive types (byte, short, int, long, float, double, boolean, char) Thread stack
Object references Reference on stack, object typically on heap
Primitive variables/references in object methods Stack
Objects referenced within objects Typically on heap
Static members in objects On heap with the object

Objects stored in the heap can be accessed by all threads holding a reference to the object. When two threads call the same method on the same object, they both access the object's member variables, but each thread has its own private copy of local variables.

Key JMM Concepts

  • Atomicity: Ensures instructions are not affected by thread context switching
  • Visibility: Ensures instructions are not affected by CPU caching (consistency issues between cache and memory)
  • Ordering: Ensures instructions are not affected by CPU instruction parallel optimization

Visibility in Java

Case Study: Main Thread and Worker Thread


package concurrency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class VisibilityExample {
    // volatile boolean active = true;  // Case 2
    static boolean active = true;       // Case 1
    
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(VisibilityExample.class);
        
        Thread worker = new Thread(() -> {
            while(active) {
                // Busy waiting
            }
        }, "worker");
        
        worker.start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        logger.warn("Attempting to stop worker thread");
        active = false;
    }
}
Case 1: Regular Static Variable

The worker thread fails to stop because:

  • The worker thread reads the value from its high-speed cache
  • The JIT compiler caches the value in the thread's working memory to improve performance
  • The main thread modifies the value in main memory
  • The worker thread always reads the old value from its cache
Case 2: Volatile Modifier

With volatile, threads directly operate on main memory, avoiding reading from their own working cache. This allows the thread to stop properly.

Visibility Summary

Visibility ensures that the latest value of a variable can be seen by all threads.

Strategies to Ensure Visibility
  1. Use the volatile keyword
  2. Access variables through locks

package concurrency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class LockVisibilityExample {
    static boolean active = true;
    static Object lock = new Object();
    
    public static void main(String[] args) {
        Logger logger = LoggerFactory.getLogger(LockVisibilityExample.class);
        
        Thread worker = new Thread(() -> {
            while(true) {
                synchronized (lock) {
                    if (!active)
                        break;
                }
            }
        }, "worker");
        
        worker.start();
        
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        logger.warn("Attempting to stop worker thread");
        synchronized (lock) {
            active = false;
        }
    }
}

The lock ensures visibility and atomicity of the shared variable.

Applications of Volatile

Two-Phase Termination Pattern

Requirement: "Gracefully" terminate a thread T2 from thread T1, allowing T2 to perform cleanup operations.

Incorrect Approaches
  • Using Thread.stop() - may not release locks
  • Using System.exit() - terminates the entire program
Approach 2: Volatile Shared Variable

package concurrency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class TwoPhaseTermination {
    private Thread monitor;
    private volatile boolean stop = false;
    
    public void stop() {
        stop = true;
        // Optional: uncomment to ensure thread stops even during sleep
        // monitor.interrupt();
    }
    
    public void start() {
        monitor = new Thread(() -> {
            while(true) {
                Thread current = Thread.currentThread();
                if(stop) {
                    Logger logger = LoggerFactory.getLogger(TwoPhaseTermination.class);
                    logger.warn("Performing cleanup before termination");
                    logger.warn("Stopping monitor thread");
                    break;
                }
                
                try {
                    current.sleep(2000);
                    Logger logger = LoggerFactory.getLogger(TwoPhaseTermination.class);
                    logger.warn("Sleeping for 2 seconds");
                    logger.warn("Performing monitoring tasks");
                } catch (Exception e) {
                    e.printStackTrace();
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }
}

Balking Pattern

Definition: Balking (hesitation) pattern is used when a thread finds that another thread or itself has already performed a certain task, so it ends directly without repeating the task.

Problem: Duplicate Thread Creation

TwoPhaseTermination service = new TwoPhaseTermination();
service.start();
service.start();  // Creates a second duplicate thread
Implementing Balking Pattern
Template

public class MonitoringService {
    private volatile boolean started;
    
    public void start() {
        synchronized (this) {
            if (started) {
                return;
            }
            started = true;
        }
        // Start the actual work
    }
}
Implementation Example

package concurrency;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SafeTwoPhaseTermination {
    private Thread monitor;
    private volatile boolean stop = false;
    private volatile boolean started = false;
    
    public void stop() {
        stop = true;
        started = false;
    }
    
    public void start() {
        synchronized (this) {
            Logger logger = LoggerFactory.getLogger(SafeTwoPhaseTermination.class);
            logger.warn("Attempting to create new monitor thread");
            if(started) {
                return;
            }
        }
        
        started = true;
        
        monitor = new Thread(() -> {
            while (true) {
                Thread current = Thread.currentThread();
                if (stop) {
                    Logger logger = LoggerFactory.getLogger(SafeTwoPhaseTermination.class);
                    logger.warn("Performing cleanup before termination");
                    logger.warn("Stopping monitor thread");
                    break;
                }
                
                try {
                    current.sleep(2000);
                    Logger logger = LoggerFactory.getLogger(SafeTwoPhaseTermination.class);
                    logger.warn("Sleeping for 2 seconds");
                    logger.warn("Performing monitoring tasks");
                } catch (Exception e) {
                    e.printStackTrace();
                    current.interrupt();
                }
            }
        });
        monitor.start();
    }
}
Balking Pattern for Thread-Safe Singleton

public final class Singleton {
    private Singleton() {}
    
    private static Singleton instance = null;
    
    public static synchronized Singleton getInstance() {
        if (instance != null) {
            return instance;
        }
        instance = new Singleton();
        return instance;
    }
}

Ordering Issues in Java

Overview

Java's ordering is reflected in instruction reordering - instructions that should be executed serially may be reordered for efficiency.

Principle: CPUs also perform instruction reordering. Reordering can cause non-unique results in multi-threaded environments.

Ordering Issues and Volatile in Multi-threading

Using Write Barriers with Volatile

Write barriers ensure that all shared variable saves before the barrier are synchronized to main memory.


number = 2;
volatile boolean ready = true;  // Write barrier ensures writes to volatile happen before
Using Read Barriers with Volatile

Read barriers ensure that all shared variable reads after the barrier are from main memory.


if(ready) {  // Read barrier ensures reads from volatile happen before
    number = number + 1;         
}

Summary: Volatile ensures variable reads and writes are synchronized to main memory. Note that volatile cannot prevent overall instruction reordering but ensures code block ordering within threads.

Double-Checked Locking Singleton Issues


public final class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    
    public static Singleton getInstance() {
        if(instance == null) {
            synchronized(Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
Why This Code Has Safety Issues

In bytecode, the initialization can be reordered:

  1. Assign reference to instance
  2. Call constructor to initialize object

This can cause another thread to see a non-fully initialized object.

Correct Double-Checked Locking with Volatile

public final class Singleton {
    private Singleton() {}
    
    // Volatile prevents instruction reordering
    private static volatile Singleton instance = null;
    
    public static Singleton getInstance() {
        if (instance != null) {
            return instance;
        }
        synchronized (Singleton.class) {
            if (instance != null) {
                return instance;
            }
            instance = new Singleton();
            return instance;
        }
    }
}

Happens-Before Rules

These rules ensure that writes to shared variables are visible to other threads.

Rule 1: Lock Synchronization

Variable reads/writes within synchronized blocks on the same object are visible to other threads.


static int x;
static Object lock = new Object();

new Thread(() -> {
    synchronized(lock) {
        x = 10;
    }
}).start();

new Thread(() -> {
    synchronized(lock) {
        System.out.println(x);
    }
}).start();
Rule 2: Volatile Variable Writes

A thread's write to a volatile variable is visible to subsequent reads by other threads.


volatile static int x;

new Thread(() -> {
    x = 10;
}).start();

new Thread(() -> {
    System.out.println(x);
}).start();
Rule 3: Thread Start

Writes to variables before thread start are visible to reads after the thread starts.


static int x;
x = 10;
new Thread(() -> {
    System.out.println(x);
}).start();
Rule 4: Thread Termination

Writes to variables before thread termination are visible to other threads after they know the thread has terminated.


static int x;
Thread t = new Thread(() -> {
    x = 10;
});
t.start();
t.join();  // Main thread knows t has terminated
System.out.println(x);
Rule 5: Thread Interruption

Writes to variables before interrupting a thread are visible to other threads after they know the thread was interrupted.


static int x;
Thread t = new Thread(() -> {
    while(true) {
        if(Thread.currentThread().isInterrupted()) {
            System.out.println(x);
            break;
        }
    }
});
t.start();

new Thread(() -> {
    sleep(1);
    x = 10;
    t.interrupt();
}).start();
Rule 6: Default Values

Writes to default values (0, false, null) are visible to other threads. The relationship is transitive: if x hb-> y and y hb-> z, then x hb-> z.


volatile static int x;
static int y;

new Thread(() -> {
    y = 10;
    x = 20;
}).start();

new Thread(() -> {
    System.out.println(x);  // x=20 is visible, and so is y=10
}).start();

Exercises

Balking Pattern Exercise

Identify the issue in this implementation:


public class VolatileExercise {
    volatile boolean initialized = false;
    
    void init() {
        if (initialized) {
            return;
        }
        doInit();
        initialized = true;
    }
    
    private void doInit() {
        // Initialization code
    }
}

Issue: No synchronization on the shared initialized variable, which could cause doInit() to be called multiple times in a multi-threaded environment.

Singleton Pattern Thread Safety Analysis

Implementation 1: Eager Initialization

public final class Singleton implements Serializable {
    private Singleton() {}
    
    private static final Singleton INSTANCE = new Singleton();
    
    public static Singleton getInstance() {
        return INSTANCE;
    }
    
    // Prevent serialization from creating new instances
    public Object readResolve() {
        return INSTANCE;
    }
}

Questions:

  1. Why final? Prevents inheritance and potential singleton pattern violations.
  2. Serializable considerations? Implement readResolve() to prevent deserialization from creating new instances.
  3. Why private constructor? Prevents direct instantiation but doesn't prevent reflection.
  4. Thread safety of initialization? Safe because static members are created during class loading by JVM.
  5. Why static method instead of public INSTANCE? Allows for lazy loading, additional control, and generic support.
Implementation 2: Enum Singleton

enum Singleton {
    INSTANCE;
}

Questions:

  1. How does enum restrict instances? Only the defined constants exist.
  2. Thread safety during creation? Safe, as static members are created during class loading.
  3. Reflection破坏单例? Cannot be broken by reflection.
  4. Serialization破坏单例? Safe, as enums handle serialization properly.
  5. Lazy or eager? Eager, as instances are created during class loading.
  6. Initialization logic? Can be added through constructors.
Implementation 3: Synchronized Method

public final class Singleton {
    private Singleton() {}
    private static Singleton instance = null;
    
    public static synchronized Singleton getInstance() {
        if(instance != null) {
            return instance;
        }
        instance = new Singleton();
        return instance;
    }
}

Analysis: Thread-safe but inefficient due to synchronization on every call.

Implementation 4: Double-Checked Locking

public final class Singleton {
    private Singleton() {}
    
    // Volatile prevents instruction reordering
    private static volatile Singleton instance = null;
    
    public static Singleton getInstance() {
        if (instance != null) {
            return instance;
        }
        synchronized (Singleton.class) {
            if (instance != null) {
                return instance;
            }
            instance = new Singleton();
            return instance;
        }
    }
}

Questions:

  1. Why volatile? Prevents instruction reordering that could expose partially initialized objects.
  2. Advantage over implementation 3? Subsequent calls don't require synchronization.
  3. Why null check inside synchronized block? Prevents duplicate creation when multiple threads enter the block.
Implementation 5: Static Holder

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

Questions:

  1. Lazy or eager? Lazy, as the instance is created only when needed.
  2. Thread safety during creation? Safe, as JVM ensures thread safety during class loading.

Singleton Pattern Summary

When designing singletons, consider:

  • Instruction reordering
  • Serialization/deserialization
  • Reflection mechanisms
  • Multi-threading and locking
  • Inheritance

The double-checked locking pattern with lazy loading is generally recommended.

Summary of Ordering and Visibility

  • Visibility: Caused by JVM caching optimization
  • Ordering: Caused by JVM instruction reordering optimization
  • Happens-before rules: Seven rules ensuring visibility of shared variable writes

Underlying Principles

  • CPU instruction parallelism (reordering issues)
  • Volatile (read/write barriers)

Application Patterns

  • Two-phase termination pattern using volatile variables
  • Balking pattern using volatile shared variables to avoid creating duplicate threads

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.