Concurrency Basics

Processes, Threads & Concurrency

15 min Lesson 1 of 13

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.

Key distinction: Threads share heap memory; processes do not. That shared heap is what makes threads powerful for communication — and dangerous for correctness.

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.

Rule of thumb: Concurrency is a design problem; parallelism is a deployment advantage. Write correct concurrent code first. Parallel speedup is then essentially free on modern hardware.

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:

// WARNING: this code is intentionally broken public class UnsafeCounter { private int count = 0; public void increment() { count++; // looks atomic, but is NOT } public int get() { return count; } }

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.

// WARNING: may loop forever on some architectures public class VisibilityProblem { private boolean stop = false; // no visibility guarantee public void requestStop() { stop = true; } public void run() { while (!stop) { // Thread B may never see stop = true // busy spin } System.out.println("Stopped."); } }

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.
Concurrency bugs are the hardest bugs to reproduce. A race condition that manifests only under specific timing conditions may appear in production under load, disappear when you add a log statement (because logging changes timing), and never show up in unit tests running on a single-threaded test runner. Defensive programming from the start is far cheaper than debugging later.

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:

public class TwoThreads { public static void main(String[] args) throws InterruptedException { Thread a = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("Thread A: " + i); } }); Thread b = new Thread(() -> { for (int i = 0; i < 5; i++) { System.out.println("Thread B: " + i); } }); a.start(); // schedules Thread A with the OS b.start(); // schedules Thread B with the OS a.join(); // main thread waits for A to finish b.join(); // main thread waits for B to finish System.out.println("Both threads done."); } }

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.