Skip to Content
Course content
Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've seen this happen more times than I care to admit, usually during a late-night debugging session where the developer is trying to "reuse" a thread to save on overhead. Take a look at this snippet. The goal here is simple: a background worker that processes a batch of data, and once it's done, the main program tries to kick it off again for a second batch.

public class BatchProcessor {
    public static void main(String[] args) {
        Thread worker = new Thread(() -> {
            System.out.println("Processing batch...");
            try { Thread.sleep(1000); } catch (InterruptedException e) {}
            System.out.println("Batch complete.");
        });

        worker.start();
        
        try {
            worker.join(); // Wait for it to finish
        } catch (InterruptedException e) {}

        System.out.println("Restarting worker for next batch...");
        worker.start(); // This is where it blows up
    }
}

The IllegalThreadStateException Mystery

If you run this, Java isn't going to let you get away with it. You'll hit an IllegalThreadStateException the moment that second worker.start() is called. At first glance, it seems weird—the thread object is still there, the code is still there, so why can't we just "restart" it?

The problem is that you're fighting the Thread Lifecycle. In Java, a thread isn't just a piece of code; it's a state machine managed by the JVM and the underlying OS. Once a thread completes its run() method, it enters the TERMINATED state. There is no "Reset" button for a terminated thread. Once it's dead, it's dead.

Why Threads Only Live Once

To fix this, you have to stop thinking of the Thread object as the "task" and start thinking of it as the "worker." If the worker has retired (terminated), you don't try to force them back to work; you hire a new worker.

The fix is simply to instantiate a new Thread object for every new execution. If you're worried about the cost of creating threads—which is a valid concern in high-performance apps—that's why we use ExecutorService and thread pools, which keep workers in a WAITING state instead of letting them reach TERMINATED. But for basic thread usage, the rule is: one start per object.

// The correct approach: create a new instance
public void runBatch() {
    Thread worker = new Thread(() -> {
        System.out.println("Processing...");
    });
    worker.start();
}

// Call runBatch() whenever you need a new execution

Navigating the State Machine

To really master concurrency, you need to visualize exactly where your thread is at any given moment. Java defines these specific states in the Thread.State enum:

  • NEW: You've called new Thread(), but you haven't called .start() yet. It's just an object sitting in memory.
  • RUNNABLE: This is a bit of a misnomer. A runnable thread might actually be running, or it might be waiting for the OS to give it a CPU time slice. Essentially, it's "ready to go."
  • BLOCKED: The thread is alive, but it's stuck waiting to acquire a monitor lock (like trying to enter a synchronized block that another thread is currently using).
  • WAITING: The thread is hanging out indefinitely. This happens when you call Object.wait() or Thread.join() without a timeout. It won't wake up until another thread explicitly signals it.
  • TIMED_WAITING: Same as waiting, but with a countdown. Think Thread.sleep(ms) or wait(ms).
  • TERMINATED: The run() method has finished. The thread is no longer eligible to execute.

Waiting vs. Blocked: The Nuance

I often see people use "blocked" as a general term for "the thread isn't moving," but in the Java Lifecycle, there is a massive difference between BLOCKED and WAITING.

If a thread is BLOCKED, it is actively fighting for a lock. It's like standing in line at a door, waiting for the person inside to leave. If a thread is WAITING, it has stepped out of line and gone to a waiting room. It isn't even looking at the door anymore; it's waiting for someone to come tell it, "Okay, now you can go back and try to get the lock."

Understanding this distinction is the only way to diagnose deadlocks. If you see a thread dump where everything is BLOCKED, you have a lock contention problem. If everything is WAITING, you probably forgot to call notify() or notifyAll().




📋 Practical Task

The Thread State Lifecycle Logger

Your task is to build a program that proves you understand the transition between states. You will create a "Watcher" thread and a "Worker" thread.

Requirements:

  • Create a Worker thread that does the following in order:
    1. Sleeps for 100ms.
    2. Enters a synchronized block (on a shared object) and sleeps for another 100ms.
    3. Calls wait() on that shared object.
  • Create a Watcher thread that polls the Worker thread using worker.getState() every 50ms.
  • The Watcher must print the current state of the Worker to the console.
  • The Watcher should continue running until the Worker reaches the TERMINATED state.
  • The main thread must eventually call notify() on the shared object to allow the Worker to finish and terminate.

Goal: Your console output should show a progression of states: NEWRUNNABLETIMED_WAITINGWAITINGTERMINATED. (Note: BLOCKED may appear depending on how you synchronize the Watcher's access).

Rating
0 0

There are no comments for now.

to be the first to leave a comment.