Appearance
Threads, Processes, and Their Memory Model
Multithreading is one of those topics where engineers who have been coding for years still stumble in interviews. Not because the concept is exotic, but because the follow up questions go deep fast. You say "a thread is a lightweight process" and the interviewer says "okay, so what memory does it share and what memory does it own?" Then you hesitate. Then they ask about race conditions. Then about the Java Memory Model. This article is going to make sure that never happens to you.
Before we talk about threads or any of the machinery around concurrency, you need to build a clear picture of two things: what a process actually is, and what a thread actually is. Everything else in multithreading, every lock, every race condition, every visibility bug, flows directly from understanding these two structures and the memory they use.
What Is a Process?
Here is the definition you will hear everywhere: a process is an instance of a program that is getting executed. That sounds clean but it hides a lot, so let us unpack it slowly.
You have a file called Main.java sitting on your disk. Right now it is just text. Nothing is happening. Now you open a terminal and type javac Main.java. The compiler reads your source file and produces bytecode, a Main.class file. Still nothing is running. The bytecode is just sitting there.
Now you type java Main. At this exact moment, the JVM starts a brand new process. The operating system sees that you want to execute a program and it springs into action: it allocates memory for that program, sets up a bunch of internal data structures, and creates an environment where your code can actually run. That entire environment is the process.
So a process is not just your code. A process is the whole living, breathing execution context: the memory, the resources, the threads inside it, everything. When your program is running, a process exists. When the program finishes or crashes, the process dies.
The really important thing about processes is that each one is completely isolated from every other process. If you open two terminal windows and run java Main in each, you get two separate processes. They have their own memory. They cannot accidentally read each other's variables. If one crashes with an OutOfMemoryError, the other keeps running completely unaffected. The operating system enforces this separation at the hardware level.
What Is a Thread?
Now that you understand a process, you can understand what a thread is. The definition: a thread is the smallest sequence of instructions that are executed by the CPU independently.
Think about your Java code as a long list of instructions. Add these numbers. Store the result. Check this condition. Call this method. Somebody has to actually read those instructions one by one and carry them out. That somebody is the CPU, but the CPU does not work on instructions directly from your Java class file. It works on machine code, the native binary instructions specific to the processor architecture. The thread is what ties together a sequence of those machine code instructions and hands them to the CPU to execute.
When a process is created, it automatically gets one thread. That thread is called the main thread, and it starts executing your main method. You can see this yourself:
java
public class ThreadDemo {
public static void main(String[] args) {
// This prints "main" because that is the name of the starting thread
System.out.println(Thread.currentThread().getName());
}
}From the main thread you can create additional threads to do work in parallel. This is what multithreading means: one process, multiple threads, all executing simultaneously (or appearing to).
How the JVM and Process Connect
Here is where things get precise, and this is what trips people up in interviews. You know that the JVM manages things like heap memory and a stack. But how does that relate to processes?
The answer is: every time you execute java Main, a brand new JVM instance is created and assigned to that process. Not a shared JVM. A fresh, isolated instance belonging entirely to that process.
This JVM instance has several distinct memory regions:
- Heap memory (where objects live)
- Stack memory (where method call frames live)
- Code segment (where machine code lives)
- Data segment (where static and global variables live)
- Registers (temporary storage during execution)
- Program Counter (pointer to the next instruction)
Each process gets its own copy of all of these. Process A and Process B have completely separate heap memories. They have separate code segments. They cannot touch each other's data.
Controlling How Much Memory a Process Gets
Because each process gets its own JVM instance with its own heap, you can control the heap size when you launch a program:
bash
# Set initial heap to 256 MB and maximum heap to 2 GB
java -Xms256m -Xmx2g MainThe -Xms flag sets the initial heap size that the process starts with. The -Xmx flag sets the ceiling. If your program tries to allocate more objects than the ceiling allows, the JVM throws OutOfMemoryError: Java heap space and the process crashes. The other process you have running in a different terminal is completely unaffected because it has its own separate heap.
This is why developers sometimes run multiple JVM processes for isolation: if one goes out of memory, the others survive.
The Six Memory Areas in Detail
Now let us go through each memory area properly, because understanding these is the key to understanding everything about thread safety.
Code Segment
The code segment stores the compiled machine code of your program. When the JVM starts, it takes your bytecode and uses either its interpreter or the JIT (Just in Time) compiler to convert it into machine code that the CPU understands. That machine code gets stored in the code segment.
The code segment is read only. No thread can modify it. Once the machine code is generated, it stays fixed. Because it is read only, all threads can access it simultaneously without any risk of corrupting anything. This is one part of thread safety you get for free.
Data Segment
The data segment stores static variables and global variables. In Java this means your static fields. Any variable declared as static on a class lives in the data segment.
Unlike the code segment, the data segment is read and write. Multiple threads can read and modify the same static variable. This is dangerous and this is where synchronization becomes necessary. If two threads both try to modify a static counter variable at the same time without any coordination, they will corrupt each other's work.
Heap Memory
The heap stores every object you create with the new keyword. Instance variables of objects live on the heap. The String pool lives on the heap. Anything that is allocated dynamically at runtime goes to the heap.
Heap memory is shared among all threads within the same process. This is the core of why multithreading is hard. Thread one and thread two can both hold a reference to the same object. If they both try to modify that object at the same time, the result is unpredictable. This is called a race condition and we will look at it closely in a moment.
Heap memory is not shared between processes. Process A and Process B have completely separate heaps. Even though the JVM as a whole has a total memory budget, each process's JVM instance maps to a different region of that memory.
Stack Memory
Each thread has its own private stack. The stack stores method call frames: local variables, method parameters, the return address. When your method calls another method, a new frame is pushed onto the stack. When the method returns, the frame is popped off.
Because each thread has its own stack, local variables are completely thread safe by design. Thread A cannot reach into Thread B's stack and read or modify its local variables. This isolation is built in at the architecture level.
This is an important interview point: if a variable is a local variable inside a method, it is always thread safe. Race conditions only happen with shared state, meaning objects on the heap or static variables in the data segment.
Registers
The JVM has its own registers, similar to the CPU's registers. They store intermediate values during computation. When the JIT compiler generates machine code, it sometimes needs to hold temporary results between instructions. Those go in the registers.
Registers are private to each thread. This is crucial for context switching, which we will get to shortly. Each thread has its own set of register values that represent the thread's current computation state.
Program Counter
The program counter, often called the PC register, holds the memory address of the next instruction that the thread needs to execute in the code segment. As each instruction finishes successfully, the PC increments to point to the next instruction.
Think of the code segment as a long list of machine code instructions numbered by address. The program counter is simply your finger pointing to the line you are about to read. Each thread has its own finger pointing to its own place in the code.
This is what allows multiple threads to execute different parts of the same code simultaneously. Thread one's program counter points to the machine code for your calculateInterest method. Thread two's program counter points to the machine code for your checkBalance method. They both read from the same code segment but at different addresses.
What Threads Share and What They Own
This is probably the most important concept in this entire topic. Let us be absolutely explicit.
Threads within the same process share:
- The code segment (machine code of the program)
- The data segment (static variables)
- The heap (all objects created with new)
Threads within the same process own privately:
- Their stack (local variables and method frames)
- Their registers (intermediate computation state)
- Their program counter (which instruction to execute next)
The shared areas are where concurrency bugs happen. The private areas are thread safe by design.
Different processes share nothing. Full stop.
Context Switching: How One CPU Runs Multiple Threads
You might only have one CPU core, but you can have dozens of threads. How does that work?
The operating system uses time slicing. It gives each thread a short window of CPU time, maybe a few milliseconds, then pauses that thread and switches to another one. This switching is called a context switch.
Here is exactly what happens during a context switch. Thread one is running on the CPU. The OS timer fires and says time is up. The OS grabs all the intermediate state from the CPU: the current values in the CPU registers, the current program counter value. It saves all of this into Thread one's register storage. Thread one is now paused, but all its state is preserved.
Next, the OS picks Thread two. It loads Thread two's saved register values into the CPU registers and loads Thread two's program counter into the PC. The CPU resumes executing from exactly where Thread two left off.
When Thread one gets a turn again, the process reverses. Its saved state is loaded back, and the CPU continues Thread one from exactly where it paused.
From a human perspective, all threads appear to be running at the same time. From the CPU's perspective, it is rapidly switching between them. This is called concurrency, not true parallelism.
True parallelism only happens when you have multiple CPU cores. If you have four cores and three threads, all three threads can physically execute at the same instant on separate cores. No context switching needed. But if you have four cores and forty threads, you get a mix: some threads run truly parallel, and the OS context switches to service the rest.
java
public class SystemInfo {
public static void main(String[] args) {
// See how many cores the JVM can use
int cores = Runtime.getRuntime().availableProcessors();
System.out.println("Available CPU cores: " + cores);
Thread current = Thread.currentThread();
System.out.println("Current thread name: " + current.getName());
System.out.println("Current thread ID: " + current.getId());
System.out.println("Current thread state: " + current.getState());
}
}The Complete Flow From Source File to Running Threads
Let us trace the entire journey one time from start to finish so everything connects.
You have Main.java with a main method that creates two additional threads. Here is what happens when you run java Main:
Step one: The JVM creates a new process. The operating system allocates resources for this process and creates an isolated execution environment.
Step two: A new JVM instance is allocated to that process. This JVM instance has all the memory regions: heap, stack, code segment, data segment, registers, program counter.
Step three: The JVM starts converting your bytecode to machine code using its interpreter or JIT compiler. During this conversion, it discovers that you need three threads: the main thread, and the two you create explicitly.
Step four: The machine code is stored in the code segment.
Step five: Three threads are created. Each thread gets its own stack, its own registers, and its own program counter. The program counters are set to point to the address in the code segment where each thread should begin executing.
Step six: The OS scheduler takes over. It assigns threads to CPU cores. If you have one core, it time slices. If you have multiple cores, multiple threads can run physically at the same time.
Step seven: As threads execute, they read instructions from the code segment using their program counter, store intermediate values in their registers, create objects that land on the heap, and use their stack for local variables and method calls.
Step eight: When a thread's time slice runs out, the OS saves its register state and program counter, loads the next thread's saved state, and continues. When the first thread's turn comes again, its state is restored and it continues from exactly where it left off.
Multithreading vs Multitasking
There is a common interview question that asks you to distinguish between multithreading and multitasking. The answer is straightforward once you understand processes and threads.
Multitasking is running multiple processes at the same time. Your browser is one process. Your code editor is another process. Your music player is a third process. The OS switches between them using context switching at the process level. These processes share no memory. They cannot touch each other's variables, objects, or static data. If one crashes, the others keep running.
Multithreading is running multiple threads within a single process. Your web server might handle each incoming request in its own thread, but all those threads live inside the same JVM process. They share the heap. They share static variables. If one thread throws an unhandled exception and crashes the process, all threads go down with it.
The resource sharing situation is exactly opposite. Processes: no sharing, total isolation. Threads: share heap and static data, private stacks and registers.
Why Multithreading Is Hard: Race Conditions
Now that you understand shared heap memory, you can understand why concurrency is dangerous. Here is the classic example.
Imagine a bank account object sitting on the heap. Two threads both want to withdraw money from this account at the same time.
java
public class BankAccount {
private int balance = 1000; // This object lives on the heap
public void withdraw(int amount) {
// Step 1: Read the current balance
if (balance >= amount) {
// Step 2: Compute the new balance
// Step 3: Write the new balance back
balance = balance - amount;
}
}
}Thread one calls withdraw(800). It reads the balance: 1000. It checks: is 1000 >= 800? Yes. Right at this moment, the OS context switches to Thread two.
Thread two calls withdraw(800). It also reads the balance: 1000 (because Thread one has not written back yet). It checks: is 1000 >= 800? Yes. Thread two computes 1000 minus 800 = 200 and writes 200 back to the balance field.
Now Thread one gets its time slice back. It resumes right where it left off: at the step where it computes 1000 minus 800 = 200. It writes 200 back to the balance field.
Both withdrawals succeeded. The account started with 1000, two withdrawals of 800 each went through, and the final balance is 200. The account is now overdrawn by 600 without anyone noticing. That is a race condition.
The problem is that the three steps (read, check, write) are not atomic. Another thread can jump in between any of them. The heap is shared, so both threads can read and modify the same balance field. Without synchronization, the result depends on the timing of context switches, which is unpredictable.
This is why multithreading is hard. The bugs are not reproducible every time. They only appear under specific timing conditions. They are nearly impossible to reproduce with unit tests. They show up in production under load.
The Visibility Problem
There is another layer to thread memory that makes things even trickier. Modern CPUs have caches: L1, L2, and L3 cache. Each CPU core has its own L1 and L2 cache. When a thread reads a variable, the value might be sitting in the core's cache rather than in main memory.
Thread one on Core 1 reads the balance, modifies it, and writes the new value to its L1 cache. Thread two on Core 2 reads the balance from its own L1 cache. Core 2's cache has not been refreshed yet, so Thread two sees the old value. This is the visibility problem: a write by one thread is not immediately visible to other threads.
Java provides mechanisms to deal with this. The volatile keyword tells the JVM that a variable must always be read from and written to main memory, never cached. The synchronized keyword not only prevents concurrent access but also guarantees memory visibility: when a thread exits a synchronized block, its writes are flushed to main memory.
java
public class SharedFlag {
// Without volatile, thread two might never see thread one's write
private volatile boolean running = true;
public void stop() {
running = false; // Thread one writes this
}
public void loop() {
while (running) { // Thread two reads this
// do work
}
}
}Without volatile, the JVM is free to cache running in a CPU register. Thread two's loop might read from that cached copy forever and never see that Thread one set it to false. The program hangs. With volatile, every read goes to main memory and every write goes to main memory.
Benefits of Multithreading
Given all these dangers, why bother with multithreading at all?
The first reason is improved performance. If you have a task that can be divided into independent pieces, multiple threads can work on those pieces simultaneously. On a machine with four cores, four threads can genuinely execute at the same time, potentially reducing total execution time to a quarter.
The second reason is responsiveness. A web server that handles each request in its own thread can respond to many users at once. If one request takes a long time (maybe it is waiting for a database query), other threads keep processing other requests. The server stays responsive.
The third reason is resource utilization. When a thread is blocked waiting for I/O (reading a file, waiting for a network response), the CPU would sit idle without multithreading. With multithreading, the OS can run another thread on that CPU core while the first thread waits. You extract more useful work from the same hardware.
Challenges of Multithreading
The challenges are real and significant.
Concurrency issues like race conditions, deadlocks, and data inconsistency are all possible when multiple threads access shared state without proper synchronization. A deadlock happens when Thread one holds Lock A and waits for Lock B, while Thread two holds Lock B and waits for Lock A. Neither can proceed.
Synchronization overhead is real. Adding synchronized blocks and locks prevents race conditions but adds performance cost. Every lock acquisition and release takes time. Heavily contended locks become bottlenecks.
Testing and debugging multithreaded code is genuinely difficult. Race conditions are timing dependent. They might not appear in testing but emerge in production under high load. When they do appear, they can be nearly impossible to reproduce consistently. The act of adding logging or debugging can change the timing enough to make the bug disappear.
Common Interview Questions on This Topic
The most frequently asked question is: what is the difference between a process and a thread? The answer: a process is an instance of a program in execution, with its own isolated memory. A thread is the smallest unit of instructions executed by the CPU, and a process can contain multiple threads. Threads share the heap, code segment, and data segment, but each has its own stack, registers, and program counter.
A follow up question: what memory do threads share? Heap memory (all objects), code segment (machine code), and data segment (static variables).
Another follow up: what memory is private to each thread? Stack (local variables and method frames), registers (intermediate computation state), and program counter (next instruction address).
Then: what is a race condition? A race condition occurs when two or more threads access shared mutable state (like a heap object or static variable) concurrently, and the outcome depends on the order and timing of their execution. The bank account example is the canonical illustration.
And: what is the difference between multitasking and multithreading? Multitasking runs multiple processes that share no memory. Multithreading runs multiple threads within one process that share the heap and static variables but have private stacks.
Finally: what is context switching? Context switching is when the OS saves the current thread's register state and program counter, then loads another thread's saved state and resumes it. This allows multiple threads to share a single CPU core by taking turns. Each thread's private registers preserve its computation state between turns.
Putting It All Together
The picture is now complete. A process is an isolated execution environment with its own JVM instance and all the memory regions that come with it. A thread is a sequence of instructions executing within that process. Multiple threads share the heap and static data, which makes sharing easy but also makes race conditions possible. Each thread has its own stack, registers, and program counter, which makes them independently executable units.
Context switching lets the OS multiplex many threads onto fewer CPU cores, giving the appearance of true parallelism even when cores are outnumbered by threads. When cores are not outnumbered, threads run in genuine parallel.
Everything that comes next in multithreading: creating threads, locks, synchronized, volatile, thread states, thread pools, the Java concurrency utilities, all of it builds on this foundation. If you understand what is shared and what is private, and if you understand why shared mutable state is dangerous, you have the mental model that makes the rest of the topic make sense.