
本文深入探讨了在Java并发编程中使用`ExecutorService`时,由于不当继承`Thread`类并在`run()`方法中重复创建`Thread`实例而导致的常见问题,即任务执行结果混乱和线程名称识别错误。文章通过分析错误代码,阐明了应使用`Runnable`接口将任务逻辑与线程管理解耦,并利用`Thread.currentThread().getName()`准确获取当前执行线程名称的最佳实践,以构建健壮高效的并发应用。
在Java并发编程中,ExecutorService是管理和执行异步任务的强大工具。然而,如果不正确地使用它,可能会导致意想不到的行为,例如任务结果重复或线程身份混淆。本文将分析一个典型的案例,并提供使用Runnable接口和ExecutorService的最佳实践。
现象分析:任务执行中的异常重复输出
在某些情况下,开发者可能会遇到在使用ExecutorService提交任务时,最后一个任务的完成信息被重复打印多次的现象。这通常发生在自定义的任务类继承了Thread,并且在run()方法内部错误地创建了新的Thread实例。
考虑以下一个简化的示例代码结构:
立即学习“Java免费学习笔记(深入)”;
错误的sampleThread.java实现:
import java.util.Random;
public class sampleThread extends Thread { // 错误:直接继承Thread
sampleThread thread; // 错误:在任务类中声明一个自身的实例
Random rand = new Random();
public void run() {
thread = new sampleThread(); // 错误:在run方法内部创建新的Thread实例
int randSleep = rand.nextInt(1000);
// 使用内部创建的thread实例的名称
System.out.println(thread.getName() + " is sleeping for " + randSleep + " milliseconds");
try {
Thread.sleep(randSleep);
// 使用内部创建的thread实例的名称
System.out.println(thread.getName() + " is NOW AWAKE");
} catch (InterruptedException e) {
Thread.currentThread().interrupt(); // 更好的中断处理
throw new RuntimeException(e);
}
}
}driver.java提交任务:
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// import java.util.concurrent.ExecutionException; // 如果不调用future.get(),可以不导入
public class driver {
public static void main(String[] args) /* throws ExecutionException, InterruptedException */ {
List> futArray = new ArrayList<>();
ExecutorService es = Executors.newFixedThreadPool(6); // 创建一个固定大小的线程池
sampleThread temp = new sampleThread(); // 创建一个sampleThread实例
for (int i = 0; i < 120; i++) {
// 将同一个temp实例提交给线程池
Future> future = es.submit(temp);
futArray.add(future);
}
es.shutdown(); // 关闭线程池,等待所有任务完成
// 可以选择等待所有任务完成
// for (Future> future : futArray) {
// try {
// future.get();
// } catch (InterruptedException | ExecutionException e) {
// e.printStackTrace();
// }
// }
}
} 当上述代码运行时,可能会观察到类似以下输出:
Thread-117 is sleeping for 547 milliseconds Thread-117 is NOW AWAKE ... Thread-120 is sleeping for 487 milliseconds Thread-120 is NOW AWAKE Thread-120 is NOW AWAKE Thread-120 is NOW AWAKE Thread-120 is NOW AWAKE Thread-120 is NOW AWAKE Thread-120 is NOW AWAKE
其中,最后一个线程的“NOW AWAKE”信息被重复打印了多次。
问题根源分析
这个问题的核心在于对Java并发编程模型和ExecutorService工作原理的误解。
-
不当继承Thread并内部创建实例:
- 当一个类继承Thread时,它本身就是一个线程对象。
- 在sampleThread的run()方法内部,又创建了一个新的sampleThread实例 (thread = new sampleThread();)。这意味着每次ExecutorService从线程池中取出一个工作线程来执行temp对象的run()方法时,该工作线程都会在内部创建一个新的、独立的Thread对象。
- 这个内部创建的Thread对象 (thread) 从未被显式启动 (thread.start()),但它的getName()方法被调用来打印信息。这导致打印出的线程名称 (Thread-X) 实际上是这个未启动的、内部Thread实例的名称,而不是ExecutorService中真正执行任务的工作线程的名称。
- 由于driver.java中提交的是同一个temp实例,其内部的thread字段在不同的任务执行中可能会被重复赋值,或者由于并发访问导致状态混乱,从而引发重复打印的问题,尤其是在任务执行接近尾声时,这种状态混乱可能表现得更加明显。
-
ExecutorService与Runnable的关系:
- ExecutorService的设计目的是管理线程池,并执行Runnable或Callable任务。它会从池中分配一个工作线程来执行提交的任务的run()或call()方法。
- 任务本身(即你提交给ExecutorService的对象)不应该是一个Thread实例,而应该是一个定义了任务逻辑的Runnable或Callable。ExecutorService会负责将这些任务包装到它自己的工作线程中执行。
解决方案:使用Runnable接口和Thread.currentThread()
解决这个问题的正确方法是遵循Java并发编程的最佳实践:将任务逻辑封装在Runnable接口中,并使用Thread.currentThread()来获取当前执行任务的线程信息。
-
实现Runnable接口:
- 将sampleThread类修改为实现Runnable接口,而不是继承Thread。
- 移除sampleThread类内部的sampleThread thread;字段和run()方法中的thread = new sampleThread();语句。
-
使用Thread.currentThread().getName():
- 在run()方法中,使用Thread.currentThread().getName()来获取当前正在执行该run()方法的线程的名称。这将是ExecutorService分配的工作线程的名称,而不是一个无关的、未启动的Thread实例的名称。
修正后的sampleThread.java实现:
import java.util.Random;
// 正确:实现Runnable接口,将任务逻辑与线程管理分离
public class sampleThread implements Runnable {
Random rand = new Random();
@Override // 明确重写Runnable接口的run方法
public void run() {
int randSleep = rand.nextInt(1000);
// 正确:获取当前执行任务的工作线程的名称
System.out.println(Thread.currentThread().getName() + " is sleeping for " + randSleep + " milliseconds");
try {
Thread.sleep(randSleep);
// 正确:获取当前执行任务的工作线程的名称
System.out.println(Thread.currentThread().getName() + " is NOW AWAKE");
} catch (InterruptedException e) {
// 当线程被中断时,设置中断标志并处理
Thread.currentThread().interrupt();
throw new RuntimeException("Thread interrupted during sleep", e);
}
}
}修正后的driver.java提交任务:
driver.java中的主要改动是提交任务的方式,现在每次循环都创建一个新的sampleThread实例(一个Runnable任务)并提交。
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
// import java.util.concurrent.ExecutionException; // 如果不调用future.get(),可以不导入
public class driver {
public static void main(String[] args) {
List> futArray = new ArrayList<>();
ExecutorService es = Executors.newFixedThreadPool(6); // 创建一个固定大小的线程池
for (int i = 0; i < 120; i++) {
// 正确:每次提交一个独立的Runnable任务实例
Future> future = es.submit(new sampleThread());
futArray.add(future);
}
es.shutdown(); // 关闭线程池,等待所有任务完成
// 可以选择等待所有任务完成
// for (Future> future : futArray) {
// try {
// future.get();
// } catch (InterruptedException | ExecutionException e) {
// e.printStackTrace();
// }
// }
}
} 使用上述修正后的代码,输出将是清晰且符合预期的,每个任务都会由线程池中的一个工作线程执行,并正确打印其状态:
pool-1-thread-1 is sleeping for 526 milliseconds pool-1-thread-6 is sleeping for 497 milliseconds pool-1-thread-4 is sleeping for 565 milliseconds pool-1-thread-5 is sleeping for 978 milliseconds pool-1-thread-2 is sleeping for 917 milliseconds pool-1-thread-3 is sleeping for 641 milliseconds pool-1-thread-6 is NOW AWAKE pool-1-thread-6 is sleeping for 847 milliseconds pool-1-thread-1 is NOW AWAKE pool-1-thread-1 is sleeping for 125 milliseconds ...
可以看到,pool-1-thread-X是ExecutorService内部管理的工作线程的名称,输出清晰地展示了6个线程在并发执行120个任务。
注意事项与最佳实践
-
Runnable vs Thread:
- 实现Runnable:这是推荐的方式,因为它将任务逻辑(run()方法中的代码)与线程的创建和管理分离。当使用ExecutorService时,你通常应该提交Runnable或Callable任务。
- 继承Thread:只有当你需要修改线程的行为(例如,重写start()方法)时才应该继承Thread,但这在大多数情况下是不必要的,并且可能导致设计上的耦合。
-
ExecutorService的作用:
- ExecutorService的主要职责是管理线程池,复用线程,从而减少线程创建和销毁的开销。
- 它提供了一种高级抽象,让开发者可以专注于任务逻辑,而不是底层线程管理。
-
避免在任务内部创建新线程:
- 除非有非常特殊和明确的理由,否则不应在提交给ExecutorService的Runnable或Callable的run()/call()方法内部创建并启动新的Thread实例。这通常是反模式,会导致线程管理混乱,并可能耗尽系统资源。
-
正确获取当前线程信息:
- 始终使用Thread.currentThread()来获取当前正在执行代码的线程的引用。这对于日志记录、调试和任何需要当前线程上下文的操作都至关重要。
总结
在Java并发编程中,理解ExecutorService与Runnable、Thread之间的关系至关重要。当使用ExecutorService时,应将任务逻辑封装在实现Runnable接口的类中,并避免在run()方法内部创建新的Thread实例。同时,使用Thread.currentThread().getName()可以确保获取到执行任务的实际工作线程的正确名称。遵循这些最佳实践,可以帮助我们构建出更加健壮、高效且易于调试的并发应用程序。











