Processes, Threads & Concurrency
Processes, Threads & Concurrency
You have already mastered the sequential building blocks of Java — OOP, generics, collections, lambdas, and streams. All of that knowledge lives in a world where one thing happens at a time, in a predictable order. Concurrency tears that assumption apart, and understanding why it does so is the essential first step before you write a single concurrent line of code.
What is a Process?
When the OS launches a Java program it creates a process: an isolated unit of execution with its own memory space (heap, stack, code segment), file handles, and OS resources. Two separate Java processes cannot read each other's memory directly. That isolation is valuable — a crash in one process does not corrupt another — but it makes sharing data expensive.
Every JVM instance you start (e.g. running java MyApp from the terminal) is one process. The ProcessBuilder API lets you spawn child processes from within Java, but that is a rare need. Most of the time you want cheaper, lighter-weight units of work that share memory.
What is a Thread?
A thread is an independent sequence of execution that lives inside a process and shares its heap with every other thread in the same process. The JVM always starts with at least one thread — the main thread, which runs main(). You can create additional threads so that multiple call stacks advance simultaneously.
Every running Java application already has several threads even if you never create one explicitly. The garbage collector, the JIT compiler, and the finaliser all run on background threads managed by the JVM. You can see them with a thread dump (jstack <pid>) at any time.
Concurrency vs Parallelism
These two words are often used interchangeably, but they mean different things:
- Concurrency is about structure: designing a program so that multiple tasks can be in progress at the same time. On a single CPU core the OS time-slices the threads — only one actually executes at each instant, but they all appear to advance. Concurrency is a software property.
- Parallelism is about execution: multiple computations physically running at the same instant on multiple CPU cores. Parallelism is a hardware property.
A concurrent program can run in parallel on a multi-core machine, but it does not have to. A sequential program cannot benefit from extra cores no matter how many it has. The distinction matters for reasoning: you reason about concurrency for correctness, and you measure parallelism for performance.
Why Concurrency is Hard
Concurrency introduces three interrelated problems that do not exist in sequential code:
1 — Race Conditions
A race condition occurs when the correctness of the program depends on the relative timing of thread execution. Consider a simple counter:
count++ compiles to three bytecode instructions: read count, add 1, write the result back. If two threads execute those three steps in an interleaved order they can both read the same stale value and each write the same incremented result — effectively losing one increment. Run this with 1,000 threads each calling increment() once and you will almost certainly get a final count less than 1,000.
2 — Memory Visibility
Modern CPUs have multi-level caches. A write by Thread A to a variable may sit in Thread A's CPU cache for an indefinite time before being flushed to main memory. Thread B, running on a different core with its own cache, may never see the update. This is a hardware optimisation that breaks naive reasoning about shared variables.
The Java Memory Model (JMM) defines exactly when one thread is guaranteed to see the writes of another. Without the right synchronisation primitives (volatile, synchronized, locks, or the utilities in java.util.concurrent) there is no such guarantee.
3 — Atomicity
An operation is atomic if it completes in a single, indivisible step from the perspective of other threads. In Java, reading and writing a 32-bit int or object reference is atomic; reading or writing a long or double is not guaranteed to be atomic on all platforms (it may happen as two 32-bit writes). Compound operations like check-then-act (e.g. if (map.containsKey(k)) map.get(k)) are never atomic, even if each individual call is.
Why Bother? The Case for Concurrency
Given these hazards, why use threads at all? Because the alternative is worse in many real systems:
- Responsiveness: A desktop or server app that blocks its main thread on I/O freezes the UI or stops accepting requests. Offloading I/O to a background thread keeps the app responsive.
- Throughput on multi-core hardware: Every modern server has 8, 32, or even 256 cores. A single-threaded Java program uses exactly one. Parallelising CPU-bound work like data processing, image encoding, or model inference can multiply throughput by the number of cores.
- Independent concerns: A web server that handles each HTTP request on a dedicated thread is a natural model — requests are logically independent, and the framework can manage the thread pool transparently.
A First Runnable Example
Here is the simplest legal Java concurrent program — two threads printing to standard output simultaneously. It shows that threads can truly interleave, producing non-deterministic output:
Run this several times and you will likely see different interleavings of "Thread A" and "Thread B" lines. Nothing is wrong — that is concurrency in action. The OS scheduler decides which thread runs when, and you have no control over that decision. Your job as a concurrent programmer is to write code that is correct regardless of which interleaving actually occurs.
The Road Ahead
This tutorial covers the full toolkit: creating threads, understanding their lifecycle, synchronising access with synchronized and volatile, using atomic variables, coordinating with wait/notify, diagnosing deadlocks, and building a thread-safe counter as a capstone. By the end you will be able to reason confidently about shared state and use Java's concurrency facilities correctly.