Processes & Threads — Cheat Sheet
Operating Systems · 12 topics. Download the PDF or the Instagram carousel and share it.
Process vs Thread
A process is an independent program in execution with its own memory space, while a thread is a lightweight unit of execution that shares memory within the same process.
- ✓A process has its own memory space; threads within a process share the heap.
- ✓Process creation (fork) is expensive; thread creation is cheap.
- ✓A thread crash can kill the entire process; a process crash is isolated.
- ✓Java threads map 1-to-1 to OS kernel threads on the HotSpot JVM.
- ✓Java 21 virtual threads are user-space threads that do not block OS threads.
- ✓Inter-process communication (IPC) requires pipes/sockets; inter-thread communication uses shared memory.
// Process: launching a separate OS process from Java
ProcessBuilder pb = new ProcessBuilder("python3", "script.py");
pb.redirectErrorStream(true);
Process process = pb.start();
int exitCode = process.waitFor(); // blocks until child process finishes
System.out.println("Exit: " + exitCode);
// Thread: lightweight execution unit within the same JVM process
Thread thread = new Thread(() -> {
System.out.println("Thread id: " + Thread.currentThread().getId());
// shares heap with parent — can read/write same objects
});
thread.start();
thread.join(); // wait for thread to finishProcess Control Block (PCB)
The PCB is a data structure maintained by the OS kernel that stores all information about a process — its state, registers, memory maps, open files, and scheduling metadata.
- ✓PCB (task_struct in Linux) is the kernel data structure representing a process.
- ✓It stores PID, state, saved registers, memory info, open files, and scheduling data.
- ✓During context switch, the OS saves the current PCB and loads the next one.
- ✓PCB resides in kernel memory — user processes cannot directly access it.
- ✓Java's ProcessHandle API provides read-only access to some PCB-equivalent fields.
// PCB fields (conceptual mapping to Java/OS concepts):
// ┌──────────────────────────────────────────────┐
// │ Process ID (PID) — unique identifier │
// │ Process State — NEW/READY/RUNNING/... │
// │ Program Counter (PC) — next instruction addr │
// │ CPU Registers — AX, BX, SP, etc. │
// │ Memory Limits — base + limit registers │
// │ Open File Table — file descriptors │
// │ I/O Status — pending I/O requests │
// │ Scheduling Info — priority, burst time │
// │ Accounting Info — CPU time, wall time │
// │ Parent PID (PPID) — who created this │
// └──────────────────────────────────────────────┘
// Reading process info from Java
long pid = ProcessHandle.current().pid();
ProcessHandle.Info info = ProcessHandle.current().info();
System.out.println("PID: " + pid);
System.out.println("Command: " + info.command().orElse("unknown"));
System.out.println("CPU time: " + info.totalCpuDuration().orElse(Duration.ZERO));Process States & Transitions
A process moves through five states — New, Ready, Running, Waiting, and Terminated — driven by OS scheduler decisions and I/O events.
- ✓Five process states: New, Ready, Running, Waiting (Blocked), Terminated.
- ✓Only one process per CPU core can be in Running state at a time.
- ✓Waiting state means the process is blocked on I/O or a synchronization event — not just waiting for CPU.
- ✓Java Thread.State adds BLOCKED, WAITING, and TIMED_WAITING as sub-states of OS Waiting.
- ✓A process in the Ready queue is runnable; it just hasn't been scheduled yet.
// State transition diagram (textual):
//
// NEW ──(admitted)──► READY ◄──────────────────────────────┐
// │ │
// (scheduler dispatch) (I/O or event complete)
// │ │
// ▼ │
// RUNNING ──(I/O or event wait)──► WAITING
// │
// (exit / error)
// │
// ▼
// TERMINATED
// Observing Java thread states (maps closely to process states)
Thread t = new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException e) {}
});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE (= Ready or Running)
t.join();
System.out.println(t.getState()); // TERMINATEDContext Switching
Context switching is the process of storing and restoring the state of a CPU so that multiple processes or threads can share a single CPU resource.
// Simulating context switching in pseudocode
while (true) {
save_state(current_process);
current_process = scheduler.get_next_process();
load_state(current_process);
}User-Level vs Kernel-Level Threads
User-level threads are managed entirely in user space by a library, while kernel-level threads are managed by the OS, enabling true parallelism at the cost of heavier context-switch overhead.
- ✓User-level threads are managed by a library in user space — fast switches, no kernel involvement, but one blocking call blocks the whole process.
- ✓Kernel-level threads are managed by the OS — true parallelism on multi-core, but heavier creation and switch cost.
- ✓Java platform threads use the one-to-one (KLT) model on HotSpot JVM.
- ✓Java 21 virtual threads implement the N:M model — millions of virtual threads on a handful of carrier kernel threads.
- ✓Virtual threads should not be pooled; create one per task for I/O-bound work.
- ✓CPU-bound tasks do not benefit from virtual threads — use platform threads equal to core count.
// Platform thread — maps 1:1 to a kernel thread (KLT)
Thread platformThread = new Thread(() -> {
// If this blocks on I/O, only THIS kernel thread is blocked
// Other platform threads continue to run in parallel
System.out.println("Kernel thread: " + Thread.currentThread().getName());
});
platformThread.start();
platformThread.join();
// Virtual thread (Java 21) — user-level thread, N:M model
Thread virtualThread = Thread.ofVirtual().name("vt-1").start(() -> {
// Blocking I/O here parks the virtual thread but RELEASES the carrier kernel thread
// The carrier thread picks up another virtual thread — true M:N multiplexing
System.out.println("Virtual thread: " + Thread.currentThread().isVirtual()); // true
});
virtualThread.join();Multithreading Models
The three multithreading models — Many-to-One, One-to-One, and Many-to-Many — define how user-level threads map to kernel threads, determining parallelism capability and scheduling flexibility.
- ✓Many-to-One: all user threads share one kernel thread — no parallelism, rarely used today.
- ✓One-to-One: each user thread = one kernel thread — true parallelism; used by Linux, Windows, and standard Java.
- ✓Many-to-Many: M user threads on N kernel threads — flexible and efficient; used by Go and Java 21 virtual threads.
- ✓Platform threads (one-to-one) are best for CPU-bound tasks equal to core count.
- ✓Virtual threads (many-to-many) are best for I/O-bound tasks where threads spend most time waiting.
- ✓Java virtual threads are not pooled — the JVM carrier pool handles multiplexing automatically.
// One-to-One: each Thread = one OS kernel thread
// Default stack size ~512KB–1MB per thread
ExecutorService pool = Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors() // match CPU cores for CPU-bound
);
List<Future<Integer>> results = new ArrayList<>();
for (int i = 0; i < 100; i++) {
int task = i;
results.add(pool.submit(() -> {
// Each task runs on a kernel thread from the pool
return task * task;
}));
}
for (Future<Integer> f : results) System.out.println(f.get());
pool.shutdown();
// Check active thread count
System.out.println("Active threads: " + Thread.activeCount());Process Creation: fork, exec, wait
Unix process creation uses fork() to duplicate the parent, exec() to replace the process image with a new program, and wait() for the parent to collect the child's exit status.
- ✓fork() creates a child process as a copy of the parent using copy-on-write pages.
- ✓exec() replaces the process image with a new program; the PID is preserved.
- ✓wait()/waitpid() collects the child's exit status and removes its PCB from the process table.
- ✓A zombie process has exited but its PCB remains because the parent hasn't called wait().
- ✓An orphan process's parent has exited; init (PID 1) adopts it and calls wait().
- ✓Java ProcessBuilder internally uses posix_spawn/fork+exec; waitFor() is equivalent to wait().
// Unix process creation (C pseudocode — conceptual)
pid_t pid = fork(); // duplicate current process
if (pid == 0) {
// Child process: replace image with "ls -la"
execl("/bin/ls", "ls", "-la", NULL);
// exec never returns on success
perror("exec failed");
exit(1);
} else if (pid > 0) {
// Parent process: wait for child to finish
int status;
waitpid(pid, &status, 0); // blocks until child exits
if (WIFEXITED(status)) {
printf("Child exited with: %d\n", WEXITSTATUS(status));
}
} else {
perror("fork failed");
}
// A zombie occurs if parent exits without calling wait()
// An orphan occurs if parent exits before child — init adopts itInter-Process Communication (IPC)
IPC mechanisms — pipes, message queues, shared memory, and sockets — allow processes to exchange data, each with different trade-offs in speed, complexity, and synchronization requirements.
- ✓Anonymous pipes are unidirectional byte streams for parent-child communication.
- ✓Named pipes (FIFOs) allow unrelated processes to communicate via the filesystem.
- ✓Message queues are kernel-managed, asynchronous, and self-synchronized.
- ✓Shared memory is the fastest IPC — zero copy — but requires explicit synchronization.
- ✓Unix domain sockets (Java 16+) are faster than TCP loopback for same-machine IPC.
- ✓TCP sockets are the only IPC mechanism that works across machines.
// Java pipe via ProcessBuilder — parent reads child's stdout
ProcessBuilder pb = new ProcessBuilder("cat", "/etc/hostname");
pb.redirectErrorStream(true);
Process child = pb.start();
// parent reads from child's stdout pipe
try (var reader = new BufferedReader(
new InputStreamReader(child.getInputStream()))) {
reader.lines().forEach(line -> System.out.println("Got: " + line));
}
child.waitFor();
// Java Pipe streams between threads (in-process analog)
PipedOutputStream pos = new PipedOutputStream();
PipedInputStream pis = new PipedInputStream(pos);
Thread writer = new Thread(() -> {
try { pos.write("hello pipe".getBytes()); pos.close(); }
catch (IOException e) { e.printStackTrace(); }
});
Thread reader2 = new Thread(() -> {
try { System.out.println(new String(pis.readAllBytes())); }
catch (IOException e) { e.printStackTrace(); }
});
writer.start(); reader2.start();
writer.join(); reader2.join();Daemon Threads
Daemon threads are background threads that serve other threads and do not prevent the JVM from exiting when all non-daemon threads have finished.
- ✓Daemon threads do not prevent JVM exit — the JVM exits when all non-daemon threads complete.
- ✓setDaemon(true) must be called before start(); calling it after throws IllegalThreadStateException.
- ✓The JVM's garbage collector, finalizer, and internal JVM threads are all daemon threads.
- ✓Never perform critical I/O or writes in a daemon thread — it may be killed abruptly.
- ✓Use daemon threads for heartbeats, monitoring, log flushing, and cache eviction.
- ✓Java's Thread.ofPlatform().daemon(true) builder API is the modern way to create daemon threads.
// Daemon thread example — JVM exits even while daemon is "running"
Thread daemon = new Thread(() -> {
while (true) {
try {
System.out.println("Daemon: heartbeat ping at " + Instant.now());
Thread.sleep(500);
} catch (InterruptedException e) {
System.out.println("Daemon interrupted — JVM shutting down");
break;
}
}
});
daemon.setDaemon(true); // MUST be called before start()
daemon.start();
// Non-daemon (user) thread
Thread userThread = new Thread(() -> {
try {
System.out.println("User thread: doing work...");
Thread.sleep(1500); // simulate 1.5 seconds of work
System.out.println("User thread: done.");
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
userThread.start();
userThread.join();
// After userThread ends, JVM exits — daemon is killed mid-heartbeatThread Pools
A thread pool maintains a set of pre-created worker threads that pick up tasks from a queue, avoiding the overhead of creating and destroying threads for every task.
- ✓Thread creation is expensive (~512KB–1MB stack + kernel thread overhead) — pools reuse threads.
- ✓ThreadPoolExecutor parameters: corePoolSize, maxPoolSize, keepAlive, queue, threadFactory, rejectionPolicy.
- ✓FixedThreadPool uses an unbounded queue — can OOM if tasks accumulate; prefer bounded queues in production.
- ✓CachedThreadPool can create unlimited threads — dangerous under sustained high load.
- ✓CallerRunsPolicy provides natural back-pressure: the producer thread runs rejected tasks, slowing submission.
- ✓Always call shutdown() or shutdownNow() to release thread resources; use awaitTermination() for graceful drain.
// ThreadPoolExecutor with all parameters explicitly configured
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // corePoolSize: always-on threads
16, // maximumPoolSize: peak threads under load
60L, TimeUnit.SECONDS, // keepAliveTime: idle extra threads live 60s
new ArrayBlockingQueue<>(1000), // bounded work queue (prevents memory blow-up)
new ThreadFactory() {
private final AtomicInteger count = new AtomicInteger(1);
public Thread newThread(Runnable r) {
Thread t = new Thread(r, "worker-" + count.getAndIncrement());
t.setDaemon(false);
return t;
}
},
new ThreadPoolExecutor.CallerRunsPolicy() // rejection: caller thread runs the task
);
// Submit a task and get a Future
Future<String> future = executor.submit(() -> {
Thread.sleep(100);
return "result-" + Thread.currentThread().getName();
});
System.out.println(future.get(5, TimeUnit.SECONDS));
// Graceful shutdown
executor.shutdown(); // stop accepting new tasks
executor.awaitTermination(30, TimeUnit.SECONDS); // wait for in-flight tasksConcurrency vs Parallelism
Concurrency is about dealing with multiple tasks at once by interleaving progress; parallelism is about doing multiple tasks simultaneously using multiple CPU cores.
- ✓Concurrency: multiple tasks making progress via interleaving — possible on a single core.
- ✓Parallelism: multiple tasks executing simultaneously — requires multiple CPU cores.
- ✓I/O-bound workloads benefit from concurrency (many threads); CPU-bound from parallelism (threads = cores).
- ✓Java parallel streams use ForkJoinPool.commonPool() with parallelism = availableProcessors() - 1.
- ✓Concurrency introduces race conditions; parallelism amplifies them — both require synchronization.
- ✓Virtual threads maximize concurrency for I/O-bound tasks without increasing parallelism.
// Concurrency on a single core — tasks interleave
// This is concurrent even if availableProcessors() == 1
ExecutorService single = Executors.newSingleThreadExecutor();
Runnable taskA = () -> {
System.out.println("Task A started on: " + Thread.currentThread().getName());
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Task A done");
};
Runnable taskB = () -> {
System.out.println("Task B started on: " + Thread.currentThread().getName());
try { Thread.sleep(100); } catch (InterruptedException e) {}
System.out.println("Task B done");
};
// Both submitted — executed concurrently in structure, sequentially on 1 thread
single.submit(taskA);
single.submit(taskB);
single.shutdown();
// Available processors — determines parallelism potential
System.out.println("Cores: " + Runtime.getRuntime().availableProcessors());Process vs Program
A program is a passive, static set of instructions stored on disk; a process is a program in active execution with its own memory space, OS resources, and lifecycle.
- ✓A program is a static file on disk (bytecode, binary); a process is that program in active execution.
- ✓A process has its own virtual address space: code, data, heap, and stack segments.
- ✓Multiple processes can run the same program — each gets an independent PID and memory space.
- ✓Process isolation prevents one crashing process from corrupting another's memory.
- ✓The JVM is itself an OS process; your Java code runs within it.
- ✓Java ProcessHandle gives runtime introspection of the JVM as an OS process.
// A Java .jar is a program (static bytecode on disk)
// The JVM process is what brings it to life
// Check the current JVM process details
ProcessHandle self = ProcessHandle.current();
System.out.println("JVM Process PID: " + self.pid());
System.out.println("Command: " + self.info().command().orElse("unknown"));
System.out.println("Start time: " + self.info().startInstant().orElse(null));
System.out.println("Total CPU duration: " + self.info().totalCpuDuration().orElse(Duration.ZERO));
// Memory segments of the JVM process (via Runtime)
Runtime rt = Runtime.getRuntime();
long heapUsed = rt.totalMemory() - rt.freeMemory();
long heapMax = rt.maxMemory();
System.out.printf("Heap used: %d MB / %d MB max%n",
heapUsed / 1_048_576, heapMax / 1_048_576);
// List all JVM processes on this machine (Java 9+)
ProcessHandle.allProcesses()
.filter(ph -> ph.info().command()
.map(cmd -> cmd.contains("java")).orElse(false))
.forEach(ph -> System.out.println("JVM process: " + ph.pid()
+ " — " + ph.info().command().orElse("?")));