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 或 FastTaskif (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 如果线程在休眠期间被中断*/@Overridepublic String call() throws Exception {// 线程休眠 5 秒钟Thread.sleep(5000);// 返回任务结果return "速度慢的任务";}}/*** 表示一个执行速度较快的任务。* 实现了 Callable 接口,返回一个字符串结果。*/static class FastTask implements Callable<String> {/*** 执行任务的主要逻辑。* 立即返回一个表示任务完成的字符串。* * @return 表示任务完成的字符串* @throws Exception 如果发生异常*/@Overridepublic 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 再次重新执行任务。
这一点和线程、线程池的状态是一样的,线程和线程池的状态也是不能后退的。关于线程的状态和流转路径,在线程状态与线程停止已经讲过了,如图所示。
这个图也是我们当时讲解所用的图,如果有些遗忘,可以回去复习一下当时的内容。
注意点:
- 检查任务是否完成
使用 isDone() 方法检查任务是否完成,避免不必要的等待。- 避免长时间阻塞
使用 get() 方法会阻塞当前线程,直到任务完成。为了避免长时间阻塞,可以使用 get(long timeout, TimeUnit unit) 方法,并设置超时时间。- 处理异常
如果任务抛出异常,get() 方法会抛出 ExecutionException,其 cause 是任务抛出的异常。需要妥善处理这些异常。- 取消任务
如果任务尚未完成,可以使用 cancel(boolean mayInterruptIfRunning) 方法取消任务。如果任务已经完成或被取消,cancel 方法将返回 false。- 资源管理
确保在任务完成后释放相关资源,避免内存泄漏。- 线程安全
Future 本身是线程安全的,但任务的执行结果需要确保线程安全,尤其是在多个线程访问共享资源时。- 避免直接使用 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 本身不会产生新的线程,它主要是用于管理异步任务的结果,与线程池中的线程协作来实现异步计算。