Java线程池以及Future和CompletableFuture的用法

参考:https://blog.csdn.net/weixin_50330544/article/details/131687150

1.线程池

为什么使用线程池?
频繁申请/销毁资源和调度资源,将带来额外的消耗,可能会非常巨大。
对资源无限申请缺少抑制手段,易引发系统资源耗尽的风险。
系统无法合理管理内部的资源分布,会降低系统的稳定性。

使用线程池的好处?
重用存在的线程,减少对象创建、消亡的开销。
有效的控制最大并发数,提高系统资源使用率。
统一的分配、调优和监控、可定时执行、定期执行。

线程池所在包java.util.concurrent
顶级接口Executor,真正的线程池接口是ExecutorService
Executors类提供创建线程池的方法
image
Java内置的线程池
image
image
image
自定义线程池
image
image
image
当线程池满了之后就会执行拒绝策略。线程池数量等于最大线程数+阻塞队列数
1、AbortPolicy
当任务添加到线程池中被拒绝时,它将抛出RejectedExecutionException异常。(该策路下,直接丢弃任务,并抛出RejectedExecutionException异常)
2、DiscardPolicy
当任务添加到线程池中被拒绝时,默认情况下它将丢弃被拒绝的任务。(即该策略下,直接丢弃任务,什么都不做)
3.DiscardoldestPolicy
当任务添加到线程池中被拒绝时,线程池会放弃等待队列中最旧的未处理任务,然后将被拒绝的任务添加到等待队列中。(该策略下,抛弃进入队列最早的那个任务,然后尝试把这次拒绝的任务放入队列)
4、CallerRunsPolicy
不进入线程池执行,在这种方式(CallerRunsPolicy)中,任务将由调用者线程去执行。(用于被拒绝任务的处理程序,它直接在execute方法的调用线程中运行被拒绝的任务:如果执行程序已关闭,则会丢弃该任务。)

2.Future和CompletableFuture的用法

在Java多线程编程中,Future是一个接口,用于表示一个异步计算的结果。当启动callable线程时,就可以声明一个Future,用于接收返回结果。它提供了一系列的方法,用于管理和获取任务的执行结果。Future接口定义在java.util.concurrent包中。
使用Future可以将任务提交给线程池执行,并在需要时获取任务的执行结果。它的主要作用是允许主线程在提交异步任务后,继续执行其他操作,而不需要等待任务执行完成。
下面是Future接口的一些常用方法:
1、boolean cancel(boolean mayInterruptIfRunning): 取消任务的执行。如果任务正在执行,并且2、mayInterruptIfRunning参数设置为true,则会尝试中断任务的执行。
3、boolean isCancelled(): 判断任务是否已被取消。
4、boolean isDone(): 判断任务是否已经完成。
5、V get() throws InterruptedException, ExecutionException: 获取任务的执行结果。如果任务还未执行完毕,get()方法会阻塞直到任务执行完成。如果任务执行过程中发生异常,get()方法会抛出ExecutionException,可以通过getCause()方法获取具体的异常信息。
6、V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException: 在指定的时间内获取任务的执行结果,如果任务在指定时间内未执行完毕,会抛出TimeoutException异常。
通过Future接口,我们可以方便地提交任务并获取任务执行结果。这对于需要处理耗时操作的应用程序非常有用,可以提高系统的并发性和响应性。
栗子:四个刚需(线程)去买房摇号,future获取摇号结果。摇号结果未出,就一直阻塞。

public class FutureTest {
 
    /**
     * 买房摇号
     */
    public static class Yaohao implements Callable<Integer> {
        /**
         * 返回摇号结果
         * @return 0:中签   1:没中
         * @throws Exception
         */
        @Override
        public Integer call() throws Exception {
            Random random = new Random();
            //模拟摇号,10天内出结果
            TimeUnit.SECONDS.sleep(random.nextInt(10));
            int result = random.nextInt(2);
            System.out.println("     "+Thread.currentThread().getName()+" is done!");
            return result;
        }
    }
 
    public static void main(String[] args) throws InterruptedException, ExecutionException {
 
        Yaohao gangxu1 = new Yaohao();
        Yaohao gangxu2 = new Yaohao();
        Yaohao gangxu3 = new Yaohao();
        Yaohao gangxu4 = new Yaohao();
        ExecutorService es = Executors.newCachedThreadPool();
        Future<Integer> result1 = es.submit(gangxu1);
        Future<Integer> result2 = es.submit(gangxu2);
        Future<Integer> result3 = es.submit(gangxu3);
        Future<Integer> result4 = es.submit(gangxu4);
        es.shutdown();
 
        System.out.println("刚需1,摇号结果:"+(result1.get()==1?"中签":"没中"));
        System.out.println("刚需2,摇号结果:"+(result2.get()==1?"中签":"没中"));
        System.out.println("刚需3,摇号结果:"+(result3.get()==1?"中签":"没中"));
        System.out.println("刚需4,摇号结果:"+(result4.get()==1?"中签":"没中"));
    }
 
}

两个例子

public class FutureTest {

    public static void main(String[] args) throws ExecutionException, InterruptedException {

        ExecutorService executorService = Executors.newFixedThreadPool(10);

        UserInfoService userInfoService = new UserInfoService();
        MedalService medalService = new MedalService();
        long userId =666L;
        long startTime = System.currentTimeMillis();

        //调用用户服务获取用户基本信息
        FutureTask<UserInfo> userInfoFutureTask = new FutureTask<>(new Callable<UserInfo>() {
            @Override
            public UserInfo call() throws Exception {
                return userInfoService.getUserInfo(userId);
            }
        });
        executorService.submit(userInfoFutureTask);

        Thread.sleep(300); //模拟主线程其它操作耗时

        FutureTask<MedalInfo> medalInfoFutureTask = new FutureTask<>(new Callable<MedalInfo>() {
            @Override
            public MedalInfo call() throws Exception {
                return medalService.getMedalInfo(userId);
            }
        });
        executorService.submit(medalInfoFutureTask);

        UserInfo userInfo = userInfoFutureTask.get();//获取个人信息结果
        MedalInfo medalInfo = medalInfoFutureTask.get();//获取勋章信息结果

        System.out.println("总共用时" + (System.currentTimeMillis() - startTime) + "ms");
    }
}

FutureTask和Future的区别

1、功能不同:Future是一个接口,用于表示一个异步计算的结果,可以用来取消任务、查询结果是否完成以及获取计算结果。FutureTask是一个实现了Future接口的具体类,同时也是一个可执行的任务,可以被Executor执行。

2、使用方式不同:Future通常与ExecutorService一起使用,用来提交任务并获取任务的执行结果。FutureTask既可以直接使用FutureTask对象来提交任务,也可以将其作为Runnable或Callable对象提交给ExecutorService。

3、创建方式不同:创建Future对象时,通常使用ExecutorService提交任务后返回的Future实例。而创建FutureTask对象时,可以直接通过new FutureTask(callable)构造函数或new FutureTask(runnable, result)构造函数来实例化。

4、取消任务的能力不同:Future接口提供了cancel(boolean mayInterruptIfRunning)方法,用于取消任务的执行。而FutureTask类还提供了cancel(boolean mayInterruptIfRunning)方法以及boolean cancel(boolean mayInterruptIfRunning)方法,用于取消任务的执行并返回取消成功与否的信息。

总的来说,Future是一个接口,用于表示一个异步计算的结果;FutureTask是Future接口的具体实现类,同时也是一个可执行的任务。FutureTask相比Future更为灵活,能够直接作为任务提交给ExecutorService执行,并提供了更多的取消任务的方法。

ExecutorService 的submit方法的作用
ExecutorService 是 Java 提供的一个用于管理线程池的接口。它可以用来执行异步任务并管理线程的生命周期。submit 方法是 ExecutorService 接口定义的一个方法,用于向线程池提交一个任务并获取一个 Future 对象来表示任务的执行结果。

具体来说,submit 方法用于提交一个 Callable 或 Runnable 对象到线程池中执行。Callable 是带有返回结果的任务,而 Runnable 是没有返回结果的任务。submit 方法将任务提交给线程池后会立即返回一个 Future 对象,通过这个对象可以获得任务的执行结果或者取消任务的执行。

使用 submit 方法可以方便地管理线程池中的任务,可以通过 Future 对象获取任务的状态、结果或者取消任务的执行。通常情况下,我们可以使用 submit 方法来替代 execute 方法,因为它具有更多的功能和灵活性。
submit 和execute 的区别
submit() 和 execute() 是 ExecutorService 接口用于提交任务到线程池的两种方法,它们之间有以下区别:

1、返回值类型不同: submit() 方法返回一个 Future 对象,可以用来获取任务的执行结果或者取消任务的执行;而 execute() 方法没有返回值,无法获取任务的执行结果。

2、异常处理不同: submit() 方法能够捕获任务执行过程中抛出的异常,将异常封装到 Future 对象中,通过调用 get() 方法或者 get(long, TimeUnit) 方法获取任务的执行结果时,如果任务抛出了异常,可以通过 ExecutionException 来获取异常信息;而 execute() 方法无法对任务抛出的异常进行处理,如果任务抛出了异常,线程池会将异常记录到控制台。

3、参数类型不同: submit() 方法接受 Callable 或者 Runnable 对象作为参数,Callable 是带有返回值的任务,而 Runnable 是没有返回值的任务;而 execute() 方法只接受 Runnable 对象作为参数。因此,如果需要获取任务执行的结果,应该使用 submit() 方法,如果不需要获取结果,可以使用 execute() 方法。

总的来说,submit() 方法比 execute() 方法更加灵活,可以获取任务的执行结果和捕获任务中抛出的异常,因此在处理需要获取结果或者处理异常的任务时,推荐使用 submit() 方法。而对于不需要获取结果的简单任务,可以使用 execute() 方法。

Future的局限
Future对于结果的获取,不是很友好,只能通过阻塞或者轮询的方式得到任务的结果。
Future.get() 就是阻塞调用,在线程获取结果之前get方法会一直阻塞。Future提供了一个isDone方法,可以在程序中轮询这个方法查询执行结果。
阻塞的方式和异步编程的设计理念相违背,而轮询的方式会耗费无谓的CPU资源。因此,JDK8设计出CompletableFuture,提供了一种观察者模式类似的机制,可以让任务执行完成后通知监听的一方。

CompletableFuture 使用详解(重要)

参考:https://www.jianshu.com/p/6bac52527ca4
https://blog.csdn.net/sermonlizhi/article/details/123356877
https://juejin.cn/post/6970558076642394142
image
CompletableFuture提供了多种方法来获取异步计算的结果。以下是一些常用的方法:

使用get()方法:CompletableFuture类继承了Future接口,因此可以使用get()方法来获取计算的结果。但需要注意的是,get()方法是阻塞的,会等待异步计算完成后返回结果。如果计算没有完成,get()方法会一直阻塞,直到计算完成并返回结果或者抛出异常。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
String result = future.get(); // 阻塞获取计算结果

使用join()方法:与get()方法类似,join()方法也可以获取计算结果,但它是非阻塞的。如果计算没有完成,join()方法会等待计算完成后立即返回结果。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello");
String result = future.join(); // 非阻塞获取计算结果

使用CompletableFuture组合方法:CompletableFuture提供了一系列方法,如thenApply、thenAccept、thenCompose等,用于对计算结果进行处理。这些方法返回的是新的CompletableFuture实例,可以继续链式调用。通过这些方法,我们可以在异步计算完成后,对结果进行进一步的操作或处理。

CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(s -> s + " World")
        .thenApply(String::toUpperCase);
String result = future.get();

使用回调方法:CompletableFuture还提供了一系列回调方法,如whenComplete、handle、thenAccept、exceptionally等,用于在计算完成后执行相应的操作或处理。这些方法允许我们以非阻塞的方式处理计算结果,并在计算完成或出现异常时执行相应的逻辑。

CompletableFuture.supplyAsync(() -> "Hello")
        .thenApply(s -> s + " World")
        .thenAccept(System.out::println)
        .exceptionally(ex -> {
            System.out.println("Error: " + ex.getMessage());
            return null;
        });

案例:
1、CompletableFuture默认使用的线程池是 ForkJoinPool.commonPool(),commonPool是当前 JVM(进程) 上的所有 CompletableFuture、并行 Stream 共享的,commonPool 的目标场景是非阻塞的 CPU 密集型任务,其线程数默认为 CPU 数量减1,所以对于我们用java常做的IO密集型任务,默认线程池是远远不够使用的
2、CompletableFuture是否使用默认线程池的依据,和机器的CPU核心数有关。当CPU核心数-1>1时,才会使用默认的线程池,否则将会为每个CompletableFuture的任务创建一个新线程去执行。
也就是说,CompletableFuture的默认线程池,只有在双核以上的机器内才会使用。在双核及以下的机器中,会为每个任务创建一个新线程,等于没有使用线程池,且有资源耗尽的风险。
原文链接:https://blog.csdn.net/IndexMan/article/details/134720625
在使用CompletableFuture时,务必要自定义线程池。

@Configuration
public class ThreadPoolConfig {
    //参数初始化
    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    //核心线程数量大小
    private static final int corePoolSize = Math.max(2, Math.min(CPU_COUNT-1,4));
    //线程池最大容纳线程数
    private static final int maxPoolSize = CPU_COUNT * 2 + 1;
    //阻塞队列
    private static final int workQueue = 20;
    //线程空闲后的存活时长
    private static final int keepAliveTime = 30;

    @Bean("asyncTaskExecutor")
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        //核心线程数
        threadPoolTaskExecutor.setCorePoolSize(corePoolSize);
        //最大线程数
        threadPoolTaskExecutor.setMaxPoolSize(maxPoolSize);
        //等待队列
        threadPoolTaskExecutor.setQueueCapacity(workQueue);
        //线程前缀
        threadPoolTaskExecutor.setThreadNamePrefix("asyncTaskExecutor-");
        //线程池维护线程所允许的空闲时间,单位为秒
        threadPoolTaskExecutor.setKeepAliveSeconds(keepAliveTime);
        // 线程池对拒绝任务(无线程可用)的处理策略
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();

        return threadPoolTaskExecutor;
    }
}

在CompletableFuture中指定自定义线程池

@RestController
@RequestMapping("/task")
public class CompletableTaskController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    @Qualifier("asyncTaskExecutor")
    private Executor asyncTaskExecutor;

    @RequestMapping("testOrderTask")
    public String testOrderTask(){
        List<CompletableFuture<List<Integer>>> futureList = Lists.newArrayList();
        // 任务1,计算3秒
        CompletableFuture<List<Integer>> task1 = CompletableFuture.supplyAsync(() -> {
            sleepSeconds(3L);
            return Lists.newArrayList(1,2,3);
        }, asyncTaskExecutor);
        futureList.add(task1);

        // 任务2,计算2秒,得答案5
        CompletableFuture<List<Integer>> task2 = CompletableFuture.supplyAsync(() -> {
            sleepSeconds(1L);
            return Lists.newArrayList(4,5,6);
        }, asyncTaskExecutor);
        futureList.add(task2);

        // 任务3,计算3秒,得答案5
        CompletableFuture<List<Integer>> task3 = CompletableFuture.supplyAsync(() -> {
            sleepSeconds(2L);
            return Lists.newArrayList(7,8,9);
        }, asyncTaskExecutor);
        futureList.add(task3);
        // 写法1
        List<Integer> newList = futureList.stream().map(CompletableFuture::join).flatMap(List::stream).collect(Collectors.toList());
        return JSON.toJSONString(newList);
    }

    private void sleepSeconds(long seconds) {
        try {
            Thread.sleep(seconds * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

设置CompletableFuture设置异步超时的方式

下面举例一个常见场景。

假如我们有两个 RPC 远程调用服务,我们需要获取两个 RPC 的结果后,再进行后续逻辑处理。

public static void main(String[] args) {
    // 任务 A,耗时 2 秒
    int resultA = compute(1);
    // 任务 B,耗时 2 秒
    int resultB = compute(2);

    // 后续业务逻辑处理
    System.out.println(resultA + resultB);
}

可以预估到,串行执行最少耗时 4 秒,并且 B 任务并不依赖 A 任务结果。

对于这种场景,我们通常会选择并行的方式优化,Demo 代码如下:

public static void main(String[] args) {
    // 仅简单举例,在生产代码中可别这么写!

    // 统计耗时的函数
    time(() -> {
        CompletableFuture<Integer> result = Stream.of(1, 2)
                                                  // 创建异步任务
                                                  .map(x -> CompletableFuture.supplyAsync(() -> compute(x), executor))
                                                  // 聚合
                                                  .reduce(CompletableFuture.completedFuture(0), (x, y) -> x.thenCombineAsync(y, Integer::sum, executor));

        // 等待结果
        try {
            System.out.println("结果:" + result.get());
        } catch (ExecutionException | InterruptedException e) {
            System.err.println("任务执行异常");
        }
    });
}

输出:
[async-1]: 任务执行开始:1
[async-2]: 任务执行开始:2
[async-1]: 任务执行完成:1
[async-2]: 任务执行完成:2
结果:3
耗时:2 秒

看上去 CompletableFuture 现有功能可以满足我们诉求。但当我们引入一些现实常见情况时,一些潜在的不足便暴露出来了。
compute(x) 如果是一个根据入参查询用户某类型优惠券列表的任务,我们需要查询两种优惠券并组合在一起返回给上游。假如上游要求我们 2 秒内处理完毕并返回结果,但 compute(x) 耗时却在 0.5 秒 ~ 无穷大波动。这时候我们就需要把耗时过长的 compute(x) 任务结果放弃,仅处理在指定时间内完成的任务,尽可能保证服务可用。
那么以上代码的耗时由耗时最长的服务决定,无法满足现有诉求。通常我们会使用 get(long timeout, TimeUnit unit) 来指定获取结果的超时时间,并且我们会给 compute(x) 设置一个超时时间,达到后自动抛异常来中断任务。

解决方法

在 JDK 9,CompletableFuture 正式提供了 orTimeout、completeTimeout 方法,来准确实现异步超时控制

public CompletableFuture<T> orTimeout(long timeout, TimeUnit unit) {
    if (unit == null)
        throw new NullPointerException();
    if (result == null)
        whenComplete(new Canceller(Delayer.delay(new Timeout(this), timeout, unit)));
    return this;
}

JDK 9 orTimeout 其实现原理是通过一个定时任务,在给定时间之后抛出异常。如果任务在指定时间内完成,则取消抛异常的操作。

以上代码我们按执行顺序来看下:

首先执行 new Timeout(this)。

static final class Timeout implements Runnable {
    final CompletableFuture<?> f;
    Timeout(CompletableFuture<?> f) { this.f = f; }
    public void run() {
        if (f != null && !f.isDone())
            // 抛出超时异常
            f.completeExceptionally(new TimeoutException());
    }
}

通过源码可以看到,Timeout 是一个实现 Runnable 的类,run() 方法负责给传入的异步任务通过 completeExceptionally CAS 赋值异常,将任务标记为异常完成。
那么谁来触发这个 run() 方法呢?我们看下 Delayer 的实现。

static final class Delayer {
    static ScheduledFuture<?> delay(Runnable command, long delay,
                                    TimeUnit unit) {
        // 到时间触发 command 任务
        return delayer.schedule(command, delay, unit);
    }

    static final class DaemonThreadFactory implements ThreadFactory {
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r);
            t.setDaemon(true);
            t.setName("CompletableFutureDelayScheduler");
            return t;
        }
    }

    static final ScheduledThreadPoolExecutor delayer;
    static {
        (delayer = new ScheduledThreadPoolExecutor(
            1, new DaemonThreadFactory())).
            setRemoveOnCancelPolicy(true);
    }
}

Delayer 其实就是一个单例定时调度器,Delayer.delay(new Timeout(this), timeout, unit) 通过 ScheduledThreadPoolExecutor 实现指定时间后触发 Timeout 的 run() 方法。
到这里就已经实现了超时抛出异常的操作。但当任务完成时,就没必要触发 Timeout 了。因此我们还需要实现一个取消逻辑。

static final class Canceller implements BiConsumer<Object, Throwable> {
    final Future<?> f;
    Canceller(Future<?> f) { this.f = f; }
    public void accept(Object ignore, Throwable ex) {
        if (ex == null && f != null && !f.isDone())
        // 3 未触发抛异常任务则取消
            f.cancel(false);
    }
}

当任务执行完成,或者任务执行异常时,我们也就没必要抛出超时异常了。因此我们可以把 delayer.schedule(command, delay, unit) 返回的定时超时任务取消,不再触发 Timeout。 当我们的异步任务完成,并且定时超时任务未完成的时候,就是我们取消的时机。因此我们可以通过 whenComplete(BiConsumer<? super T, ? super Throwable> action) 来完成。
Canceller 就是一个 BiConsumer 的实现。其持有了 delayer.schedule(command, delay, unit) 返回的定时超时任务,accept(Object ignore, Throwable ex) 实现了定时超时任务未完成后,执行 cancel(boolean mayInterruptIfRunning) 取消任务的操作。

jdk8如何实现

以下是自己的工具类以及用法

调用方式:

CompletableFutureExpandUtils.orTimeout(异步任务, 超时时间, 时间单位);

工具类:

package com.jd.jr.market.reduction.util;

import com.jdpay.market.common.exception.UncheckedException;

import java.util.concurrent.*;
import java.util.function.BiConsumer;

/**
 * CompletableFuture 扩展工具
 *
 * @author zhangtianci7
 */
public class CompletableFutureExpandUtils {

    /**
     * 如果在给定超时之前未完成,则异常完成此 CompletableFuture 并抛出 {@link TimeoutException} 。
     *
     * @param timeout 在出现 TimeoutException 异常完成之前等待多长时间,以 {@code unit} 为单位
     * @param unit    一个 {@link TimeUnit},结合 {@code timeout} 参数,表示给定粒度单位的持续时间
     * @return 入参的 CompletableFuture
     */
    public static <T> CompletableFuture<T> orTimeout(CompletableFuture<T> future, long timeout, TimeUnit unit) {
        if (null == unit) {
            throw new UncheckedException("时间的给定粒度不能为空");
        }
        if (null == future) {
            throw new UncheckedException("异步任务不能为空");
        }
        if (future.isDone()) {
            return future;
        }

        return future.whenComplete(new Canceller(Delayer.delay(new Timeout(future), timeout, unit)));
    }

    /**
     * 超时时异常完成的操作
     */
    static final class Timeout implements Runnable {
        final CompletableFuture<?> future;

        Timeout(CompletableFuture<?> future) {
            this.future = future;
        }

        public void run() {
            if (null != future && !future.isDone()) {
                future.completeExceptionally(new TimeoutException());
            }
        }
    }

    /**
     * 取消不需要的超时的操作
     */
    static final class Canceller implements BiConsumer<Object, Throwable> {
        final Future<?> future;

        Canceller(Future<?> future) {
            this.future = future;
        }

        public void accept(Object ignore, Throwable ex) {
            if (null == ex && null != future && !future.isDone()) {
                future.cancel(false);
            }
        }
    }

    /**
     * 单例延迟调度器,仅用于启动和取消任务,一个线程就足够
     */
    static final class Delayer {
        static ScheduledFuture<?> delay(Runnable command, long delay, TimeUnit unit) {
            return delayer.schedule(command, delay, unit);
        }

        static final class DaemonThreadFactory implements ThreadFactory {
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setDaemon(true);
                t.setName("CompletableFutureExpandUtilsDelayScheduler");
                return t;
            }
        }

        static final ScheduledThreadPoolExecutor delayer;

        static {
            delayer = new ScheduledThreadPoolExecutor(1, new DaemonThreadFactory());
            delayer.setRemoveOnCancelPolicy(true);
        }
    }
}
posted @ 2024-06-12 18:14  spiderMan1-1  阅读(12)  评论(0编辑  收藏  举报