Definition
- Process – an independent running program with its own address space (heap, code segment, data segment), fully isolated from other processes by the operating system.
- Thread – a lightweight unit of execution within a process that shares the process heap with other threads but maintains its own call stack for local variables and method frames.
- Isolation – if one process crashes, other processes continue running; if one thread throws an unhandled exception, the entire process (and all its threads) can terminate.
- Communication – processes communicate through IPC (Inter-Process Communication) mechanisms like sockets and pipes, which is slow; threads communicate through shared memory, which is fast but needs synchronization.
- Cost – creating a thread costs roughly 1ms and 1MB of stack space; creating a process costs roughly 100ms and 100MB of memory.
Think of a process as a separate bank branch with its own building, vault, and staff. If one branch catches fire, other branches keep operating because they are fully independent. A thread is an employee within one branch. All employees share the same vault (heap memory) and can access any drawer, but each employee keeps personal notes on their own desk (stack). If two employees update the same account register at the same time without coordinating, the register gets corrupted.
Understanding the Concepts
What is a process?
A process is an instance of a running program. The operating system gives each process its own virtual address space, which includes a heap for dynamic memory allocation, a code segment for instructions, and a data segment for global variables. Because each process has its own address space, one process cannot directly read or modify the memory of another process.
In Java, every time you launch java -jar app.jar, you start a new JVM process with its own heap, its own garbage collector, and its own set of class loaders.
package com.bank.core;
public class ProcessDemo {
public static void main(String[] args) throws Exception {
// Starting a separate OS process from Java
ProcessBuilder pb = new ProcessBuilder("java", "-jar", "settlement-service.jar");
Process process = pb.start();
// Communication with the child process uses streams (IPC)
// This is slow compared to shared memory
int exitCode = process.waitFor();
System.out.println("Settlement process exited with code: " + exitCode);
}
}
What is a thread?
A thread is a lightweight unit of execution that runs within a process. All threads in the same process share the heap memory, which means they can read and write the same objects. However, each thread has its own stack where it stores local variables, method parameters, and return addresses.
package com.bank.core;
import java.math.BigDecimal;
public class ThreadDemo {
private static BigDecimal sharedBalance = new BigDecimal("100000.00");
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
// This thread shares 'sharedBalance' with the main thread
System.out.println("Worker sees balance: " + sharedBalance);
});
worker.start();
worker.join();
System.out.println("Main sees balance: " + sharedBalance);
}
}
Worker sees balance: 100000.00
Main sees balance: 100000.00
How is memory organized differently?
The key difference lies in memory isolation.
- A process gets its own virtual address space from the OS. Its heap, stack, code, and data segments are private.
- A thread shares the process heap with all other threads in the same process but has its own stack.
The default thread stack size in HotSpot JVM is about 1MB (tunable with -Xss). If you create 1,000 threads, you consume roughly 1GB just for stacks, in addition to the shared heap.
package com.bank.core;
public class StackDemo {
public static void main(String[] args) {
// Each thread gets its own stack
for (int i = 0; i < 5; i++) {
final int threadId = i;
new Thread(() -> {
int localVar = threadId * 1000; // local variable on this thread's stack
System.out.println(Thread.currentThread().getName()
+ " localVar=" + localVar);
}, "BankWorker-" + i).start();
}
}
}
BankWorker-0 localVar=0
BankWorker-1 localVar=1000
BankWorker-2 localVar=2000
BankWorker-3 localVar=3000
BankWorker-4 localVar=4000
How does the cost compare?
| Feature | Process | Thread |
|---|---|---|
| Memory | Own address space (~100MB+) | Shares heap; own stack (~1MB) |
| Creation cost | Heavy (~100ms) | Light (~1ms) |
| Communication | IPC: pipes, sockets (slow) | Shared memory (fast) |
| Isolation | Crash-safe (others survive) | One crash kills all threads |
| Context switch | Expensive (TLB flush needed) | Cheap (same address space) |
| Typical count | Hundreds | Thousands |
Why does shared memory require synchronization?
Because threads share the heap, two threads can read and modify the same variable at the same time. Without synchronization, a race condition occurs.
package com.bank.core;
import java.math.BigDecimal;
public class RaceDemo {
private static int balance = 100000;
public static void main(String[] args) throws InterruptedException {
// WRONG: two threads modify balance without synchronization
Thread t1 = new Thread(() -> {
for (int i = 0; i < 10000; i++) balance--;
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 10000; i++) balance--;
});
t1.start(); t2.start();
t1.join(); t2.join();
// Expected: 80000. Actual: unpredictable (race condition)
System.out.println("Balance: " + balance);
}
}
Balance: 83567
The result varies on every run because balance-- is not atomic. It reads, decrements, and writes back as three separate operations. Another thread can interleave between these steps.
What happens when a thread or process crashes?
If a process crashes, other processes are unaffected because the OS isolates their memory. If one thread throws an unhandled exception, the default behavior is that the thread terminates but the JVM process continues. However, if the failed thread held a lock or was responsible for critical work, the entire application may hang or produce incorrect results.
package com.bank.core;
public class CrashDemo {
public static void main(String[] args) throws InterruptedException {
Thread worker = new Thread(() -> {
throw new RuntimeException("Settlement failed!");
});
worker.setUncaughtExceptionHandler((t, e) ->
System.out.println("Thread " + t.getName() + " crashed: " + e.getMessage()));
worker.start();
worker.join();
System.out.println("Main thread continues running.");
}
}
Thread Thread-0 crashed: Settlement failed!
Main thread continues running.
When should you use processes vs threads?
- Use separate processes when you need crash isolation. In a banking microservices architecture, the payment gateway, fraud detection, and notification services run as separate processes. If fraud detection crashes, payments continue.
- Use threads within one process when tasks need to share data frequently. An order-matching engine uses multiple threads that read and write a shared order book stored in a
ConcurrentHashMap. - Use a combination in production. One JVM process runs 200 threads for trade processing. Separate processes (connected via Kafka or gRPC) handle settlement, notification, and reporting.
What about virtual threads (Java 21)?
Java 21 introduced virtual threads (Project Loom) that are managed by the JVM rather than the OS. They are extremely lightweight: you can create millions of virtual threads because they do not each consume 1MB of OS stack. Virtual threads are ideal for IO-bound workloads like database calls and HTTP requests.
package com.bank.core;
public class VirtualThreadDemo {
public static void main(String[] args) throws InterruptedException {
Thread vt = Thread.ofVirtual().name("vt-settlement").start(() -> {
System.out.println("Running on: " + Thread.currentThread());
});
vt.join();
}
}
Running on: VirtualThread[#21,vt-settlement]/runnable@ForkJoinPool-1-worker-1
What should a senior engineer know?
- Thread stack sizing: the default 1MB per thread stack is tunable with
-Xss512k. Reducing it allows more threads but risksStackOverflowErrorfor deep call stacks. - OS thread limits: Linux limits threads per process via
/proc/sys/kernel/threads-max. A banking application with 2,000 platform threads approaches OS limits. - NUMA awareness: on multi-socket servers, threads pinned to one NUMA node access local memory faster. JVM flags like
-XX:+UseNUMAhelp. - Virtual threads (Java 21): platform threads are expensive OS resources. Virtual threads are JVM-managed and scale to millions, making them the preferred choice for IO-bound banking services.
- Container limits: in Docker/Kubernetes, thread count is bounded by cgroup CPU and memory limits, not the host OS limits.
Visual Overview
┌───────────────────────────────────────────────────────────┐ │ OPERATING SYSTEM │ │ │ │ ┌─────────────────────┐ ┌─────────────────────┐ │ │ │ Process A (JVM) │ │ Process B (JVM) │ │ │ │ Own heap, own GC │ │ Own heap, own GC │ │ │ │ │ │ │ │ │ │ ┌──────┐ ┌──────┐ │ │ ┌──────┐ ┌──────┐ │ │ │ │ │ T1 │ │ T2 │ │ │ │ T3 │ │ T4 │ │ │ │ │ │Stack │ │Stack │ │ │ │Stack │ │Stack │ │ │ │ │ └──┬───┘ └──┬───┘ │ │ └──┬───┘ └──┬───┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ │ │ ▼ ▼ │ │ │ │ ┌──────────────┐ │ │ ┌──────────────┐ │ │ │ │ │ Shared Heap │ │ │ │ Shared Heap │ │ │ │ │ │ (objects) │ │ │ │ (objects) │ │ │ │ │ └──────────────┘ │ │ └──────────────┘ │ │ │ └─────────────────────┘ └─────────────────────┘ │ │ ISOLATED ISOLATED │ │ (crash A does not (crash B does not │ │ affect B) affect A) │ └───────────────────────────────────────────────────────────┘
Within One Process:
Thread-1 Stack Thread-2 Stack Thread-3 Stack
┌────────────┐ ┌────────────┐ ┌────────────┐
│ localVar=5 │ │ localVar=9 │ │ localVar=2 │
│ methodA() │ │ methodB() │ │ methodC() │
└─────┬──────┘ └─────┬──────┘ └─────┬──────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────┐
│ SHARED HEAP │
│ Account(SAV2001, balance=100000) │
│ ConcurrentHashMap<String, Order> │
│ LinkedBlockingQueue<Trade> │
└─────────────────────────────────────────────┘
Key Takeaway
A process is an independent program with its own isolated memory space. If one process crashes, others keep running because the OS enforces memory isolation. Communication between processes requires IPC mechanisms like sockets or message queues, which adds latency. A thread is a lightweight execution unit within a process. All threads share the same heap, which makes communication instant but demands synchronization to prevent race conditions.
The cost difference matters at scale. Creating a thread takes roughly 1ms and 1MB of stack space. Creating a process takes roughly 100ms and 100MB. In a banking system, one JVM process with 200 threads can handle thousands of concurrent transactions. Separate processes (connected via Kafka) isolate critical services like settlement and fraud detection so that a crash in one does not bring down the others.
The shared memory trade-off is the central challenge of concurrent programming. Threads can read and write the same balance variable, but without synchronization, two threads debiting the same account can lose an update. Every subsequent question in this chapter addresses how to manage that shared memory safely.
Remember one sentence: a process owns its memory and is crash-isolated, while a thread shares the process heap and needs synchronization to stay safe.
---