Process vs Thread
A process is an independent program in execution with its own isolated memory space, file handles, and system resources, managed by the operating system and separated from all other processes by strict boundaries. A thread is a unit of execution that lives inside a process, sharing that process's memory, heap, and resources with every other thread in the same process. Java programs run inside a JVM process; the JVM itself creates and manages threads, and every Java application starts with at least one thread — the main thread — with additional threads created by the JVM for garbage collection, JIT compilation, signal handling, and other runtime tasks. Understanding the distinction between processes and threads is the foundation for all concurrent programming in Java: it determines what is shared and what is isolated, what is fast and what is expensive, what fails independently and what fails together. This entry covers the OS-level and JVM-level model of processes and threads, the memory model that follows from the shared-versus-isolated distinction, the cost model for creation and context switching, failure isolation and its consequences, inter-process and inter-thread communication mechanisms, and the practical decision of when to use multiple processes versus multiple threads.
Memory Model — Isolation vs Sharing
// ── What each thread owns vs what it shares ──────────────────────────
// PRIVATE TO EACH THREAD (on the thread's stack):
public void threadLocalExample() {
int localVar = 42; // private — stack variable
String localRef = "hello"; // reference is private; "hello" is interned but immutable
Object localObj = new Object(); // reference private; object on heap but not shared
// No synchronization needed for any of these — no other thread can see them
}
// SHARED ACROSS ALL THREADS (on the heap):
class SharedState {
static int staticCounter = 0; // shared — static field on heap
int instanceCounter = 0; // shared — instance field on heap
List<String> sharedList = new ArrayList<>(); // shared — object on heap
void unsafeIncrement() {
staticCounter++; // NOT thread-safe — read-modify-write is not atomic
instanceCounter++; // NOT thread-safe — same issue
}
}
// ── Demonstrating shared heap — two threads, one object ──────────────
class Counter {
int value = 0;
void increment() { value++; } // unsafe — illustrating sharing, not correctness
}
Counter shared = new Counter(); // ONE object on the heap
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) shared.increment();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) shared.increment();
});
t1.start(); t2.start();
t1.join(); t2.join();
// Result is less than 2000 — lost updates due to unsynchronized shared access:
System.out.println(shared.value); // e.g., 1843 — not 2000
// ── JVM startup threads — threads you didn't create yourself ─────────
// These threads exist in every JVM process:
Thread.getAllStackTraces().keySet().forEach(t ->
System.out.println(t.getName() + " [daemon=" + t.isDaemon() + "]")
);
// main [daemon=false] — your code runs here
// Reference Handler [daemon=true] — processes reference queue (GC)
// Finalizer [daemon=true] — runs finalizers
// Signal Dispatcher [daemon=true] — handles OS signals (SIGTERM etc.)
// Notification Thread [daemon=true] — JVM internal notifications
// Common-Cleaner [daemon=true] — java.lang.ref.Cleaner
// (plus GC threads, JIT compiler threads, etc.)
// ── ProcessBuilder — launching a separate OS process ─────────────────
ProcessBuilder pb = new ProcessBuilder("java", "-version");
pb.redirectErrorStream(true);
Process proc = pb.start();
// proc is a SEPARATE OS process with its own heap, stack, and address space.
// No memory is shared between this JVM and the child process.
String output = new String(proc.getInputStream().readAllBytes());
int exitCode = proc.waitFor();
System.out.println("Child process output: " + output.trim());
System.out.println("Exit code: " + exitCode);Creation Cost, Failure Isolation, and Process vs Thread Decision
// ── Thread creation cost vs process creation cost ────────────────────
// Timing thread creation:
long start = System.nanoTime();
Thread t = new Thread(() -> {});
t.start();
t.join();
long threadNanos = System.nanoTime() - start;
System.out.printf("Thread create+start+join: %.2f ms%n", threadNanos / 1e6);
// Typical: 0.05 – 0.5 ms
// Timing process creation:
long pStart = System.nanoTime();
Process p = new ProcessBuilder("true").start(); // Unix "true" — exits immediately
p.waitFor();
long processNanos = System.nanoTime() - pStart;
System.out.printf("Process create+wait: %.2f ms%n", processNanos / 1e6);
// Typical: 2 – 20 ms (much higher for "java -version": 80 – 500 ms)
// ── Failure isolation — thread death vs process death ─────────────────
// Thread death: only that thread stops; other threads continue
Thread fragile = new Thread(() -> {
System.out.println("Thread starting");
throw new RuntimeException("Thread failed");
});
fragile.setUncaughtExceptionHandler((thread, ex) ->
System.err.println("Thread " + thread.getName() + " died: " + ex.getMessage())
);
fragile.start();
fragile.join();
// Main thread and all other threads continue after fragile dies:
System.out.println("Main thread still running"); // this always prints
// JVM-level failure kills everything — cannot recover:
// Runtime.getRuntime().halt(1); // kills the JVM process and ALL threads immediately
// OutOfMemoryError from the GC thread — no recovery possible
// ── Inter-thread communication — shared memory, fast ─────────────────
// Threads share the heap — communication is a field write + field read:
class Message { volatile String text; }
Message msg = new Message();
Thread writer = new Thread(() -> {
try { Thread.sleep(100); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
msg.text = "hello from writer";
});
Thread reader = new Thread(() -> {
while (msg.text == null) Thread.onSpinWait(); // busy-wait for demo
System.out.println("Received: " + msg.text);
});
writer.start(); reader.start();
writer.join(); reader.join();
// No serialization, no network, no system call — just a heap write and read
// ── Inter-process communication — boundaries and cost ────────────────
// Processes must serialize data to cross the boundary:
// Option 1: Files (high latency, high durability)
// Option 2: Sockets / TCP (flexible, works across machines)
// Option 3: Pipes (fast, same machine only)
// Option 4: Shared memory segments (fast, complex setup, Linux/Unix)
// Simple pipe IPC between parent and child process:
ProcessBuilder childPB = new ProcessBuilder("cat"); // echoes stdin to stdout
childPB.redirectErrorStream(true);
Process child = childPB.start();
// Parent writes to child via pipe:
child.getOutputStream().write("hello process
".getBytes());
child.getOutputStream().close();
// Parent reads child's output:
String reply = new String(child.getInputStream().readAllBytes());
System.out.println("Child replied: " + reply.trim()); // hello process
// ── Decision matrix ───────────────────────────────────────────────────
// Factor │ Threads │ Processes
// ────────────────────┼──────────────────────┼───────────────────────
// Memory sharing │ Shared heap │ Isolated (explicit IPC)
// Creation cost │ ~0.1ms │ ~5–500ms
// Communication │ Field read/write │ Serialization + IPC
// Failure isolation │ None (JVM crash = all die) │ Strong (one crash ≠ others)
// Security boundary │ None (same JVM) │ OS process separation
// Typical use │ Parallel computation │ Microservices, plugins, fault tolerance