Multithreading · Lesson 98 of 150
Thread
Learn Java threads: the main thread, extending Thread vs Runnable, start() vs run(), lifecycle states and join() for results that print the same every run.
Drive up to a highway toll plaza on a holiday weekend. If there were only one booth, every car would wait behind every other car, and the queue would stretch for kilometres. Instead the plaza opens several lanes. Each booth serves its own line of cars at the same time, and at the end of the day the manager adds up the takings from every booth.
A Java program can work the same way. By default it has one line of work, but it can open more. Each line of work is called a thread. In this guide you will see what a thread is, meet the main thread every program starts with, create threads in two ways, learn why start() and run() behave so differently, follow a thread through its lifecycle, and use join() to wait for results.
What is a Thread?
When you run java on a class, the JVM creates one thread named main and calls your main method on it. Everything you have written so far ran on that single thread. The class java.lang.Thread represents a thread, and you can create more of them.
Threads are lighter than separate programs. They share objects on the heap, so they can pass data to each other cheaply. That sharing is powerful, and it is also the reason multithreaded code needs care.
Why is Thread used?
- Doing several jobs at once. A download can continue while the screen stays responsive.
- Using every CPU core. A modern laptop has many cores. A single thread uses only one of them at a time.
- Not waiting idle. While one thread waits for a slow network or disk, another can keep working.
- Splitting big work. A large calculation can be cut into pieces, one per thread, and the results combined.
- Serving many users. Servers often handle each request on its own thread, so one slow user does not block the rest.
Real-Life Example
The toll plaza maps onto Java neatly. The plaza is the process. Each booth with its own queue is a thread with its own call stack. The shared cash room behind the booths is the heap, which every booth can reach.
The plaza manager is the main thread. She opens the booths (starts the threads). Near closing time she waits at each booth until its last car has passed, then collects that booth's total. Waiting like that is join(). Only after every booth has finished does she write the plaza total in the daily report. If she wrote the report before waiting, she would miss money that was still being counted.
Syntax
java// way 1: extend Thread and override run() class Lane extends Thread { @Override public void run() { // work for this thread } } Lane lane = new Lane(); lane.start(); // way 2: pass a Runnable to a Thread Runnable job = () -> { /* work */ }; Thread worker = new Thread(job, "worker-1"); worker.start(); worker.join(); // wait until it finishes
- A subclass of
Threadputs its work inrun(). start()asks the JVM to create a new thread, which then callsrun()on its own stack.- A
Runnableis just the task. You hand it to aThreadobject, often as a lambda. - The second constructor argument gives the thread a readable name.
join()makes the current thread wait until the other one ends. It throws the checkedInterruptedException.
How Threads Work
Every thread moves through a small set of states, available from getState().
text+-------+ | NEW | +-------+ | | start() v +----------+ | RUNNABLE |<------+ +----------+ | | | | | | busy lock, | | join(), wait() | v | | +---------+ | | | BLOCKED | | | | WAITING |--+ | +---------+ lock free | or signal | run() ends v +------------+ | TERMINATED | +------------+
A thread object that has not been started is NEW. After start() it becomes RUNNABLE, which means it is running or ready to run whenever the operating system gives it a core. If it tries to enter a synchronized block whose lock another thread holds, it becomes BLOCKED. If it calls join(), wait() or waits on a latch, it becomes WAITING (or TIMED_WAITING with a time limit). When the lock is released or the signal arrives, it returns to RUNNABLE. When run() finishes, the thread is TERMINATED and can never be started again.
The operating system decides which runnable thread gets a core and for how long. That is why the exact order of work between threads is not fixed, and why our programs collect results first and print only after join().
Example Programs
Extending Thread and joining
javaclass TollLane extends Thread { private final int[] fees; private int total; TollLane(String name, int[] fees) { super(name); this.fees = fees; } @Override public void run() { for (int fee : fees) { total += fee; } } int getTotal() { return total; } } public class TollLanes { public static void main(String[] args) throws InterruptedException { System.out.println("started by: " + Thread.currentThread().getName()); TollLane north = new TollLane("north", new int[] {60, 60, 95}); TollLane south = new TollLane("south", new int[] {95, 150}); north.start(); south.start(); north.join(); south.join(); System.out.println(north.getName() + " lane: " + north.getTotal()); System.out.println(south.getName() + " lane: " + south.getTotal()); System.out.println("plaza total: " + (north.getTotal() + south.getTotal())); } }
Output:
textstarted by: main north lane: 215 south lane: 245 plaza total: 460
Both lanes add their fees at the same time, but main prints nothing about them until both join() calls return. The joins also guarantee that main sees the finished totals.
start() versus run() with a Runnable
javapublic class StartVsRun { static String[] seenBy = new String[2]; public static void main(String[] args) throws InterruptedException { Runnable first = () -> seenBy[0] = Thread.currentThread().getName(); Runnable second = () -> seenBy[1] = Thread.currentThread().getName(); Thread direct = new Thread(first, "booth-A"); Thread real = new Thread(second, "booth-B"); direct.run(); real.start(); real.join(); System.out.println("run() ran on: " + seenBy[0]); System.out.println("start() ran on: " + seenBy[1]); System.out.println("booth-A state : " + direct.getState()); } }
Output:
textrun() ran on: main start() ran on: booth-B booth-A state : NEW
Calling run() directly is an ordinary method call, so the task ran on main and booth-A was never started. Only start() created a new thread.
Watching the lifecycle states
javaimport java.util.concurrent.CountDownLatch; public class BoothStates { static final Object barrier = new Object(); static final CountDownLatch greenLight = new CountDownLatch(1); static Thread.State selfSeen; static void waitFor(Thread t, Thread.State s) throws InterruptedException { while (t.getState() != s) { Thread.sleep(1); } } public static void main(String[] args) throws InterruptedException { Thread car = new Thread(() -> { selfSeen = Thread.currentThread().getState(); synchronized (barrier) { // the barrier is up until main lets go } try { greenLight.await(); } catch (InterruptedException e) { return; } }, "car"); System.out.println("before start : " + car.getState()); synchronized (barrier) { car.start(); waitFor(car, Thread.State.BLOCKED); System.out.println("at barrier : " + car.getState()); } waitFor(car, Thread.State.WAITING); System.out.println("at red light : " + car.getState()); greenLight.countDown(); car.join(); System.out.println("after join : " + car.getState()); System.out.println("inside run() : " + selfSeen); } }
Output:
textbefore start : NEW at barrier : BLOCKED at red light : WAITING after join : TERMINATED inside run() : RUNNABLE
main holds the barrier lock, so the car thread is BLOCKED. Once the lock is free, the car waits on the latch and is WAITING until countDown(). A thread that checks its own state always sees RUNNABLE, because it must be running to ask.
Extending Thread vs Implementing Runnable
| Point | extends Thread | implements Runnable |
|---|---|---|
| Inheritance | Uses up your one superclass | Class can still extend another |
| What it models | The worker itself | Only the task |
| Reuse | One object, one run | One task, many threads or pools |
| Lambda support | No | Yes, it is a functional interface |
| Executor support | Awkward | Works directly |
| Typical use | Tiny demos | Real applications |
Prefer Runnable. It keeps the task separate from the machinery that runs it, and the same task can later move to a thread pool unchanged.
Common Mistakes
- Starting a thread twice. A second
start()on the same object throwsIllegalThreadStateException. Create a newThreadinstead. - Reading results without `join()`. The worker may not have finished, and
mainmay not even see its latest values. - Printing from many threads at once. The lines interleave differently on every run. Collect results and print from one place.
- Sharing a counter without protection. Two threads doing
count++on the same variable can lose updates. Give each thread its own slot or use synchronization. - Ignoring `InterruptedException`. Swallowing it hides a request to stop. Return or restore the interrupt flag.
Interview Questions
What is the difference between a process and a thread?
Ans:A process is a running program with its own memory space. A thread is a path of execution inside a process; threads of one process share the heap but each has its own stack. Creating and switching threads is cheaper than doing the same with processes.
What is the difference between `start()` and `run()`?
Ans:start() asks the JVM to create a new thread, which then calls run(). Calling run() directly is a normal method call on the current thread, so no new thread is created.
Which is better, extending Thread or implementing Runnable?
Ans:Implementing Runnable is usually better. The class stays free to extend something else, the task is separated from the thread, and it works with lambdas and executor thread pools.
What does `join()` do?
Ans:It makes the calling thread wait until the target thread terminates. It also guarantees that everything the finished thread wrote to memory is visible to the thread that joined.
What are the states of a Java thread?
Ans:Thread.State has six values: NEW, RUNNABLE, BLOCKED, WAITING, TIMED_WAITING and TERMINATED. A thread moves from NEW to RUNNABLE on start(), can pause in the blocked or waiting states, and ends in TERMINATED.
Summary
- A thread is a path of execution with its own stack, sharing the heap with other threads.
- Every program begins on the thread named
main. - Create a thread by extending
Threador, better, by passing aRunnableto aThread. start()creates a new thread; callingrun()directly does not.- Threads move through NEW, RUNNABLE, BLOCKED or WAITING, and TERMINATED.
- Use
join()before reading a worker's results, and print from one thread for predictable output.
Related Topics
- Runnable: the task interface behind most real threads.
- Thread Lifecycle: every state and transition in more detail.
- Thread Methods:
sleep,join,interruptand friends. - Synchronization: keeping shared data safe between threads.
Practice Problems
Try each problem on your own first, then compare with the solution.
Easy: Parking Floor Counter
A shopping mall's parking app keeps one boolean per spot on floor 2, where true means the spot is taken. The ten spots are true, false, true, true, false, false, true, false, true, true. Start one thread named floor-2-counter that counts the free spots. After it finishes, main prints the thread's name, the free count and the taken count.
Expected output:
textfloor-2-counter done free spots: 4 taken spots: 6
Show answerHide answer
join() makes main wait until that count is complete before printing it.javapublic class ParkingCount { static int freeSpots; public static void main(String[] args) throws InterruptedException { boolean[] taken = {true, false, true, true, false, false, true, false, true, true}; Thread counter = new Thread(() -> { for (boolean t : taken) { if (!t) { freeSpots++; } } }, "floor-2-counter"); counter.start(); counter.join(); System.out.println(counter.getName() + " done"); System.out.println("free spots: " + freeSpots); System.out.println("taken spots: " + (taken.length - freeSpots)); } }
Output:
textfloor-2-counter done free spots: 4 taken spots: 6
Medium: Bakery Sales Split Across Workers
A bakery chain has 12 shop totals for the day: 120, 95, 140, 80, 110, 175, 60, 130, 90, 150, 105, 85. Split the array into three equal chunks and give each chunk to its own thread. Each thread adds up only its own chunk. main then joins the workers in order, prints each partial sum, prints the grand total, and checks it against a plain single-thread sum.
Expected output:
textworker 0: 435 worker 1: 475 worker 2: 430 total : 1340 matches : true
Show answerHide answer
main keeps the output identical on every run.javapublic class BakerySales { public static void main(String[] args) throws InterruptedException { int[] sales = {120, 95, 140, 80, 110, 175, 60, 130, 90, 150, 105, 85}; int workers = 3; int[] partial = new int[workers]; Thread[] team = new Thread[workers]; int chunk = sales.length / workers; for (int w = 0; w < workers; w++) { final int id = w; final int from = w * chunk; final int to = from + chunk; team[w] = new Thread(() -> { for (int i = from; i < to; i++) { partial[id] += sales[i]; } }); team[w].start(); } int total = 0; for (int w = 0; w < workers; w++) { team[w].join(); System.out.println("worker " + w + ": " + partial[w]); total += partial[w]; } int check = 0; for (int s : sales) { check += s; } System.out.println("total : " + total); System.out.println("matches : " + (total == check)); } }
Output:
textworker 0: 435 worker 1: 475 worker 2: 430 total : 1340 matches : true