JUC从实战到源码:Future实战与优缺点

Future编码实战与优缺点

😄生命不息,写作不止

🔥 继续踏上学习之路,学之分享笔记

👊 总有一天我也能像各位大佬一样

🏆 博客首页 @怒放吧德德 To记录领地 @一个有梦有戏的人

🌝分享学习心得,欢迎指正,大家一起学习成长!

转发请携带作者信息 @怒放吧德德(掘金) @一个有梦有戏的人(CSDN)

前言

在现代并发编程中,Java 的 Future 接口提供了一种处理异步计算结果的机制。Future 是 Java 5 中引入的 java.util.concurrent 包的一部分,用于表示一个任务的未来结果。随着应用程序需求的复杂化和多线程编程的普及,理解和运用 Future 变得尤为重要。本篇文章将深入探讨 Java 中 Future 的概念、使用方法及其在实际编程中的应用场景。通过学习这篇文章,读者将能够掌握如何使用 Future 接口进行异步操作,提升程序的性能和响应速度。此外,我们还将介绍与 Future 相关的其他关键类和接口,如 Callable 和 ExecutorService,以帮助读者全面了解并发编程的相关知识。无论你是刚接触 Java 并发编程的新手,还是希望深入理解和优化异步任务处理的开发者,这篇文章都将为你提供有价值的指导和参考。让我们一同开启对 Java Future 的学习之旅,探索并发编程的奥秘。

Future相关概念

Future 是一种用于表示异步计算结果的接口。通过 Future,你可以提交一个任务,并在稍后某个时间点检查任务的完成情况、获取任务的结果、取消任务等。Future接口通常与Java的并发包(java.util.concurrent)中的其他类一起使用,特别是与 ExecutorService 和 Callable 接口配合使用。

简单的说,Future接口可以为主线程开启一个分支任务,专门用来处理主线程耗时较长的复杂业务。

FutureTask的引进

要引入FutureTask,我们就要先了解一下为什么需要出现Future。当我们遇到一些业务比较繁杂的时候,通过将耗时的任务交给 Future 执行,你可以继续执行其他的任务,充分利用系统资源,特别是在多核处理器上。

Future接口

我们来了解一下Future接口,它是Java5新增加的接口,提供了异步并行计算的功能。

Future接口提供了几个方法来管理和查询异步计算的状态和结果:

  • cancel(boolean mayInterruptIfRunning): 尝试取消执行这个任务。参数mayInterruptIfRunning指定如果任务正在运行,是否允许中断执行这个任务的线程。
  • isCancelled(): 如果任务在完成前被取消,则返回true。
  • isDone(): 如果任务完成了,不管是正常结束、异常终止还是取消,都会返回true。
  • get(): 等待计算完成,然后获取其结果。如果计算已完成,则此方法会立即返回结果;否则会阻塞直到任务完成,然后返回结果。
  • get(long timeout, TimeUnit unit): 如果必要,最多等待到指定的时间以让任务完成,并返回其结果,如果指定时间内任务未完成,则抛出一个TimeoutException。

从代码中,我们可以看出Future接口的方法并不多。

那么,是如何引出FutureTask的呢,接下来就是通过Runnable、Callable、Future接口和FutureTask来进行学习,主要目的就是异步多线程执行并且获取返回结果,这里就引发出了三个概念,多线程、有返回值、异步任务。

FutureTask的出现

我们通过代码的形式,来一步一步去了解和使用FutureTask。

正如上面所说的,需要线程,我们都知道,创建线程主要有两种方式。

① 实现Callable接口,需要实现call()方法,这是个有返回值的线程创建。

java 复制代码
class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("进入了 call() 方法");
        return "hello world";
    }
}

② 实现Runnable接口,需要实现run()方法,这是没有返回值的线程创建。

java 复制代码
class MyThread2 implements Runnable {

    @Override
    public void run() {

    }
}

既然我们需要创建多线程,我们首先就要看一下Thread类的构造方法。

我们能够看到,Thread创建时候,带入构造函数的,只有Runnable接口。但是我们根据条件来看却需要有返回值的,那就不能是Runnable,需要的时候Callable接口,那这要怎么办呢?

所以就有了RunnableFuture接口,它不仅实现了Runnable接口,还实现了Future接口。

java 复制代码
public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}

有接口就会有实现类,RunnableFuture接口的实现类FutureTask类。

我们可以看一下这几个类的类图。我们能够清晰的看到FutureTask是如何将三者(多线程、有返回值、异步任务)联系起来。也就是需要达到这些条件,至始引出了FutureTask类。

接下来我们直接看一下案例代码。

java 复制代码
public class FutureDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        FutureTask<String> futureTask = new FutureTask<>(new MyThread());
        Thread t1 = new Thread(futureTask);
        t1.start();

        System.out.println(futureTask.get());
    }
}

class MyThread implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("进入了 call() 方法");
        return "hello world";
    }
}

这段代码的执行结果如下:

java 复制代码
进入了 call() 方法
hello world

首先,我们可以从FutureTask的结构中看到,它有构造函数来携带Callable。

因此我们首先new FutureTask时候,将我们实现Callable接口的线程类做为参数代入。FutureTask<String> futureTask = new FutureTask<>(new MyThread());这样我们就得到一个有返回值的任务,那要怎么异步执行呢?显然我们需要在开一个线程,让新的线程去异步执行。

java 复制代码
Thread t1 = new Thread(futureTask);
t1.start();

这样t1就是异步执行我们耗时的任务,最后就是需要获得返回值。FutureTask有两个get方法,能够获取线程执行的返回值。一般我们都是采用了不带参数的,携带参数的是能够判断超时中断。

java 复制代码
public V get() throws InterruptedException, ExecutionException {
    int s = state;
    if (s <= COMPLETING)
        s = awaitDone(false, 0L);
    return report(s);
}

public V get(long timeout, TimeUnit unit)
    throws InterruptedException, ExecutionException, TimeoutException {
    if (unit == null)
        throw new NullPointerException();
    int s = state;
    if (s <= COMPLETING &&
        (s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
        throw new TimeoutException();
    return report(s);
}

以上这段代码是Java的源码,在get()方法执行的时候,首先会判断该任务的状态是不是完成的,如果这个任务还没有结束,那就会进入等待(也就是会阻塞主线程),这也是Future的一个缺点。

Futere编码实战

接下来,我们模拟一个案例,假如说,我们在一段代码中,里面有3个可以独立开来的业务处理,分别需要处理100ms,200ms,300ms。先看以下代码。

java 复制代码
public class FutureThreadPoolDemo {
    @SneakyThrows
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();

        System.out.println("前面的业务");
        Thread.sleep(100);
        Thread.sleep(200);
        Thread.sleep(300);
        System.out.println("后面的业务");

        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) + " 毫秒");
    }
}

运行的输出结果

java 复制代码
前面的业务
后面的业务
耗时:625 毫秒

案例1

每次的休眠代表一个可独立执行的业务代码,从上看就知道耗时至少是500毫秒。那么,我们可以使用Future的方式来达到目的。

java 复制代码
public class FutureThreadPoolDemo {
    @SneakyThrows
    public static void main(String[] args) {
        long startTime = System.currentTimeMillis();
        System.out.println("前面的业务");

        FutureTask<String> futureTask = new FutureTask<>(() -> {
            Thread.sleep(100);
            return "任务1执行完毕";
        });
        new Thread(futureTask).start();
        FutureTask<String> futureTask2 = new FutureTask<>(() -> {
            Thread.sleep(100);
            return "任务2执行完毕";
        });
        new Thread(futureTask2).start();
        FutureTask<String> futureTask3 = new FutureTask<>(() -> {
            Thread.sleep(100);
            return "任务3执行完毕";
        });
        new Thread(futureTask3).start();

        System.out.println("后面的业务");
        System.out.println("获取输出值:");
        System.out.println(futureTask.get());
        System.out.println(futureTask2.get());
        System.out.println(futureTask3.get());
        long endTime = System.currentTimeMillis();
        System.out.println("耗时:" + (endTime - startTime) + " 毫秒");
    }
}

很明显,这个的耗时是能够下降。

java 复制代码
前面的业务
后面的业务
获取输出值:
任务1执行完毕
任务2执行完毕
任务3执行完毕
耗时:149 毫秒

但是上面的写法并不好,如果我们有很多个任务,那岂不是需要new很多个线程。这样会导致性能降低。所以这里我们需要使用线程池,尽量不要用new Thread。

java 复制代码
public static void main(String[] args) {
    ExecutorService threadPool = Executors.newFixedThreadPool(3);
    long startTime = System.currentTimeMillis();
    System.out.println("前面的业务");
    FutureTask<String> futureTask = new FutureTask<>(() -> {
        Thread.sleep(100);
        return "任务1执行完毕";
    });
    threadPool.submit(futureTask);
    FutureTask<String> futureTask2 = new FutureTask<>(() -> {
        Thread.sleep(100);
        return "任务2执行完毕";
    });
    threadPool.submit(futureTask2);
    FutureTask<String> futureTask3 = new FutureTask<>(() -> {
        Thread.sleep(100);
        return "任务3执行完毕";
    });
    threadPool.submit(futureTask3);
    System.out.println("后面的业务");
    System.out.println("获取输出值:");
    System.out.println(futureTask.get());
    System.out.println(futureTask2.get());
    System.out.println(futureTask3.get());
    long endTime = System.currentTimeMillis();
    System.out.println("耗时:" + (endTime - startTime) + " 毫秒");
    threadPool.shutdown(); // 资源释放
}

通过使用线程池,就能是的线程复用。从上面的结果对比,使用Future结合多线程的案例会比原来的方式耗时少得多。

案例2

我们再来看另一个案例

java 复制代码
public class FutureDemo2 {
    @SneakyThrows
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<>(() -> {
            Thread.sleep(3000);
            return "执行完毕";
        });
        new Thread(futureTask).start();
        System.out.println(">>>> 继续执行其他业务");
        System.out.println(futureTask.get());
    }
}

执行结果

java 复制代码
>>>> 继续执行其他业务
执行完毕

这个简单得案例是个常规案例,我们到最后获取异步任务得返回值,因为是在最后才调用得get(),这样是会如愿的执行其他任务,到最后等到异步任务执行完之后拿到返回值。

这回,我们把后面两句做一个对调:

java 复制代码
public class FutureDemo2 {
    @SneakyThrows
    public static void main(String[] args) {
        FutureTask<String> futureTask = new FutureTask<>(() -> {
            Thread.sleep(3000);
            return "执行完毕";
        });
        new Thread(futureTask).start();
        System.out.println(futureTask.get());
        System.out.println(">>>> 继续执行其他业务");
    }
}

执行结果

java 复制代码
执行完毕
>>>> 继续执行其他业务

和上面对比,明显的不一样,在执行其他任务前调用了get(),会导致主线程的后续任务要等待异步任务的执行完毕才能够往下执行,这就是get()导致了主线程的阻塞。

我们从上面get方法的源码可以看到,get()方法会判断是否执行完成,没有执行完成将会阻塞,知道任务完成后拿到返回值,主线程才能够继续执行。这样就违背了多任务的初心了。我们知道,get还有另一个含参的方法,分别是携带数值和单位,表示等待的时间,如果超过这个时间,就会中断阻塞。

而在Future的API中,提供了isDone()方法,用来判断程序是否执行完成,可以通过不断轮询去判断,知道完成后调用get获取,但是这样的操作会导致浪费CPU资源。

总结

在使用Future模式时,我们需要遵循一些最佳实践。首先,我们应该尽可能早地创建Future对象,以便尽可能早地开始异步操作。其次,我们应该避免在一个Future对象上进行多次等待,因为这会浪费CPU时间。最后,我们应该在Future对象上注册回调函数,以便在异步操作完成时立即处理结果,而不是等待结果后再进行处理。


转发请携带作者信息 @怒放吧德德 @一个有梦有戏的人

持续创作很不容易,作者将以尽可能的详细把所学知识分享各位开发者,一起进步一起学习。

👍创作不易,如有错误请指正,感谢观看!记得点赞哦!👍

谢谢支持!

相关推荐
黑胡子大叔的小屋15 分钟前
基于springboot的海洋知识服务平台的设计与实现
java·spring boot·毕业设计
ThisIsClark18 分钟前
【后端面试总结】深入解析进程和线程的区别
java·jvm·面试
星就前端叭1 小时前
【开源】一款基于Vue3 + WebRTC + Node + SRS + FFmpeg搭建的直播间项目
前端·后端·开源·webrtc
雷神乐乐1 小时前
Spring学习(一)——Sping-XML
java·学习·spring
小林coding2 小时前
阿里云 Java 后端一面,什么难度?
java·后端·mysql·spring·阿里云
AI理性派思考者2 小时前
【保姆教程】手把手教你在Linux系统搭建早期alpha项目cysic的验证者&证明者
后端·github·gpu
V+zmm101342 小时前
基于小程序宿舍报修系统的设计与实现ssm+论文源码调试讲解
java·小程序·毕业设计·mvc·ssm
测试19982 小时前
外包干了2年,技术退步明显....
自动化测试·软件测试·python·功能测试·测试工具·面试·职场和发展
从善若水2 小时前
【2024】Merry Christmas!一起用Rust绘制一颗圣诞树吧
开发语言·后端·rust
文大。2 小时前
2024年广西职工职业技能大赛-Spring
java·spring·网络安全