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
volatileand 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:
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
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:
You control threads with a few key methods:
Thread.sleep(ms)— pause the current thread for a whilethread.join()— wait for another thread to finishthread.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:
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
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:
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
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:
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
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.