The job and the worker
Riya wants her canteen app to send order-ready notifications in the background. She writes class Notifier extends Thread, but her class already extends BaseService, and the compiler refuses.
Sir, Java allows only one superclass. How do I make a thread now?
Don't make your class be a thread. Make it a job: implement Runnable. Then hand the job to a Thread, the worker.
Job and worker. And can a lambda be the job?
Yes, that's the shortest way. Let's see all four styles, and what each creates in memory.
What does it mean to create a thread?
In Java a thread is represented by an object of class java.lang.Thread. Its work is the code of its run() method. You supply that work either by extending Thread and overriding run(), or by passing an object that implements the functional interface java.lang.Runnable (method void run()) to a Thread constructor, where it is stored as the thread's target. Calling start() makes the JVM create a new thread of execution that invokes run(); calling run() directly is an ordinary method call on the current thread.
The Runnable is the job card. The Thread is the worker. start() is “go!”. Without start(), no new worker ever begins.
Words you'll meet in this lesson
void run(): a piece of work.run()."mail-1" shown in logs and thread dumps.run().extends Thread vs implements Runnable
| extends Thread | implements Runnable | |
|---|---|---|
| Your class is… | a thread | a job |
| Can extend another class? | No (one superclass only) | Yes |
| Start it | new MyThread().start() | new Thread(job).start() |
| Reuse the work | New object each time | Same job, any number of workers or a pool |
| Lambda possible? | No | Yes: () -> work() |
| Used in modern code | Rarely | Almost always (with executors, Lesson 12.6) |
Exam tip: “Implementing Runnable is preferred because Java supports single inheritance of classes, and it separates the task from the thread that runs it.” And: calling run() does not create a thread; only start() does.
Four ways to write the job
Subclass of Thread
class X extends Thread with run(). Simple, but uses up your one superclass.
Runnable class
class X implements Runnable. Best when the job carries its own data (fields).
Lambda
new Thread(() -> send(otp)). Shortest; captures effectively final locals.
Method reference
new Thread(Main::backup) for an existing method with no parameters.
All four, one program
class Packer extends Thread {
public void run() {
System.out.println("1. subclass of Thread");
}
}
class Labeller implements Runnable {
public void run() {
System.out.println("2. Runnable class");
}
}
public class Main {
static void ship() {
System.out.println("4. method reference");
}
public static void main(String[] args) throws InterruptedException {
Thread[] ts = {
new Packer(),
new Thread(new Labeller()),
new Thread(() -> System.out.println("3. lambda")),
new Thread(Main::ship)
};
for (Thread t : ts) {
t.start();
t.join();
}
}
}Each thread is joined before the next one starts, so the lines always come out in order. Normally you'd start them all first and join afterwards.
Thread constructors you'll use
new Thread(Runnable task) // default name Thread-N
new Thread(Runnable task, String name) // recommended: a meaningful name
new Thread(String name) // for subclasses that override run()
Thread.ofPlatform().name("worker").start(task) // Java 21 builder styleName your threads. When a production server has 200 threads called Thread-37, nobody can read the logs. new Thread(job, "invoice-mailer") costs nothing and saves hours.
From new Thread(job) to TERMINATED
Create the job
The lambda becomes an object on the heap that implementsRunnable job = () -> send("98xxx-101");Runnable. Nothing runs yet.Create the worker
A Thread object is created on the heap, storingThread t = new Thread(job, "otp-1");jobas its target and "otp-1" as its name. State: NEW. Still no new thread of execution.start()
The JVM asks the OS for a real thread, gives it its own stack, and marks it RUNNABLE.start()returns at once; main does not wait.The new thread calls run()
The first frame on the new stack isThread.run(). It callstarget.run(), which runs your lambda. If you extended Thread, your ownrun()override is the first frame instead.run() returns
The thread ends (TERMINATED), its stack is freed, and any thread waiting injoin()wakes up. The Thread object can't be started again.
Job and worker: both views
Left: the code you write in each style. Right: the objects on the heap and the new stack that start() creates.
- Way 1: write a class that
extends Thread. - Override
run(): whatever is inside it is the thread's work. start()creates the new thread, and that thread callsrun(). You never callrun()yourself.- Way 2: put the work in a
Runnable, an interface with one methodrun(). A lambda is the shortest way to write one. - Hand the Runnable to
new Thread(job). The Thread is the worker; the Runnable is the job. - Separating job and worker is the preferred style: your class stays free to extend something else, and the same job can later go to a thread pool (Lesson 12.6).
- The lambda becomes a Runnable object (#1) on the heap.
new Thread(job)creates a Thread object (#2). It stores a reference to the job in its target field. State: NEW.start()creates a new thread with its own stack. The first frame on that stack isThread.run().Thread.run()checks its target and callstarget.run(), which callssend(). (If you extend Thread, your overridingrun()is called instead.)- Meanwhile
maincarries on with its own stack, here waiting injoin(). - When
run()returns, the thread ends: its stack is freed and the Thread object's state becomes TERMINATED. The objects stay on the heap until no longer referenced.
Where these styles show up
In real projects you'll mostly write the job (a Runnable or lambda) and let something else provide the worker.
Watch it with an example: OTP messages for three customers
A payments app must send one-time passwords to three customers at once. Each message is a Runnable job; each job gets its own Thread.
- For each customer the app creates an
OtpJob, a class thatimplements Runnableand stores the phone number. - Each job is handed to its own
new Thread(job, "otp-1"). The arrows show each Thread's target. start()is called on each Thread. The loop doesn't wait: all three are running within microseconds.- The SMS gateway calls overlap, so three messages take about as long as one. (Illustrative.)
- After
join()on each,mainknows all three are sent and can show “OTPs sent” to the operator.
1. Background jobs in apps
Upload a photo, refresh a feed, sync contacts: each is written as a small Runnable and handed to a background worker.
2. Thread pools and executors
Executors accept Runnables (and Callables), not Thread subclasses. That's the main reason the Runnable style won (Lesson 12.6).
3. Scheduled tasks
Nightly backups, hourly report emails and cache refreshes are Runnables given to a scheduler that runs them at the right time.
4. Readable logs
Production logs print the thread name on every line, such as [payment-worker-3]. Naming threads makes problems traceable.
Examples are simplified: real systems usually submit jobs to thread pools instead of creating raw threads.
Three cooks, three orders
public class Main {
static String[] orders = {"#101 pizza", "#102 dosa", "#103 biryani"};
static void prepare(String order) {
System.out.println(Thread.currentThread().getName() + " prepares " + order);
}
public static void main(String[] args) throws InterruptedException {
Thread[] cooks = new Thread[orders.length];
for (int i = 0; i < orders.length; i++) {
String order = orders[i];
cooks[i] = new Thread(() -> prepare(order), "cook-" + (i + 1));
cooks[i].start();
}
for (Thread c : cooks) {
c.join();
}
System.out.println("All " + cooks.length + " orders ready");
}
}The “prepares” lines can appear in any order, because the threads run at the same time. The last line is fixed because main joins every cook first.
String order = orders[i];A fresh local per loop pass, so the lambda can capture it (it never changes).new Thread(() -> prepare(order), "cook-" + (i + 1))Job (lambda) plus name.iis used outside the lambda, so that's allowed.cooks[i].start()Each cook starts at once; the loop moves on without waiting.c.join()Waits for each cook before printing the summary.
A Runnable job filled in by another thread
Recorded from the real JVM, following the main thread. Watch two separate objects appear: the Greeter job and the Thread worker that holds it as its target.
Worked examples: real problems, solved and traced in memory
Three styles you'll see in real code: a subclass of Thread, a Runnable class that carries its own data, and a method reference. Each trace follows main: watch the job objects and Thread objects on the heap.
A ticket-printer thread by extending Thread
Problem statement
A cinema kiosk prints tickets on a printer thread so the touch screen stays free. Write a PrinterThread class that extends Thread, holds the seat numbers, and prints one ticket line per seat. Count how many were printed.
seats: A4, A5, A6
printer: ticket for seat A4 printer: ticket for seat A5 printer: ticket for seat A6 Printed 3 tickets
Approach
- Make
PrinterThread extends Threadwith a field for the seats and a counter. - Override
run()with the printing loop. Callsuper(name)to name the thread. - Create it,
start()it,join()it, then read the counter from main.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Order-confirmation emails as Runnable jobs
Problem statement
After a sale, a shop must send confirmation emails to three customers. Write an EmailJob implements Runnable that stores the address and order id, and records a status after “sending”. Run each job on its own thread and print all statuses in order.
(asha@mail.in, 501), (ravi@mail.in, 502), (meena@mail.in, 503)
sent #501 to asha@mail.in sent #502 to ravi@mail.in sent #503 to meena@mail.in
Approach
- Each
EmailJobholds its own data and its ownstatusfield, so no two threads share a field. - Create a Thread for each job and start it.
- Join all threads, then print the statuses from the job objects.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Two reports with method references
Problem statement
At month end a bank builds an interest report and a fee report. The methods already exist in Main. Run each method on its own thread using a method reference instead of writing a Runnable class.
balances 10000, 25000, 4000; interest 4% p.a. for one month; fee Rs 50 if balance below 5000
Interest Rs 130.00, fees Rs 50
Approach
Main::interestReportfitsRunnable: no parameters, returns nothing.- Each report method writes its result into a static field.
- Start both threads, join both, print both results.
Solution: run it live
What happens in memory, step by step
Recorded from the real JVM: press Next to execute one line at a time. Watch method calls push and pop frames on the stack, and objects (numbered #1, #2 …) appear on the heap.
Check yourself
Quick revision
Lesson 12.2 in one screen
- Two ways to supply work:
extends Thread+ overriderun(), orimplements Runnableand pass it tonew Thread(job). - Runnable is a functional interface: write it as a lambda or a method reference.
- Prefer Runnable: keeps your one superclass free and separates job from worker.
start()creates a new thread, which callsrun(). Callingrun()yourself runs it on the current thread.- A Thread object can be started once; a Runnable job can be reused.
- Give threads names; call
setDaemonbeforestart(). run()can't throw checked exceptions: catch them inside.
Interview and viva questions
What are the ways to create a thread in Java?
Extend Thread and override run(), or implement Runnable (as a class, lambda or method reference) and pass it to a Thread constructor. For tasks that return a value, implement Callable and submit it to an ExecutorService (Lesson 12.6). In every case, start() begins execution.
Which is better: extending Thread or implementing Runnable?
Implementing Runnable. Java has single inheritance, so extending Thread blocks your class from extending anything else. Runnable also separates the task from the execution mechanism, so the same task can run on a raw thread, a pool or a scheduler.
What is the difference between start() and run()?
start() asks the JVM to create a new thread with its own stack; that thread then calls run(). Calling run() directly is a normal method call executed by the current thread; no new thread is created.
Can we call start() twice on the same Thread?
No. The second call throws IllegalThreadStateException. Create a new Thread object (you can reuse the same Runnable).
Can we override start()?
You technically can, but you shouldn't: if your override doesn't call super.start(), no thread is created. Override run() only.
Practice problems
Create threads in all three styles: extending Thread, implementing Runnable, and lambdas or method references. Each solution joins before printing, so its output is fixed. The last three are business tasks.
Extend Thread
Write a class Hello extends Thread whose run() prints Hello from followed by its thread name. Start one named "t1" and wait for it.
(none)
Hello from t1
Show solution and explanation
class Hello extends Thread {
Hello(String name) {
super(name);
}
public void run() {
System.out.println("Hello from " + getName());
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Hello h = new Hello("t1");
h.start();
h.join();
}
}How it works: super(name) passes the name to the Thread constructor. Inside run(), getName() is the inherited Thread method.
Implement Runnable
Write Counter implements Runnable that prints 1 to 3 on one line. Run it on a thread.
(none)
1 2 3
Show solution and explanation
class Counter implements Runnable {
public void run() {
for (int i = 1; i <= 3; i++) System.out.print(i + " ");
System.out.println();
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Counter());
t.start();
t.join();
}
}How it works: the Runnable is the job; new Thread(job) is the worker. Counter could still extend another class if it needed to.
Lambda Runnable with a name
Create a thread with a lambda and the name "beeper". The lambda prints beep on and the current thread's name.
(none)
beep on beeper
Show solution and explanation
public class Main {
public static void main(String[] args) throws InterruptedException {
Runnable beep = () -> System.out.println("beep on " + Thread.currentThread().getName());
Thread t = new Thread(beep, "beeper");
t.start();
t.join();
}
}How it works: inside a lambda there is no getName() (it's not a Thread subclass), so use Thread.currentThread().
One job class, three threads
Write Square implements Runnable that stores a number and computes its square into a field. Run it for 4, 7 and 9 on three threads and print the results in input order.
4, 7, 9
4^2 = 16 7^2 = 49 9^2 = 81
Show solution and explanation
class Square implements Runnable {
int n;
int result;
Square(int n) {
this.n = n;
}
public void run() {
result = n * n;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Square[] jobs = {new Square(4), new Square(7), new Square(9)};
Thread[] ts = new Thread[3];
for (int i = 0; i < 3; i++) {
ts[i] = new Thread(jobs[i]);
ts[i].start();
}
for (Thread t : ts) t.join();
for (Square s : jobs) System.out.println(s.n + "^2 = " + s.result);
}
}How it works: each job object carries its own input and output, so the threads never share a field. Printing from the jobs array keeps the input order.
run() vs start(): who ran it?
Store the name of the thread that executed a Runnable. Call it once with run() and once through start(), and print both names.
(none)
run(): main start(): worker
Show solution and explanation
public class Main {
static String ranOn;
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> ranOn = Thread.currentThread().getName(), "worker");
t.run();
System.out.println("run(): " + ranOn);
t.start();
t.join();
System.out.println("start(): " + ranOn);
}
}How it works: t.run() is an ordinary method call, so it runs on main. Only start() creates the new thread named worker. (Calling run() first doesn't count as starting, so start() is still allowed.)
Method reference to an instance method
A Cart object has a method total() that adds its prices into a field. Run cart::total on a thread and print the total.
prices 199, 349, 50
Cart total: Rs 598
Show solution and explanation
class Cart {
int[] prices = {199, 349, 50};
int sum;
void total() {
for (int p : prices) sum += p;
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Cart cart = new Cart();
Thread t = new Thread(cart::total);
t.start();
t.join();
System.out.println("Cart total: Rs " + cart.sum);
}
}How it works: cart::total is bound to that particular cart object. The Runnable created from it calls cart.total() on the new thread.
Parallel price check across three sellers
A comparison site asks three sellers for the price of a phone. Each Quote job looks up its seller's price (from a fixed table) on its own thread. Print each price and the cheapest seller.
Shopkart 18999, BuyNow 18499, MegaMart 19250
Shopkart: Rs 18999 BuyNow: Rs 18499 MegaMart: Rs 19250 Cheapest: BuyNow
Show solution and explanation
class Quote implements Runnable {
String seller;
int price;
Quote(String seller) {
this.seller = seller;
}
public void run() {
price = switch (seller) {
case "Shopkart" -> 18999;
case "BuyNow" -> 18499;
default -> 19250;
};
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Quote[] qs = {new Quote("Shopkart"), new Quote("BuyNow"), new Quote("MegaMart")};
Thread[] ts = new Thread[qs.length];
for (int i = 0; i < qs.length; i++) {
ts[i] = new Thread(qs[i], qs[i].seller);
ts[i].start();
}
for (Thread t : ts) t.join();
Quote best = qs[0];
for (Quote q : qs) {
System.out.println(q.seller + ": Rs " + q.price);
if (q.price < best.price) best = q;
}
System.out.println("Cheapest: " + best.seller);
}
}How it works: in a real site each lookup is a slow network call, so running them on separate threads makes the page about as fast as the slowest seller. The comparison happens on main after all joins.
Invoice and SMS after a payment
After a payment of Rs 1,250 succeeds, two independent tasks must run: generate the invoice number and prepare the SMS text. Run each as a lambda on its own thread, then print both.
payment id 7781, amount 1250
INV-2026-7781 Paid Rs 1250. Ref 7781
Show solution and explanation
public class Main {
static String invoice;
static String sms;
public static void main(String[] args) throws InterruptedException {
int paymentId = 7781;
int amount = 1250;
Thread inv = new Thread(() -> invoice = "INV-2026-" + paymentId, "invoice");
Thread msg = new Thread(() -> sms = "Paid Rs " + amount + ". Ref " + paymentId, "sms");
inv.start();
msg.start();
inv.join();
msg.join();
System.out.println(invoice);
System.out.println(sms);
}
}How it works: the lambdas capture paymentId and amount, which are effectively final. Independent tasks after an event are a classic use of threads.
Reusable job, different workers
A StockCheck job counts items below 10 units. Run the same job object twice, on two different threads one after the other, and show it ran on each.
stock 4, 25, 8, 13, 2
morning-check: 3 items low evening-check: 3 items low
Show solution and explanation
class StockCheck implements Runnable {
int[] stock = {4, 25, 8, 13, 2};
public void run() {
int low = 0;
for (int s : stock) if (s < 10) low++;
System.out.println(Thread.currentThread().getName() + ": " + low + " items low");
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
StockCheck job = new StockCheck();
Thread morning = new Thread(job, "morning-check");
morning.start();
morning.join();
Thread evening = new Thread(job, "evening-check");
evening.start();
evening.join();
}
}How it works: a Thread can be started only once, but a Runnable job can be given to as many workers as you like. low is a local variable, so each run has its own copy on its own stack.
Real-world scenario questions (asked in company interviews)
These come up when interviewers want to see whether you can read and fix real thread-creation code.
“The background thread is slow”
A teammate says: “I moved the report generation to a background thread, but the screen still freezes.” The code is new Thread(reportJob).run();. What's wrong?
The interviewer is checking: start() vs run().
Show a strong answer
run()is a plain method call, so the report still runs on the UI thread.- Use
start(). PrintThread.currentThread().getName()inside the job to confirm which thread runs it.
Avoid: blaming the report code's speed before checking which thread runs it.
A service class that must run in the background
SmsService extends BaseService and must send messages on a background thread. A colleague suggests changing it to extends Thread. What do you propose?
The interviewer is checking: single inheritance and separating task from thread.
Show a strong answer
- Keep
extends BaseService; addimplements Runnable, or pass a lambda like() -> sms.sendPending(). - Better still, submit the task to an
ExecutorServiceso threads are reused.
Avoid: copying BaseService's code into a Thread subclass.
Which thread failed?
At 2 AM the logs show Exception in thread "Thread-147" java.lang.NullPointerException. The app creates threads in twelve places. How do you make the next incident easier to diagnose?
The interviewer is checking: naming threads and handling uncaught exceptions.
Show a strong answer
- Give every thread a meaningful name (
"invoice-mailer"), or use a thread factory that names pool threads. - Catch and log exceptions inside
run()with context (order id, customer id), or set anUncaughtExceptionHandler. - Remember an uncaught exception kills only that thread; other threads continue, so failures can go unnoticed.
Avoid: wrapping run() in an empty catch block; that hides the problem completely.
Restarting a finished job
A nightly SyncThread extends Thread object is stored in a field. The next night the code calls syncThread.start() again and the job crashes with IllegalThreadStateException. How would you redesign it?
The interviewer is checking: thread lifecycle and reusable tasks.
Show a strong answer
- A thread can't be restarted once TERMINATED. Store the job (a Runnable), not the thread.
- Create a new Thread each night, or better, use a
ScheduledExecutorServiceto run the Runnable every 24 hours.
Avoid: catching the exception and ignoring it: the sync would silently never run again.