Java
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Object-Oriented Java
-
Section 4: Collections Framework
-
Section 5: Exception Handling
-
Section 6: Generics
-
Section 7: Functional Java
-
Section 8: Concurrency
-
Section 9: I/O and NIO
-
Section 10: JVM Internals
-
Section 11: Modern Java Features
-
Section 12: Build Tools and Project Structure
-
Section 13: Testing
-
Section 14: Databases and Persistence
-
Section 15: Networking
-
Section 16: Design and Best Practices
-
Section 17: Reflection and Annotations
-
Section 18: Logging and Diagnostics
-
Section 19: Date, Time, and Internationalization
-
Section 20: Java Platform Module System
-
Section 21: Security in Java
-
Section 22: Advanced Collections and Data Structures
-
Section 23: More Concurrency Patterns
-
Section 24: Compression, Files, and System Integration
-
Section 25: GUI Programming
-
Section 26: Practical Projects
-
Section 27: Data Structures and Algorithms
-
Section 28: Interview and Algorithm Practice
-
Section 29: JSON and Data Interchange
-
Section 30: More Concurrency Utilities
-
Section 31: More Collections and Streams Practice
-
Section 32: More File and System Programming
-
Section 33: Standard Library Deep Dive
-
Section 34: More Practice and Drills
-
Section 35: More Testing and Quality
-
Section 36: More Design Patterns and Architecture
-
Section 37: Career and Ecosystem
-
Section 38: More OOP and Architecture Practice
-
Section 39: More Enterprise Concepts
-
Section 40: Advanced JavaFX
-
Section 41: More Interview Practice
80: Thread Lifecycle
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
synchronizedblock that another thread is currently using). - WAITING: The thread is hanging out indefinitely. This happens when you call
Object.wait()orThread.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)orwait(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
Workerthread that does the following in order:- Sleeps for 100ms.
- Enters a
synchronizedblock (on a shared object) and sleeps for another 100ms. - Calls
wait()on that shared object.
- Create a
Watcherthread that polls theWorkerthread usingworker.getState()every 50ms. - The
Watchermust print the current state of theWorkerto the console. - The
Watchershould continue running until theWorkerreaches theTERMINATEDstate. - The main thread must eventually call
notify()on the shared object to allow theWorkerto finish and terminate.
Goal: Your console output should show a progression of states: NEW → RUNNABLE → TIMED_WAITING → WAITING → TERMINATED. (Note: BLOCKED may appear depending on how you synchronize the Watcher's access).
There are no comments for now.