
本文深入探讨了在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<Future> 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 millisecondsThread-117 is NOW AWAKE...Thread-120 is sleeping for 487 millisecondsThread-120 is NOW AWAKEThread-120 is NOW AWAKEThread-120 is NOW AWAKEThread-120 is NOW AWAKEThread-120 is NOW AWAKEThread-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()来获取当前执行任务的线程信息。
九歌
九歌–人工智能诗歌写作系统
322 查看详情
实现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<Future> 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 millisecondspool-1-thread-6 is sleeping for 497 millisecondspool-1-thread-4 is sleeping for 565 millisecondspool-1-thread-5 is sleeping for 978 millisecondspool-1-thread-2 is sleeping for 917 millisecondspool-1-thread-3 is sleeping for 641 millisecondspool-1-thread-6 is NOW AWAKEpool-1-thread-6 is sleeping for 847 millisecondspool-1-thread-1 is NOW AWAKEpool-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()可以确保获取到执行任务的实际工作线程的正确名称。遵循这些最佳实践,可以帮助我们构建出更加健壮、高效且易于调试的并发应用程序。
以上就是Java并发编程:ExecutorService与Runnable的正确实践的详细内容,更多请关注创想鸟其它相关文章!
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 chuangxiangniao@163.com 举报,一经查实,本站将立刻删除。
发布者:程序猿,转转请注明出处:https://www.chuangxiangniao.com/p/1036044.html
微信扫一扫
支付宝扫一扫