One Worker Is Not Enough

Priya built QuickCart, a small online shop for her college. At first, one customer at a time was fine. Then the campus sale started. Orders piled up. The server handled one request, then the next, then the next — and people waited. Priya needs more than one worker at the counter.

Process vs thread

A process is a whole program running on your computer. It has its own memory. Think of it as one shop building.

A thread is a worker inside that building. All threads in the same process share the same memory. They can read the same variables.

Concurrency means many tasks make progress over time. On one CPU, threads take turns — it looks like things happen at once.

Parallelism means tasks really run at the same time, on different CPU cores. Two cores can run two threads at the exact same moment.

QuickCart runs in one process. Priya wants many threads inside it so many orders can move forward together.

Thread vs Runnable

Java gives you two ways to start worker code.

Method 1: Extend Thread

You can extend the Thread class:

public class OrderWorker extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running: " + Thread.currentThread().getName());
        // Your order logic here
    }
}

OrderWorker thread = new OrderWorker();
thread.start();

Pros: simple, direct access to Thread methods.

Cons: Java only lets a class extend one parent. If you extend Thread, you cannot extend anything else. Less flexible.

The better way is Runnable — a small job description that any thread can run:

public class ProcessOrderTask implements Runnable {
    private final String orderId;

    public ProcessOrderTask(String orderId) {
        this.orderId = orderId;
    }

    @Override
    public void run() {
        System.out.println("Processing order " + orderId
            + " on " + Thread.currentThread().getName());
    }
}

Thread thread = new Thread(new ProcessOrderTask("ORD-101"));
thread.start();

Pros: can extend other classes, better separation of concerns, works with ExecutorService later.

Cons: slightly more verbose.

Method 3: Lambda (Java 8+)

Thread thread = new Thread(() -> {
    System.out.println("Thread running: " + Thread.currentThread().getName());
});
thread.start();

Pros: concise, modern style.

Cons: less reusable if you need the same task in many places.

Prefer Runnable. It keeps “what to do” separate from “how to run it.” Later you can hand the same Runnable to an ExecutorService — a thread pool Priya will use in the next chapter.

start() vs run()

This trips up almost everyone.

  • start() — creates a new thread and runs your code there.
  • run() — runs your code in the current thread. No new worker. No extra speed.
thread.start(); // Creates new thread
thread.run();   // Runs in current thread — wrong for multithreading!

Calling start() twice on the same thread throws IllegalThreadStateException. A thread lives once:

Thread thread = new Thread(() -> processOrder("ORD-1"));
thread.start(); // OK
thread.start(); // IllegalThreadStateException — already started or finished

If you never call start(), the thread stays in NEW state forever. Your code never runs on a background worker.

Thread lifecycle

A thread moves through states:

NEW → RUNNABLE → BLOCKED / WAITING / TIMED_WAITING → TERMINATED

States explained

  1. NEW — created but not started yet.
Thread thread = new Thread(() -> processOrder("ORD-1")); // NEW
  1. RUNNABLE — running or ready to run.
thread.start(); // Moves to RUNNABLE
  1. BLOCKED — waiting for a lock another thread holds.
synchronized (lock) {
    // Another thread waiting here is BLOCKED
}
  1. WAITING — waiting for another thread, with no time limit.
lock.wait(); // WAITING state (must be inside synchronized block)
  1. TIMED_WAITING — waiting with a timeout.
Thread.sleep(1000);  // TIMED_WAITING
lock.wait(1000);     // TIMED_WAITING
thread.join(5000);   // TIMED_WAITING
  1. TERMINATED — finished. Happens after run() completes.

Checking thread state

Thread.State state = thread.getState();
switch (state) {
    case NEW:
        System.out.println("Thread not started");
        break;
    case RUNNABLE:
        System.out.println("Thread running or ready");
        break;
    case BLOCKED:
        System.out.println("Thread blocked on a lock");
        break;
    case WAITING:
        System.out.println("Thread waiting");
        break;
    case TIMED_WAITING:
        System.out.println("Thread timed waiting");
        break;
    case TERMINATED:
        System.out.println("Thread finished");
        break;
}

Key Thread methods

currentThread(), setName(), getName()

Name threads so logs make sense during a sale:

Thread worker = new Thread(() -> processOrder("ORD-1"), "OrderWorker-ORD-1");
worker.setName("QuickCart-Worker-1");
System.out.println(worker.getName());

Thread current = Thread.currentThread();
System.out.println("Current thread: " + current.getName());

join() — wait for another thread to finish

Thread worker = new Thread(() -> processOrder("ORD-1"));
worker.start();
worker.join(); // Current thread waits until worker is done
System.out.println("Order processed");

join with timeout — wait at most N milliseconds:

worker.join(5000);
if (worker.isAlive()) {
    System.out.println("Still working after 5 seconds...");
}

sleep() — pause the current thread

try {
    Thread.sleep(1000); // Sleep for 1 second
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // Restore the flag
}

sleep() does not release locks. The thread just pauses.

interrupt(), isInterrupted() — ask a thread to stop

worker.interrupt(); // Sets interrupt flag

// Inside the worker:
if (Thread.currentThread().isInterrupted()) {
    return; // Stop gracefully
}

Never ignore InterruptedException. Always call Thread.currentThread().interrupt() in the catch block so the flag is not lost:

try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
    // Then exit or handle as needed
}

setPriority() / getPriority() — a hint only

worker.setPriority(Thread.MAX_PRIORITY);  // 10
worker.setPriority(Thread.NORM_PRIORITY); // 5 (default)
worker.setPriority(Thread.MIN_PRIORITY);  // 1

Priority is a hint to the scheduler, not a guarantee. Do not rely on it for correctness.

Common patterns

Pattern 1: Interrupt handling in a loop

Background workers should check the interrupt flag:

public class OrderPoller implements Runnable {
    @Override
    public void run() {
        while (!Thread.currentThread().isInterrupted()) {
            try {
                pollForNewOrders();
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break; // Exit loop
            }
        }
    }

    private void pollForNewOrders() {
        // Check order queue...
    }
}

Pattern 2: Daemon threads

A daemon thread runs in the background. When only daemon threads remain, the JVM exits:

Thread daemonThread = new Thread(() -> {
    while (true) {
        // Background cleanup or monitoring
    }
});
daemonThread.setDaemon(true);
daemonThread.start();

Use daemon threads for background tasks like monitoring or logging — not for work that must finish before the app shuts down.

Pattern 3: Thread grouping

Group related workers so you can interrupt them together:

ThreadGroup group = new ThreadGroup("QuickCartWorkers");
Thread worker1 = new Thread(group, () -> processOrder("ORD-1"), "Worker-1");
Thread worker2 = new Thread(group, () -> processOrder("ORD-2"), "Worker-2");
worker1.start();
worker2.start();

// Later: interrupt all workers in the group
group.interrupt();

Best practices

Do

  1. Implement Runnable instead of extending Thread.
// Good
public class ProcessOrderTask implements Runnable { }

// Avoid
public class ProcessOrderTask extends Thread { }
  1. Handle InterruptedException properly — restore the interrupt flag.

  2. Name your threads for easier debugging.

  3. Use join() when you need to wait for workers to finish.

  4. Check interrupt status in loops that should stop cleanly.

Do not

  1. Do not call run() directly — use start().

  2. Do not rely on thread priorities for correctness.

  3. Do not use stop(), suspend(), resume() — they are deprecated and unsafe.

  4. Do not swallow InterruptedException without restoring the flag.

// Wrong
try {
    Thread.sleep(1000);
} catch (InterruptedException e) {
    // Ignoring — bad!
}

// Correct
catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

QuickCart: one thread per order

Priya tries handling sale-day orders with raw threads:

public class OrderHandler {
    public void handleOrders(List<String> orderIds) {
        List<Thread> threads = new ArrayList<>();

        for (String orderId : orderIds) {
            Thread thread = new Thread(() -> {
                try {
                    processOrder(orderId);
                } catch (Exception e) {
                    System.err.println("Error on " + orderId + ": " + e.getMessage());
                }
            }, "OrderWorker-" + orderId);

            thread.start();
            threads.add(thread);
        }

        // Wait for all threads to complete
        for (Thread thread : threads) {
            try {
                thread.join();
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("Interrupted while waiting for orders");
            }
        }

        System.out.println("All orders handled");
    }

    private void processOrder(String orderId) {
        System.out.println("Processing: " + orderId);
        // Validate cart, charge card, send email...
    }
}

This works for a demo. But creating a brand-new thread for every order is slow and hard to control. Priya’s shop is growing faster than this approach can keep up.

What to remember

  • Use Runnable, not extending Thread.
  • Call start(), never run(), when you want a new thread.
  • A thread cannot be started twice.
  • Know the lifecycle: NEW → RUNNABLE → … → TERMINATED.
  • Use join() when you need to wait for workers to finish.
  • Handle InterruptedException properly — restore the interrupt flag.
  • Daemon threads are for background work; the JVM exits when only daemons remain.
  • Raw threads teach the basics, but they do not scale well on their own.

What Priya does next: she stops hiring a new person for every single order and sets up a proper crew with an ExecutorService.