Creating threads: Thread vs Runnable

In Lesson 12.1 you saw what threads are. Now you'll create them yourself, in the four styles real code uses: a subclass of Thread, a Runnable class, a lambda and a method reference. You'll see what each one puts on the heap, why start() and run() are very different, and which style to choose.

Module 12 · Multithreading⏱ 22 minIntermediate9 practice problems
1 The story

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.

extends Thread: job and worker in one Notifier is a Thread + does the work can't extend BaseService too implements Runnable: job handed to a worker job Runnable worker Thread the class is free to extend anything myphatshala.in
Riya

Sir, Java allows only one superclass. How do I make a thread now?

Arjun Sir

Don't make your class be a thread. Make it a job: implement Runnable. Then hand the job to a Thread, the worker.

Riya

Job and worker. And can a lambda be the job?

Arjun Sir

Yes, that's the shortest way. Let's see all four styles, and what each creates in memory.

2 Definition

What does it mean to create a thread?

Formal definition (write this in exams)

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.

In simple words

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

RunnableAn interface with one method, void run(): a piece of work.
TargetThe Runnable stored inside a Thread object.
start()Creates the new thread, which then calls run().
run()The thread's work. Calling it yourself runs it on the current thread.
Thread nameA label like "mail-1" shown in logs and thread dumps.
@OverrideAsks the compiler to check that you really override run().

extends Thread vs implements Runnable

extends Threadimplements Runnable
Your class is…a threada job
Can extend another class?No (one superclass only)Yes
Start itnew MyThread().start()new Thread(job).start()
Reuse the workNew object each timeSame job, any number of workers or a pool
Lambda possible?NoYes: () -> work()
Used in modern codeRarelyAlmost 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.

3 Types and ways to use it

Four ways to write the job

Subclass of Thread

class X extends Thread with run(). Simple, but uses up your one superclass.

old style

Runnable class

class X implements Runnable. Best when the job carries its own data (fields).

job with data

Lambda

new Thread(() -> send(otp)). Shortest; captures effectively final locals.

most common

Method reference

new Thread(Main::backup) for an existing method with no parameters.

reuse a method

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();
        }
    }
}
1. subclass of Thread 2. Runnable class 3. lambda 4. method reference

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 style

Name 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.

4 Step by step

From new Thread(job) to TERMINATED

  1. Create the job

    Runnable job = () -> send("98xxx-101");
    The lambda becomes an object on the heap that implements Runnable. Nothing runs yet.
  2. Create the worker

    Thread t = new Thread(job, "otp-1");
    A Thread object is created on the heap, storing job as its target and "otp-1" as its name. State: NEW. Still no new thread of execution.
  3. 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.
  4. The new thread calls run()

    The first frame on the new stack is Thread.run(). It calls target.run(), which runs your lambda. If you extended Thread, your own run() override is the first frame instead.
  5. run() returns

    The thread ends (TERMINATED), its stack is freed, and any thread waiting in join() wakes up. The Thread object can't be started again.
5 See it happen

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.

Creating a thread, both views 6 steps
What you see class Sender extends Thread { public void run() { send(); } } new Sender().start(); Runnable job = () -> send(); Thread t = new Thread(job); t.start(); Way 1: extend Thread Way 2: implement Runnable (a lambda) Sender IS a thread job: the work t: the worker run() was called by the new thread same job can be given to any worker or a pool Way 1: a subclass of Thread. The work goes in run(). start() runs it on a new thread. Way 2: the work is a separate Runnable. A Thread object carries it out. Job and worker stay separate. myphatshala.in
Behind the screen HEAP #1 Runnable (lambda) run() → send() #2 Thread "Thread-0" target = #1 state NEW state RUNNABLE state TERMINATED main stack main() t.start() t.join() Thread-0 stack Thread.run() #1.run() → send() stack freed new: the job object. new Thread(job): it stores the target. start(): a brand-new stack. Thread.run() calls target.run(). Both threads run at once. run() returned: thread ends. myphatshala.in
6 Real-world use

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.

Three OTP jobs, three threads 5 steps
Animated example OtpJob #1 98xxx-101 Thread otp-1 NEW RUNNABLE TERMINATED sending… sent ✓ OtpJob #2 97xxx-202 Thread otp-2 NEW RUNNABLE TERMINATED sending… sent ✓ OtpJob #3 99xxx-303 Thread otp-3 NEW RUNNABLE TERMINATED sending… sent ✓ Three jobs (Runnables). Each wrapped in a Thread. start() on all three. Messages go out together. Threads end; main reports. myphatshala.in

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.

tap “Upload”→Runnable job→worker thread

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).

job→executor.submit(job)→pooled worker

3. Scheduled tasks

Nightly backups, hourly report emails and cache refreshes are Runnables given to a scheduler that runs them at the right time.

every night 2 AM→Runnable backup→done

4. Readable logs

Production logs print the thread name on every line, such as [payment-worker-3]. Naming threads makes problems traceable.

new Thread(job, "payment-worker-3")→logs→easy debugging

Examples are simplified: real systems usually submit jobs to thread pools instead of creating raw threads.

7 Run the code

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");
    }
}
cook-1 prepares #101 pizza cook-3 prepares #103 biryani cook-2 prepares #102 dosa All 3 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. i is 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.
8 Line by line

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.

Example 1Cinema · tickets

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.

Input
seats: A4, A5, A6
Expected output
printer: ticket for seat A4
printer: ticket for seat A5
printer: ticket for seat A6
Printed 3 tickets

Approach

  1. Make PrinterThread extends Thread with a field for the seats and a counter.
  2. Override run() with the printing loop. Call super(name) to name the thread.
  3. 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.

Example 2E-commerce · emails

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.

Input
(asha@mail.in, 501), (ravi@mail.in, 502), (meena@mail.in, 503)
Expected output
sent #501 to asha@mail.in
sent #502 to ravi@mail.in
sent #503 to meena@mail.in

Approach

  1. Each EmailJob holds its own data and its own status field, so no two threads share a field.
  2. Create a Thread for each job and start it.
  3. 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.

Example 3Banking · month end

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.

Input
balances 10000, 25000, 4000; interest 4% p.a. for one month; fee Rs 50 if balance below 5000
Expected output
Interest Rs 130.00, fees Rs 50

Approach

  1. Main::interestReport fits Runnable: no parameters, returns nothing.
  2. Each report method writes its result into a static field.
  3. 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.

9 Quiz and revision

Check yourself

Quick revision

Lesson 12.2 in one screen

  • Two ways to supply work: extends Thread + override run(), or implements Runnable and pass it to new 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 calls run(). Calling run() yourself runs it on the current thread.
  • A Thread object can be started once; a Runnable job can be reused.
  • Give threads names; call setDaemon before start().
  • 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.

P1Easy

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.

Input
(none)
Expected output
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();
    }
}
Hello from t1

How it works: super(name) passes the name to the Thread constructor. Inside run(), getName() is the inherited Thread method.

P2Easy

Implement Runnable

Write Counter implements Runnable that prints 1 to 3 on one line. Run it on a thread.

Input
(none)
Expected output
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();
    }
}
1 2 3

How it works: the Runnable is the job; new Thread(job) is the worker. Counter could still extend another class if it needed to.

P3Easy

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.

Input
(none)
Expected output
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();
    }
}
beep on beeper

How it works: inside a lambda there is no getName() (it's not a Thread subclass), so use Thread.currentThread().

P4Medium

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.

Input
4, 7, 9
Expected output
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);
    }
}
4^2 = 16 7^2 = 49 9^2 = 81

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.

P5Medium

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.

Input
(none)
Expected output
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);
    }
}
run(): main start(): worker

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.)

P6Medium

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.

Input
prices 199, 349, 50
Expected output
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);
    }
}
Cart total: Rs 598

How it works: cart::total is bound to that particular cart object. The Runnable created from it calls cart.total() on the new thread.

P7Business problem

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.

Input
Shopkart 18999, BuyNow 18499, MegaMart 19250
Expected output
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);
    }
}
Shopkart: Rs 18999 BuyNow: Rs 18499 MegaMart: Rs 19250 Cheapest: BuyNow

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.

P8Business problem

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.

Input
payment id 7781, amount 1250
Expected output
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);
    }
}
INV-2026-7781 Paid Rs 1250. Ref 7781

How it works: the lambdas capture paymentId and amount, which are effectively final. Independent tasks after an event are a classic use of threads.

P9Business problem

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.

Input
stock 4, 25, 8, 13, 2
Expected output
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();
    }
}
morning-check: 3 items low evening-check: 3 items low

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.

Code review · bugFresher

“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(). Print Thread.currentThread().getName() inside the job to confirm which thread runs it.

Avoid: blaming the report code's speed before checking which thread runs it.

Design · inheritanceJunior

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; add implements Runnable, or pass a lambda like () -> sms.sendPending().
  • Better still, submit the task to an ExecutorService so threads are reused.

Avoid: copying BaseService's code into a Thread subclass.

Production · logsJunior

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 an UncaughtExceptionHandler.
  • 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.

Batch · reuseMid

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 ScheduledExecutorService to run the Runnable every 24 hours.

Avoid: catching the exception and ignoring it: the sync would silently never run again.