Memory Sharing Between Threads

About

In Java (and most modern programming languages), when we create multiple threads within the same process, they share a common memory space. This shared memory model allows threads to communicate and coordinate their actions by reading from and writing to shared variables or objects.

While this is powerful and efficient, it introduces complexity in terms of thread safety, data consistency, and visibility of changes. Understanding how memory is shared and managed across threads is essential for writing correct and performant multithreaded applications.

Why Threads Share Memory

All threads in a Java application run in the same process. A process is the operating system abstraction that provides memory, file handles, and other resources.

Since Java threads are lightweight and managed by the Java Virtual Machine (JVM), they:

  • Run within the same memory space (same heap).

  • Can reference the same objects.

  • Use their own execution stacks (method calls, local variables, etc.)

This design enables efficient communication between threads, unlike in multi-process architectures where communication requires IPC (Inter-Process Communication) mechanisms like sockets or pipes.

What’s Shared and What’s Not

Category
Shared Between Threads?
Description

Heap Memory

Yes

Includes all objects and class variables. Threads can read/write shared objects.

Stack Memory

No

Each thread has its own stack for local method variables. Not visible to other threads.

Static Variables

Yes

Belong to the class, not the instance, hence shared among all threads.

Instance Variables

Conditional

Shared only if multiple threads share a reference to the same object.

ThreadLocal Values

No

Each thread has its own isolated copy via ThreadLocal.

CPU Registers & Caches

No (by default)

Each CPU core/thread may cache values and not reflect them in main memory unless synchronized.

Dangers of Shared Memory

1. Race Condition

A race condition occurs when two or more threads access shared data and try to change it at the same time. The final outcome depends on the unpredictable timing of thread execution.

  • Threads “race” against each other to access or modify the same variable.

  • The program may produce different results on different runs even with the same input.

  • Happens due to lack of synchronization.

Example Scenario:

Two threads incrementing a shared counter simultaneously without locking it. One increment might get lost.

public class RaceConditionExample {
    private static int counter = 0;

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                counter++; // Not atomic, causes race condition
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start();
        t2.start();

        try {
            t1.join(); 
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final counter value: " + counter); // Might not be 2000
    }
}

2. Lost Update

This is a specific type of race condition where an update to a shared variable by one thread is overwritten or ignored because another thread wrote to it just before or after.

  • Multiple threads read the same value, update it, and write it back.

  • Since both used the same original value, one update "loses" the effect of the other.

  • The final result reflects only one update.

Example Scenario:

Both threads read a counter as 5, increment it to 6, and save it. Final value remains 6 instead of 7.

public class LostUpdateExample {
    private static int balance = 100;

    public static void main(String[] args) {
        Runnable withdraw = () -> {
            int temp = balance; // Thread reads balance
            temp = temp - 50;   // Deducts 50
            balance = temp;     // Writes back new balance
        };

        Thread t1 = new Thread(withdraw);
        Thread t2 = new Thread(withdraw);
        t1.start();
        t2.start();

        try {
            t1.join(); 
            t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Final balance: " + balance); // May be 50 instead of 0
    }
}

3. Visibility Problem

Even if operations happen in order, one thread might not see the updated value of a shared variable written by another thread due to CPU caching or compiler optimization.

  • Java threads may cache variables locally.

  • Changes in one thread might not be visible to others unless synchronization or volatile is used.

  • The result: a thread acts on stale data.

Example Scenario:

Thread A updates a flag to true, but Thread B keeps seeing it as false because it's using a cached copy.

public class VisibilityProblemExample {
    private static boolean flag = false;

    public static void main(String[] args) {
        Thread writer = new Thread(() -> {
            try { Thread.sleep(100); } catch (InterruptedException ignored) {}
            flag = true; // Update might not be visible to other thread
        });

        Thread reader = new Thread(() -> {
            while (!flag) {
                // May loop forever if flag is not visible
            }
            System.out.println("Flag is true");
        });

        writer.start();
        reader.start();
    }
}

4. Atomicity Violation

Even reading and writing a primitive (like int) isn't always safe if combined operations are involved.

Occurs when compound operations (like read-modify-write) are not performed atomically, i.e., they are interrupted between steps by other threads.

  • Even if visibility is fine, operations like x++ are not atomic.

  • They break down into read → compute → write.

  • Without synchronization, another thread might interleave between steps.

Example Scenario:

Multiple threads increment a value concurrently without locking. Final value is lower than expected due to lost increments.

public class AtomicityViolationExample {
    private static int shared = 0;

    public static void main(String[] args) {
        Runnable increment = () -> {
            for (int i = 0; i < 1000; i++) {
                shared = shared + 1; // Not atomic: read, modify, write
            }
        };

        Thread t1 = new Thread(increment);
        Thread t2 = new Thread(increment);
        t1.start(); t2.start();

        try {
            t1.join(); t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("Shared value: " + shared); // May not be 2000
    }
}

5. Data Corruption

Multiple threads mutate a data structure without proper synchronization, causing inconsistent state.

This is the most severe outcome. When multiple threads modify shared data in an uncoordinated way, it may lead to inconsistent, invalid, or broken data.

  • Happens when updates are partially completed.

  • Can lead to invalid program state, crashes, or security vulnerabilities.

  • Common in data structures not designed for concurrency.

Example Scenario:

Two threads modify a shared list at the same time. One adds and the other removes, leading to a corrupted internal state or ConcurrentModificationException

import java.util.ArrayList;
import java.util.List;

public class DataCorruptionExample {
    private static List<Integer> list = new ArrayList<>();

    public static void main(String[] args) {
        Runnable task = () -> {
            for (int i = 0; i < 1000; i++) {
                list.add(i); // ArrayList is not thread-safe
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);
        t1.start(); t2.start();

        try {
            t1.join(); t2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("List size: " + list.size()); // May throw exception or size < 2000
    }
}

What is Memory Visibility?

In a multithreaded Java program, memory visibility refers to whether a change made by one thread is visible to another thread.

  • Java threads do not always see the most updated value of a shared variable.

  • This happens because threads can cache variables locally (e.g., in registers or CPU caches), and those cached values may not be in sync with main memory.

  • So, even if Thread A updates a variable, Thread B might continue to see an old value.

Example of Memory Visibility Problem

public class VisibilityProblem {
    private static boolean flag = false;

    public static void main(String[] args) {
        new Thread(() -> {
            while (!flag) {
                // Might run forever if flag update is not visible
            }
            System.out.println("Flag changed!");
        }).start();

        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {}

        flag = true; // This change may not be visible to other thread
    }
}

In this code:

  • The writer thread sets flag = true.

  • But the reader thread may not see the update because the value of flag might be cached.

How to Fix It?

What is the Java Memory Model (JMM)?

The Java Memory Model is the formal set of rules that:

  • Define how threads interact with memory.

  • Specify when changes to variables made by one thread become visible to others.

  • Define synchronization rules to prevent race conditions, visibility problems, and instruction reordering issues.

It governs:

  • Reads and writes of variables.

  • Synchronization actions (volatile, synchronized, locks, atomic operations).

  • Thread safety and ordering guarantees.

Last updated