Cheat SheetsOperating SystemsProcesses & Threads

Processes & Threads — Cheat Sheet

Operating Systems · 12 topics. Download the PDF or the Instagram carousel and share it.

Cheat Sheet · AiCanCode.org
Processes & Threads
Operating Systems12 topicsQuick revision reference
1

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.
Java — ProcessBuilder vs Thread
// 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 finish
2

Process 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.
Java — ProcessHandle API mirrors PCB 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));
3

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.
Java — Thread.State mirrors process states
// 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());  // TERMINATED
4

Context 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.

Pseudocode — Context Switching
// Simulating context switching in pseudocode
while (true) {
    save_state(current_process);
    current_process = scheduler.get_next_process();
    load_state(current_process);
}
5

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.
Java 21 — platform thread (KLT) vs virtual thread (ULT)
// 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();
6

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.
Java — One-to-One model with thread pool
// 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());
7

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().
C pseudocode — fork/exec/wait (Unix model)
// 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 it
8

Inter-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 — ProcessBuilder pipe + PipedStream between threads
// 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();
9

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.
Java — daemon thread killed when last user thread exits
// 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-heartbeat
10

Thread 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.
Java — ThreadPoolExecutor with all 7 parameters
// 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 tasks
11

Concurrency 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.
Java — concurrent tasks on a single-threaded executor
// 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());
12

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.
Java — ProcessHandle shows 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("?")));
Learn this free with Aria, your AI tutor → AiCanCode.org/learn/operating-systems