Keywords: Java Multithreading | Thread Lifecycle | Thread Safe Stopping
Abstract: This article explores the mechanisms for starting, stopping, and restarting threads in Java, based on core principles of multithreading. It analyzes the irreversibility of thread lifecycles and presents two main solutions: creating new threads as replacements or implementing thread reuse through wait/notify mechanisms. Detailed explanations on safely stopping threads using flags and join() methods are provided, along with code examples that address limitations of ExecutorService, helping developers avoid common pitfalls and enhance robustness in multithreaded programming.
In Java multithreading, starting, stopping, and restarting threads are common yet often misunderstood operations. Many developers attempt to restart stopped threads directly, stemming from a lack of understanding of thread lifecycle management. This article systematically analyzes this issue based on core concepts and provides practical guidance.
Irreversibility of Thread Lifecycle
The Java thread lifecycle includes states such as new, runnable, running, blocked, and terminated. Once a thread enters the terminated state (either by completing execution or due to an exception), it cannot be restarted. This is because the thread object releases its internal state and resources upon termination, and calling the start() method again will throw an IllegalThreadStateException. For example, the following code demonstrates an attempt to restart a terminated thread:
Thread thread = new Thread(() -> System.out.println("Thread running"));
thread.start();
thread.join(); // Wait for thread to finish
// Attempting to restart throws an exception
thread.start(); // Throws IllegalThreadStateExceptionThis design ensures safety in resource management, preventing memory leaks or inconsistent states.
Solution 1: Creating New Threads as Replacements
Since threads cannot be restarted, the standard approach is to create new thread instances to replace old ones. This is suitable for scenarios requiring complete re-execution of tasks. For instance, with a Task class implementing Runnable, management can be done as follows:
public class Task implements Runnable {
private volatile boolean isTerminating = false;
@Override
public void run() {
while (!isTerminating) {
// Execute task logic
System.out.println("Task executing");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
System.out.println("Thread terminated");
}
public void stopTask() {
isTerminating = true;
}
}
// In the main application
Task task = new Task();
Thread thread = new Thread(task);
thread.start(); // Start the thread
// When stopping is needed
task.stopTask();
thread.join(); // Wait for thread to stop safely
// Restart: create a new thread
Thread newThread = new Thread(task); // Task instance can be reused, but state may need resetting
newThread.start();This method is straightforward but requires attention to the overhead of thread creation and state management.
Solution 2: Implementing Thread Reuse via Wait/Notify Mechanisms
For scenarios requiring pausing and resuming rather than complete restarts, wait/notify mechanisms can be used to reuse threads in a waiting state, avoiding frequent creation and destruction. This is achieved using wait() and notify() or Condition. For example:
public class PausableTask implements Runnable {
private final Object lock = new Object();
private volatile boolean paused = false;
private volatile boolean running = true;
@Override
public void run() {
while (running) {
synchronized (lock) {
while (paused) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
}
// Execute task
System.out.println("Task executing");
}
}
public void pause() {
paused = true;
}
public void resume() {
synchronized (lock) {
paused = false;
lock.notify();
}
}
public void stop() {
running = false;
resume(); // Ensure thread exits wait state
}
}This approach keeps the thread running continuously, controlling pauses and resumes via state flags, suitable for scenarios requiring low-latency restarts.
Practices for Safely Stopping Threads
Stopping threads should avoid the deprecated stop() method, as it may lead to unreleased resources or inconsistent states. Using flag-based control, such as isTerminating in the examples above, is recommended. Combine this with join() to ensure the main thread waits for child thread termination:
// Set termination flag
task.setTerminating(true);
// Wait for thread to stop
try {
thread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}For ExecutorService, shutdownNow() terminates all threads, but the service cannot be restarted once shut down. If task restart is needed, create a new ExecutorService instance rather than reusing a closed one.
Summary and Best Practices
The irreversibility of Java thread restarts is fundamental to multithreading design. Developers should manage task lifecycles by creating new threads or implementing reuse mechanisms. Key points include: using flags for safe thread stopping, avoiding direct thread killing, and leveraging ExecutorService appropriately. In practice, choose solutions based on task characteristics—create new threads for frequent starts/stops, and use wait mechanisms for quick resumption. This enhances code reliability and performance.