当前位置: 首页 > article >正文

Java多线程与高并发专题——使用 Future 有哪些注意点?Future 产生新的线程了吗?

Future 的注意点

1. 当 for 循环批量获取 Future 的结果时容易 block,get 方法调用时应使用 timeout 限制

对于 Future 而言,第一个注意点就是,当 for 循环批量获取 Future 的结果时容易 block,在调用 get方法时,应该使用 timeout 来限制。

下面我们具体看看这是一个什么情况。

首先,假设一共有四个任务需要执行,我们都把它放到线程池中,然后它获取的时候是按照从 1 到 4 的顺序,也就是执行 get() 方法来获取的,代码如下所示:

/**
 * 这个类演示了如何使用 Java 的 Future 和 Callable 接口来执行异步任务。
 * 它创建了一个固定大小的线程池,并提交了多个任务,其中一些任务执行速度较慢,而另一些任务执行速度较快。
 * 最后,它等待所有任务完成并打印出每个任务的结果。
 */
public class FutureDemo {
    /**
     * 程序的入口点,演示了如何使用 Future 和 Callable 接口执行异步任务。
     * 
     * @param args 命令行参数(未使用)
     */
    public static void main(String[] args) {
        // 创建一个固定大小为 10 的线程池
        ExecutorService service = Executors.newFixedThreadPool(10);
        // 用于存储所有 Future 对象的列表
        ArrayList<Future> allFutures = new ArrayList<>();
        // 循环提交 4 个任务
        for (int i = 0; i < 4; i++) {
            Future<String> future;
            // 根据 i 的值选择提交 SlowTask 或 FastTask
            if (i == 0 || i == 1) {
                // 提交 SlowTask 任务
                future = service.submit(new SlowTask());
            } else {
                // 提交 FastTask 任务
                future = service.submit(new FastTask());
            }
            // 将 Future 对象添加到列表中
            allFutures.add(future);
        }
        // 遍历所有 Future 对象并获取结果
        for (int i = 0; i < 4; i++) {
            // 从列表中获取 Future 对象
            Future<String> future = allFutures.get(i);
            try {
                // 获取任务的结果
                String result = future.get();
                // 打印任务的结果
                System.out.println(result);
            } catch (InterruptedException e) {
                // 处理线程中断异常
                e.printStackTrace();
            } catch (ExecutionException e) {
                // 处理任务执行异常
                e.printStackTrace();
            }
        }
        // 关闭线程池
        service.shutdown();
    }

    /**
     * 表示一个执行速度较慢的任务。
     * 实现了 Callable 接口,返回一个字符串结果。
     */
    static class SlowTask implements Callable<String> {
        /**
         * 执行任务的主要逻辑。
         * 线程会休眠 5 秒钟,然后返回一个表示任务完成的字符串。
         * 
         * @return 表示任务完成的字符串
         * @throws Exception 如果线程在休眠期间被中断
         */
        @Override
        public String call() throws Exception {
            // 线程休眠 5 秒钟
            Thread.sleep(5000);
            // 返回任务结果
            return "速度慢的任务";
        }
    }

    /**
     * 表示一个执行速度较快的任务。
     * 实现了 Callable 接口,返回一个字符串结果。
     */
    static class FastTask implements Callable<String> {
        /**
         * 执行任务的主要逻辑。
         * 立即返回一个表示任务完成的字符串。
         * 
         * @return 表示任务完成的字符串
         * @throws Exception 如果发生异常
         */
        @Override
        public String call() throws Exception {
            // 返回任务结果
            return "速度快的任务";
        }
    }
}

可以看出,在代码中我们新建了线程池,并且用一个 list 来保存 4 个 Future。其中,前两个 Future 所对应的任务是慢任务,也就是代码下方的 SlowTask,而后两个 Future 对应的任务是快任务。慢任务在执行的时候需要 5 秒钟的时间才能执行完毕,而快任务很快就可以执行完毕,几乎不花费时间。

在提交完这 4 个任务之后,我们用 for 循环对它们依次执行 get 方法,来获取它们的执行结果,然后再把这个结果打印出来。

执行结果如下:

可以看到,这个执行结果是打印 4 行语句,前面两个是速度慢的任务,后面两个是速度快的任务。虽然结果是正确的,但实际上在执行的时候会先等待 5 秒,然后再很快打印出这 4 行语句。

这里有一个问题,即第三个的任务量是比较小的,它可以很快返回结果,紧接着第四个任务也会返回结果。但是由于前两个任务速度很慢,所以我们在利用 get 方法执行时,会卡在第一个任务上。也就是说,虽然此时第三个和第四个任务很早就得到结果了,但我们在此时使用这种 for 循环的方式去获取结果,依然无法及时获取到第三个和第四个任务的结果。直到 5 秒后,第一个任务出结果了,我们才能获取到,紧接着也可以获取到第二个任务的结果,然后才轮到第三、第四个任务。

假设由于网络原因,第一个任务可能长达 1 分钟都没办法返回结果,那么这个时候,我们的主线程会一直卡着,影响了程序的运行效率。

此时我们就可以用 Future 的带超时参数的 get(long timeout, TimeUnit unit) 方法来解决这个问题。这个方法的作用是,如果在限定的时间内没能返回结果的话,那么便会抛出一个TimeoutException 异常,随后就可以把这个异常捕获住,或者是再往上抛出去,这样就不会一直卡着了。

2. Future 的生命周期不能后退

Future 的生命周期不能后退,一旦完成了任务,它就永久停在了“已完成”的状态,不能从头再来,也不能让一个已经完成计算的 Future 再次重新执行任务。

这一点和线程、线程池的状态是一样的,线程和线程池的状态也是不能后退的。关于线程的状态和流转路径,在线程状态与线程停止已经讲过了,如图所示。

这个图也是我们当时讲解所用的图,如果有些遗忘,可以回去复习一下当时的内容。

注意点:

  1. 检查任务是否完成
    使用 isDone() 方法检查任务是否完成,避免不必要的等待。
  2. 避免长时间阻塞
    使用 get() 方法会阻塞当前线程,直到任务完成。为了避免长时间阻塞,可以使用 get(long timeout, TimeUnit unit) 方法,并设置超时时间。
  3. 处理异常
    如果任务抛出异常,get() 方法会抛出 ExecutionException,其 cause 是任务抛出的异常。需要妥善处理这些异常。
  4. 取消任务
    如果任务尚未完成,可以使用 cancel(boolean mayInterruptIfRunning) 方法取消任务。如果任务已经完成或被取消,cancel 方法将返回 false。
  5. 资源管理
    确保在任务完成后释放相关资源,避免内存泄漏。
  6. 线程安全
    Future 本身是线程安全的,但任务的执行结果需要确保线程安全,尤其是在多个线程访问共享资源时。
  7. 避免直接使用 Thread
    推荐使用 ExecutorService 来管理线程池,而不是直接使用 Thread 创建线程。

Future 产生新的线程了吗

最后我们再来回答这个问题:Future 是否产生新的线程了?

有一种说法是,除了继承 Thread 类和实现 Runnable 接口之外,还有第三种产生新线程的方式,那就是采用 Callable 和 Future,这叫作有返回值的创建线程的方式。这种说法是不正确的。

其实 Callable 和 Future 本身并不能产生新的线程,它们需要借助其他的比如 Thread 类或者线程池才能执行任务。例如,在把 Callable 提交到线程池后,真正执行 Callable 的其实还是线程池中的线程,而线程池中的线程是由 ThreadFactory 产生的,这里产生的新线程与 Callable、Future 都没有关系,所以Future 并没有产生新的线程。

Future 本身并不直接创建新的线程。它只是一个接口,表示异步计算的结果。线程的创建和管理通常由 ExecutorService 或其他执行器来完成。

Future 的作用及特点:

  • Future 主要用于获取异步任务的结果。当一个异步任务被提交执行后,可以通过 Future 来获取任务的结果,而无需阻塞当前线程等待任务完成。例如,使用 Java 的java.util.concurrent.ExecutorService提交一个任务时,会返回一个 Future 对象。这个对象可以在稍后的时间用来检查任务是否完成,以及获取任务的结果。
  • Future 并不负责创建线程,它只是对异步任务的结果进行包装和管理。异步任务的执行通常是由线程池中的线程来完成的,而线程池在创建时就已经包含了一定数量的线程,这些线程会被重复利用来执行提交的任务。所以,Future 是利用已有的线程资源来管理异步任务,而不是创建新的线程。

线程的创建方式与 Future 的关系:

  • 在 Java 中,创建新线程主要有两种方式。一种是继承java.lang.Thread类并重写run方法,另一种是实现java.lang.Runnable接口并将其作为参数传递给Thread构造函数或ExecutorService的submit方法。无论是哪种方式,都是明确地创建新的线程来执行特定的任务。
  • 而 Future 通常是在使用线程池执行异步任务时返回的对象。线程池中的线程会执行提交的任务,任务完成后,Future 对象可以用来获取任务的结果。所以,Future 是与线程池中的线程协作,而不是直接创建新线程。

示例说明

以下是一个使用线程池和 Future 的示例代码:

/**
 * 该类演示了如何使用 Java 的 Future 接口来执行异步任务。
 * 通过 ExecutorService 提交一个异步任务,并使用 Future 对象获取任务的结果。
 */
public class FutureExample {
    /**
     * 程序的入口点,演示了如何使用 ExecutorService 和 Future 来执行异步任务。
     *
     * @param args 命令行参数
     * @throws InterruptedException 如果在等待异步任务完成时线程被中断
     * @throws ExecutionException 如果异步任务执行过程中抛出异常
     */
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        // 创建一个固定大小为 5 的线程池
        ExecutorService executorService = Executors.newFixedThreadPool(5);

        // 提交一个异步任务到线程池,并返回一个 Future 对象
        Future<Integer> future = executorService.submit(() -> {
            // 这里是异步任务的执行逻辑
            System.out.println("异步任务正在执行...");
            // 模拟耗时操作,线程休眠 2 秒
            Thread.sleep(2000);
            // 返回异步任务的结果
            return 42;
        });

        // 主线程继续执行其他任务
        System.out.println("主线程继续执行其他任务...");

        // 获取异步任务的结果,如果任务未完成,会阻塞当前线程直到任务完成
        Integer result = future.get();
        // 输出异步任务的结果
        System.out.println("异步任务结果:" + result);

        // 关闭线程池
        executorService.shutdown();
    }
}

在这个例子中,ExecutorService的submit方法提交了一个异步任务,返回一个 Future 对象。这个异步任务是在线程池中的某个线程中执行的,而不是由 Future 创建新线程来执行。主线程可以继续执行其他任务,然后通过 Future 的get方法获取异步任务的结果。如果异步任务未完成,get方法会阻塞主线程直到任务完成。

综上所述,Java 中的 Future 本身不会产生新的线程,它主要是用于管理异步任务的结果,与线程池中的线程协作来实现异步计算。


http://www.kler.cn/a/600023.html

相关文章:

  • 内网渗透-隧道通信
  • Python技术栈与数据可视化创意实践详解(三)
  • 【进阶编程】跨平台的 UI 框架
  • JVM 02
  • STM32G030移植RT-Thread
  • 学一个前端 UI 框架,要学些什么内容?
  • 当人类关系重构:从“相互需要”到“鹅卵石化”——生成式人工智能(GAI)认证的角色与影响
  • 探索AI的无限可能,体验智能对话的未来,大模型 API 演示
  • linux ptrace 图文详解(三) PTRACE_ATTACH 跟踪程序
  • Edge浏览器如何默认启动某个工作区 / 为工作区添加快捷方式
  • docker 容器 php环境中安装gd 、mysql 等扩展
  • 数据库原理及应用mysql版陈业斌实验一
  • 【Docker系列二】 Docker 镜像
  • Spring-Mybatis框架常见面试题
  • Java面试第十三山!《设计模式》
  • 快速部署Samba共享服务器作为k8s后端存储
  • Android adb调试应用程序
  • 解锁应急管理新境界:AR眼镜与指挥平台的完美融合
  • 常见框架漏洞:Thinkphp(TP)篇
  • 【Git流程最佳实践】 开发较大功能时应使用project branch