首页 > Java > java教程 > 正文

Java并发编程:ExecutorService与Runnable的正确实践

聖光之護
发布: 2025-11-10 13:13:38
原创
471人浏览过

java并发编程:executorservice与runnable的正确实践

本文深入探讨了在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 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工作原理的误解。

  1. 不当继承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字段在不同的任务执行中可能会被重复赋值,或者由于并发访问导致状态混乱,从而引发重复打印的问题,尤其是在任务执行接近尾声时,这种状态混乱可能表现得更加明显。
  2. ExecutorService与Runnable的关系

    • ExecutorService的设计目的是管理线程池,并执行Runnable或Callable任务。它会从池中分配一个工作线程来执行提交的任务的run()或call()方法。
    • 任务本身(即你提交给ExecutorService的对象)不应该是一个Thread实例,而应该是一个定义了任务逻辑的Runnable或Callable。ExecutorService会负责将这些任务包装到它自己的工作线程中执行。

解决方案:使用Runnable接口和Thread.currentThread()

解决这个问题的正确方法是遵循Java并发编程的最佳实践:将任务逻辑封装在Runnable接口中,并使用Thread.currentThread()来获取当前执行任务的线程信息。

豆包AI编程
豆包AI编程

豆包推出的AI编程助手

豆包AI编程 483
查看详情 豆包AI编程
  1. 实现Runnable接口

    • 将sampleThread类修改为实现Runnable接口,而不是继承Thread。
    • 移除sampleThread类内部的sampleThread thread;字段和run()方法中的thread = new sampleThread();语句。
  2. 使用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 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个任务。

注意事项与最佳实践

  1. Runnable vs Thread

    • 实现Runnable:这是推荐的方式,因为它将任务逻辑(run()方法中的代码)与线程的创建和管理分离。当使用ExecutorService时,你通常应该提交Runnable或Callable任务。
    • 继承Thread:只有当你需要修改线程的行为(例如,重写start()方法)时才应该继承Thread,但这在大多数情况下是不必要的,并且可能导致设计上的耦合。
  2. ExecutorService的作用

    • ExecutorService的主要职责是管理线程池,复用线程,从而减少线程创建和销毁的开销。
    • 它提供了一种高级抽象,让开发者可以专注于任务逻辑,而不是底层线程管理。
  3. 避免在任务内部创建新线程

    • 除非有非常特殊和明确的理由,否则不应在提交给ExecutorService的Runnable或Callable的run()/call()方法内部创建并启动新的Thread实例。这通常是反模式,会导致线程管理混乱,并可能耗尽系统资源。
  4. 正确获取当前线程信息

    • 始终使用Thread.currentThread()来获取当前正在执行代码的线程的引用。这对于日志记录、调试和任何需要当前线程上下文的操作都至关重要。

总结

在Java并发编程中,理解ExecutorService与Runnable、Thread之间的关系至关重要。当使用ExecutorService时,应将任务逻辑封装在实现Runnable接口的类中,并避免在run()方法内部创建新的Thread实例。同时,使用Thread.currentThread().getName()可以确保获取到执行任务的实际工作线程的正确名称。遵循这些最佳实践,可以帮助我们构建出更加健壮、高效且易于调试的并发应用程序。

以上就是Java并发编程:ExecutorService与Runnable的正确实践的详细内容,更多请关注php中文网其它相关文章!

编程速学教程(入门课程)
编程速学教程(入门课程)

编程怎么学习?编程怎么入门?编程在哪学?编程怎么学才快?不用担心,这里为大家提供了编程速学教程(入门课程),有需要的小伙伴保存下载就能学习啦!

下载
来源:php中文网
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
最新问题
开源免费商场系统广告
热门教程
更多>
最新下载
更多>
网站特效
网站源码
网站素材
前端模板
关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新 English
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送
PHP中文网APP
随时随地碎片化学习

Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号