Phase 4 · Advanced JavaModule 21~44 min read

Multithreading Fundamentals

Run code concurrently with threads — and learn to avoid race conditions and deadlocks.

What you'll learn

Modern CPUs have many cores, and modern apps do many things at once — download while rendering, handle thousands of requests. Multithreading lets your program run code concurrently. It's powerful, and it's where subtle bugs live — so we'll learn to do it safely.

By the end you'll be able to:

  • Distinguish processes from threads and create threads with Runnable
  • Understand the thread lifecycle and control it with sleep, join, and interruption
  • Recognise race conditions on shared state
  • Protect critical sections with synchronized
  • Ensure visibility with volatile and atomicity with atomic classes
  • Avoid deadlock, livelock, and starvation

Processes & threads

A process is a running program with its own memory. A thread is a lightweight path of execution inside a process — and threads in the same process share memory, which is exactly what makes them fast and dangerous. Your programs already have one thread (the one running main); now you'll create more.

The cleanest way is to pass a Runnable (a lambda!) to a Thread and call start(). Crucially, start() launches a new thread, while calling run() directly would just run it on the current thread:

Threads.java
public class Main {
    public static void main(String[] args) throws InterruptedException {
        Runnable task = () -> {
            for (int i = 1; i <= 3; i++) {
                System.out.println(Thread.currentThread().getName() + ": " + i);
            }
        };

        Thread worker = new Thread(task, "Worker-1");
        worker.start();      // runs concurrently — NOT worker.run()
        worker.join();       // main waits for worker to finish
        System.out.println("All done");
    }
}

Watch out

Always call start(), never run(), to actually get concurrency. And remember: with multiple threads, output order is not guaranteed — the scheduler decides.

Lifecycle & control

A thread moves through several states during its life:

Thread lifecycle (simplified)
NEW→RUNNABLE→BLOCKED / WAITING→TERMINATED

You control threads with a few key methods:

  • Thread.sleep(ms) — pause the current thread for a while
  • thread.join() — wait for another thread to finish
  • thread.interrupt() — politely signal a thread to stop (it must cooperate)

Race conditions

Here's the core danger. When multiple threads read and write the same data without coordination, they can interleave in bad ways. Even count++ is not one step — it's read, add, write — so two threads can clobber each other's update:

Race.java
class Counter {
    int count = 0;
    void increment() {
        count++;   // read, add, write — THREE steps, not one!
    }
}

// If two threads call increment() at once, they can read the same
// value, both add 1, and both write it back — a lost update.
// Running two threads 10,000 times each often yields < 20,000.

Key idea

A race condition is when the result depends on the unpredictable timing of threads. These bugs are nasty: they appear intermittently and often vanish when you add logging. Prevention beats debugging.

Critical sections & synchronized

The fix is to make the shared operation a critical section that only one thread enters at a time. The synchronized keyword does this using the object's intrinsic lock — a thread must acquire the lock to run the method, and others wait their turn:

Synchronized.java
class Counter {
    private int count = 0;

    // 'synchronized' lets only ONE thread run this at a time
    public synchronized void increment() { count++; }
    public synchronized int get() { return count; }
}

Note

You can synchronize a whole method or just a block: synchronized (lockObject) { ... }. Keep critical sections small — locking too much serialises your program and kills the benefit of threads.

volatile & atomic classes

Beyond mutual exclusion, threads can cache values, so one thread may not see another's change. volatile guarantees that reads and writes of a variable always go to main memory — ensuring visibility (but not atomicity of compound operations).

For counters and flags, the java.util.concurrent.atomic classes (like AtomicInteger) give lock-free, thread-safe operations that are both atomic and visible — often faster than synchronizing:

Atomic.java
import java.util.concurrent.atomic.AtomicInteger;

AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();   // atomic ++
count.addAndGet(5);        // atomic += 5

System.out.println(count.get());

Concurrency hazards

Coordinating threads introduces its own failure modes:

  • Deadlock — two threads each hold a lock the other needs, so both wait forever
  • Livelock — threads keep reacting to each other and make no progress
  • Starvation — a thread never gets CPU time or a lock it needs

Thread-safety principles

Prefer immutable objects (they're automatically thread-safe), minimise shared mutable state, always acquire multiple locks in the same order (prevents deadlock), and lean on the high-level tools coming in Module 22 rather than raw threads.

Recap & quick check

Key takeaways

  • A thread is a concurrent path of execution inside a process; threads share memory.
  • Create threads with a Runnable and start() — never run(), and don't rely on output order.
  • Race conditions happen when threads touch shared mutable state without coordination.
  • synchronized creates a critical section only one thread enters at a time.
  • volatile ensures visibility; atomic classes give lock-free thread-safe operations; immutability is safest.

Quick check

1. Which method actually starts a new thread?

2. What is a race condition?

3. What does the synchronized keyword provide?

4. What does volatile guarantee?

5. Which is the safest way to avoid concurrency bugs?

Great — you understand concurrency's power and its pitfalls. Next up: Module 22 — Advanced Concurrency, with the high-level tools that make it manageable.