[java并发] java高并发系列 - 第19天:JUC中的Executor框架详解1

原文链接:查看原文

感谢公众号“ 路人甲Java”的分享,如有冒犯,请联系删除,快去关注他吧
在这里插入图片描述

本文主要内容

  1. 介绍Executor框架相关内容
  2. 介绍Executor
  3. 介绍ExecutorService
  4. 介绍线程池ThreadPoolExecutor
  5. 介绍定时器ScheduledExecutorService及案例
  6. 介绍Executors类的使用
  7. 介绍Future接口
  8. 介绍Callable接口
  9. 介绍FutureTask接口
  10. 获取异步任务的执行结果的几种方法

Executors框架介绍

Executors框架是Doug Lea神作,通过这个框架,可以很容易的使用线程高效地处理并行任务。

Executors框架主要包含3部分内容:

  1. 任务相关的:包含被执行的任务要实现的接口 Runnable 接口或 Callable接口
  2. 任务的执行相关的:包含任务执行机制的 核心接口Executor,以及继承自ExecutorExecutorService 接口。Executor框架中有两个关键的类实现了ExecutorService接口(ThreadPoolExecutor 和 ScheduleThreadPoolExecutor)
  3. 异步计算结果相关的:包含 接口Future实现Future接口的FutureTask类

Executors框架包括:

  • Executor
  • ExecutorService
  • ThreadPoolExecutor
  • Executors
  • Future
  • Callable
  • FutureTask
  • CompletableFuture
  • CompletionService
  • ExecutorCompletionService

下面我们来一一介绍其用途和使用方法。


Executor接口

Executor接口中定义了方法execute(Runnable able)接口,该方法接受一个Runnable实例,他来执行一个任务,任务即实现一个Runnable接口的类。


ExecutorService接口

ExecutorService继承于Executor接口,他提供了更为丰富的线程实现方法,比如ExecutorService提供关闭自己的方法,以及为跟踪一个或多个异步任务执行状况而生成Future方法。

ExecutorService有三种状态:运行、关闭、终止。创建后便进入运行状态,当调用了shutDown()方法时,便进入了关闭状态,此时意味着ExecutorService不再接受新任务,但是他还是会执行已经提交的任务,当所有已经提交了的任务执行完毕后,便达到终止状态。如果不调用shutDown方法,ExecutorService方法会一直运行下去,系统一般不会主动关闭。


ThreadPoolExecutor类

线程池类,实现了ExecutorService接口中所有方法,该类也是我们经常用到的,非常重要,关于此类有详细的介绍,可以移步玩转java中的线程池


ShceduleThreadPoolExecutor定时器

ScheduleThreadPoolExecutor继承自SchedleThreadPoolExecutor,它主要用来延迟执行任务,或者定时执行任务。功能和Timer类似,但是ScheduleThreadPoolExecutor更强大、更灵活一些。Timer后台是单线程,而ScheduleThreadPoolExecutor可以在创建的时候指定多个线程。

常用方法介绍

schedule:延迟执行任务一次

使用ScheduleThreadPoolExecutor的schedule方法,看一下这个方法的声明:

 public ScheduleFuture<?> schedule(Runnable command,long delay,TimeUnit unit)

三个参数:
command:需要执行的任务
delay:需要延迟的时间
unit:参数2的时间单位,是个枚举

是不是等了很久,我也是,来个示例代码:

package aboutThread.Concurrent.Day19;

import java.sql.Time;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Demo1 {
    public static void main(String[] args) throws ExecutionException,InterruptedException{
        System.out.println(System.currentTimeMillis());
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.schedule(() ->{
                System.out.println(System.currentTimeMillis()  + " ->开始执行!");
                //模拟任务耗时
                try {
                    TimeUnit.SECONDS.sleep(3);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(System.currentTimeMillis() + " ->执行结束");
        }, 2,TimeUnit.SECONDS);
    }
}

输出:

1591327780930
1591327782948 ->开始执行!
1591327785952 ->执行结束

scheduleAtFixedRate:固定的频率执行任务

使用ScheduleThreadPoolExecutor的 scheduledAtFixedRate 方法,该方法设置了执行周期,下一次执行时间相当于上一次的执行时间加上period,任务每次执行完毕之后会计算下次的执行时间。

看一下这个方法 的生命:

public SheduleFuture<?> scheduleAtFixedRate(Runnable command,
											long initialDelay,
											long period.
											TimeUnit unit);

4个参数:
command:表示要执行的任务
intialDelay:表示延迟多久执行一次
period:连续执行之间的时间间隔
unit:参数2和参数3的时间单位

假设系统调用scheduleAtFixedRate的时间是T1,那个执行如下:

第一次,T1 + initialDelay
第二次,T1 + initialDelay + period
第三次,T1 + initialDelay + 2 * period
第n次,T1 + initialDelay + (n-1) * period

示例代码:


package aboutThread.Concurrent.Day19;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
 
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo2 {
    public static void main(String[] args) throws ExecutionException, InterruptedException{
        System.out.println(System.currentTimeMillis());
        //任务执行计数器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleAtFixedRate(() ->{
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + " 第" + currCount + "次,开始执行");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "第" + currCount + "执行结束");
        },1 , 1, TimeUnit.SECONDS);
    }
}

部分输出结果:

pool-1-thread-1
1591329477725 第1次,开始执行
1591329479729第1执行结束
pool-1-thread-1
1591329479730 第2次,开始执行
1591329481735第2执行结束
pool-1-thread-2
1591329481735 第3次,开始执行
1591329483740第3执行结束
pool-1-thread-1
1591329483740 第4次,开始执行
1591329485745第4执行结束
pool-1-thread-3
1591329485745 第5次,开始执行
1591329487750第5执行结束
pool-1-thread-2
1591329487751 第6次,开始执行
1591329489756第6执行结束

代码中设置的任务第一次执行时间是系统启动之后延迟一秒执行。后面每次时间间隔1秒,从输出中可以看出系统启动之后过了1秒任务第一次执行(1、3行输出),输出结果中可以看到任务第一次执行结束和第二次执行结束时间一样,为什么会这样呢?前面有介绍,任务当前执行完完毕后会计算下次执行时间,下次执行时间为上次执行的开始时间 + period,第一次开始执行的时间是1591329477725,加一秒是1591329478725,这个时间小于第一次结束的时间了,说明小于系统当前时间了,会立即执行


scheduleWithFixedDelay:固定的间隔执行任务

使用 scheduleThreadPoolExecutorscheduleWithFixedDelay 方法,该方法设置了执行周期,与scheduleAtFixedRate方法不同的是,下一次执行的时间是上一次任务执行完的的系统时间加上period,因而具体执行时间不是固定的,但周期是固定的,是采用相对固定的延迟来执行任务的。看一下这个方法的声明:

public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

4个参数:
coommand:表示要执行的任务
initialDelay:表示延迟多久执行一次
period:表示下一次执行时间和上次执行结束时间之间的时间间隔
unit:参数2和参数3的时间单位

假设系统调用scheduleWithFixedDelay的时间是T1,那么执行时间如下:

  • 第一次,T1 + initialDelay,执行结束时间E1
  • 第二次,E1 + period,执行结束时间E2
  • 第三次,E2 + period,执行结束时间E3
  • 第四次,E3+ period,执行结束时间,E4
  • 第n次,上次执行结束时间 + period

示例代码:

package aboutThread.Concurrent.Day19;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo3 {
    public static void main(String[] args){
        System.out.println(System.currentTimeMillis());
        //任务执行计数器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        scheduledExecutorService.scheduleWithFixedDelay(() -> {
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次 开始执行!"); 
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                    e.printStackTrace();
            }   
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次 执行结束!");    
        }, 1,3,TimeUnit.SECONDS);           
    }
}

输出:

1591338307273
pool-1-thread-1
1591338308290第1次 开始执行!
1591338310295第1次 执行结束!
pool-1-thread-1
1591338313300第2次 开始执行!
1591338315302第2次 执行结束!
pool-1-thread-2
1591338318306第3次 开始执行!
1591338320311第3次 执行结束!
pool-1-thread-1
1591338323316第4次 开始执行!
1591338325318第4次 执行结束!

延迟1秒之后执行第一次,后面每次的执行时间和上次执行结束时间间隔3秒

scheduleAtFixedRatescheduleWithFixedDelay 示例建议多看2遍


定时任务有异常会怎样?

package aboutThread.Concurrent.Day19;

import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo4 {
    public static void main(String[] args) throws InterruptedException{
        System.out.println(System.currentTimeMillis());
        //任务执行计数器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(10);
        ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() -> {
            int  currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次 开始执行!");
            System.out.println(10 / 0);
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次 执行结束");
        }, 1, 1, TimeUnit.SECONDS);

        TimeUnit.SECONDS.sleep(5);
        System.out.println(scheduledFuture.isCancelled());
        System.out.println(scheduledFuture.isDone());
    }
}

输出:

1591339008280
pool-1-thread-1
1591339009296第1次 开始执行!
false
true

补充知识:

schedule、scheduleAtFixedRate、scheduleWithFixedDelay这个方法有个返回值ScheduledFuture,通过 ScheduleFuture 可以对执行的任务做一些操作,如判断任务是否被取消,是否执行完成。

再回到上面的代码,任务中有个10/0 的操作,会触发异常,发生异常之后没有任何现象,被ScheduledExecutorService内部吞掉了,然后这个任务再也不会执行了, scheduledFuture.isDone() 输出了true,表示这个任务已经结束了,再也不会被执行了。

所以如果程序有异常,开发者注意自己处理一下,不然跑着跑着发现任务怎么不跑了,也没有异常输出


取消定时任务

可能任务执行一会儿,想取消执行,可以调用 ScheduledFuturecancel 方法,参数表示是否给任务发送中断信号。

package aboutThread.Concurrent.Day19;

import java.sql.Time;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class Demo5 {
    public static void main(String[] args) throws InterruptedException{
        System.out.println(System.currentTimeMillis());
        // 任务执行计数器
        AtomicInteger count = new AtomicInteger(1);
        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        ScheduledFuture<?> scheduledFuture = scheduledExecutorService.scheduleWithFixedDelay(() ->{
            int currCount = count.getAndIncrement();
            System.out.println(Thread.currentThread().getName());
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次 开始执行!");
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(System.currentTimeMillis() + "第" + currCount + "次 执行结束!");
        }, 1, 1, TimeUnit.SECONDS);
        TimeUnit.SECONDS.sleep(5);
        scheduledFuture.cancel(false);
        TimeUnit.SECONDS.sleep(1);
        System.out.println("任务是否被取消:" + scheduledFuture.isCancelled());
        System.out.println("任务是否已完成:" + scheduledFuture.isDone());
    }
}

输出:

1591339997274
pool-1-thread-1
1591339998290第1次 开始执行!
1591340000293第1次 执行结束!
pool-1-thread-1
1591340001295第2次 开始执行!
任务是否被取消:true
任务是否已完成:true
1591340003300第2次 执行结束!

输出中可以看到任务被取消成功了。


Executors类

Executors类,提供了一系列工厂方法用于创建线程池,返回的线程池都实现了ExecutorService接口,常用的方法有:

newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor()

public static ExecutorService newSingleThreadExecutor(ThreadFactory factory)

创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行任务。如果这个唯一的线程因为一次昂结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。内部使用了无限量的LinkedBlockingQueue阻塞队列来缓存任务,任务如果比较多,单线程如果处理不过来,会导致队列堆满,引发OOM。


newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads,ThreadFactory threadFactory)

创建固定大小的线程池,每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,再提交新任务,任务就会进入等待队列中等待。如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。内部使用了无限量的LinkedBlockingQueue阻塞队列来缓存任务,任务如果比较多,如果处理不过来,会导致队列堆满,引发OOM。


newCachedThreadPool

public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)

创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒处于等待任务到来)的线程,当任务增加时,此线程池又可以智能的添加新线程来处理任务。此线程池的最大值为Integer的最大值(2^31 - 1)。内部使用了SynchronousQueue同步队列来缓存任务,此队列的特性是放入任务时必须有对应的线程获取任务,任务才可以放入成功。如果处理的任务比较耗时,任务来的速度也比较快,会创建太多的线程引发OOM。


newScheduledThreadPool

public static ScheduledExecutorService newScheduledTheadPool(int corePoolSize)
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize,ThreadFactory threadFactory)

创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

在《阿里巴巴java开发手册》中指出了线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程,这样一方面是线程的创建更加规范,可以合理控制开辟线程的数量;另一方面线程的细节管理交给线程池处理,优化了资源的开销。而线程池不允许使用Executors去创建,而要通过ThreadPoolExecutor方式,这一方面是由于jdk中Executor框架虽然提供了如newFixedThreadPool()、newSingleThreadExecutor()、newCachedThreadPool()等创建线程池的方法,但都有其局限性,不够灵活;另外由于前面几种方法内部也是通过ThreadPoolExecutor方式实现,使用ThreadPoolExecutor有助于大家明确线程池的运行规则,创建符合自己的业务场景需要的线程池,避免资源耗尽的风险。


Future、Callable接口

Future 接口定义了操作异步任务执行一些方法,如果获取异步任务的执行结果、取消任务的执行、判断任务是否被取消、判断任务执行是否完毕 等。

Callable 接口中定义了需要有返回的任务需要实现的方法。


@FunctionalInterface
public interface Callable<V>{
	V call() throws Exeception;
}

比如主线程让一个子线程去执行任务,子线程可能比较耗时,启动子线程开始执行任务后,主线程就去做其他事情了,过了一会儿才去获取子任务的执行结果。


获取异步任务的执行结果

 package aboutThread.Concurrent.Day19;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Demo6 {
    public static void main(String[] args) throws InterruptedException,ExecutionException{
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Integer> result = executorService.submit(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis()  + "," + Thread.currentThread().getName() + " end!");
            return 10;
        });
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "");
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 结果" + result.get());
    }
}

输出:

1591342301141,main
1591342301141,pool-1-thread-1 start!
1591342306143,pool-1-thread-1 end!
1591342301141,main 结果10

代码中创建了一个线程池,调用线程池的 submit 方法执行任务,submit参数为Callable接口:表示需要执行的任务有返回值,submit方法返回一个Future对象,Future相当于一个凭证,可以在任意时间拿着这个凭着去获取对应任务的执行结果(调用其 get 方法),代码中调用了 result.get() 方法之后,此方法会阻塞当前线程直到任务执行结束。


超时获取异步任务执行结果

可能任务执行比较耗时,比如耗时1分钟,我最多只等10秒,如果10秒还没返回,我就去做其他事情了。

刚好get有个超时的方法,声明如下:

V get(long timeout,TimeUnit unit) throws InterruptedException,ExecutionException,TimeOutException;

示例代码

package aboutThread.Concurrent.Day19;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class Demo7 {
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Integer> result = executorService.submit(() -> {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " end!");
            return 10;
        });
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "");
        try {
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 结果"
                    + result.get(3, TimeUnit.SECONDS));

        } catch (TimeoutException e) {
            e.printStackTrace();
        }
    }
}

输出:

1591345695754,main
1591345695755,pool-1-thread-1 start!
java.util.concurrent.TimeoutException
        at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:204)
        at aboutThread.Concurrent.Day19.Demo7.main(Demo7.java:22)
1591345700759,pool-1-thread-1 end!

任务执行中休眠了5秒,get方法获取执行结果,超时时间是3秒,3秒还未获取到结果,get触发了 TimeOutException 异常,当前线程从阻塞状态苏醒了。

Future其他方法介绍一下:

  • cancel : 取消执行的任务,参数表示是否对执行的额任务发送中断信号,方法声明如下:
boolean cancel(boolean myInterruptIfRunning);
  • isCancelled : 用来判断任务是否被取消
  • isDone : 判断任务是否执行完毕

cancel方法来个示例:

package aboutThread.Concurrent.Day19;

import java.sql.Time;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;

public class Demo8 {
    public static void main(String[] args) throws InterruptedException,ExecutionException{
        ExecutorService executorService = Executors.newFixedThreadPool(1);
        Future<Integer> result = executorService.submit(() ->{
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " START!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," +Thread.currentThread().getName() + " END!");    
            return 10;
        });
        executorService.shutdown();

        TimeUnit.SECONDS.sleep(1);
        result.cancel(false);
        System.out.println(result.isCancelled());
        System.out.println(result.isDone());

        TimeUnit.SECONDS.sleep(5);
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName());
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + "结果,result="+ result.get());
        executorService.shutdown();

    }
}

输出:

1591346344273,pool-1-thread-1 START!
true
true
1591346349278,pool-1-thread-1 END!
1591346350280,main
Exception in thread "main" java.util.concurrent.CancellationException
        at java.base/java.util.concurrent.FutureTask.report(FutureTask.java:121)
        at java.base/java.util.concurrent.FutureTask.get(FutureTask.java:191)
        at aboutThread.Concurrent.Day19.Demo8.main(Demo8.java:28)

输出2个true,表示任务已被取消,已完成,最后调用get方法会触发 CancellationException异常。

总结:
从上面可以看出Future、Callable接口需要结合ExecutorService来使用,需要有线程池支持。


FutureTask类

FutureTask除了实现Future接口,还实现了Runnable接口,因此FutureTask可以交给Executor执行,也可以交给线程执行(Thread还有个Runnable的构造方法),FutureTask表示带返回值的任务。

上面我们演示的是通过线程池执行任务然后获取执行结果。

这次我们通过FutureTask类,自己启动一个线程来获取执行结果,示例如下:

package aboutThread.Concurrent.Day19;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class Demo9 {
    public static void main(String[] args) throws ExecutionException,InterruptedException{
        FutureTask<Integer> futureTask = new FutureTask<Integer>(() ->{
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " start!");
            TimeUnit.SECONDS.sleep(5);
            System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " end!");
            return 10;
        });
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName());
        new Thread(futureTask).start();
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName());
        System.out.println(System.currentTimeMillis() + "," + Thread.currentThread().getName() + " 结果:" + futureTask.get());
    }
}

输出:

1591348320861,main
1591348320861,main
1591348320862,Thread-0 start!
1591348325865,Thread-0 end!
1591348320861,main 结果:10

大家可以回过头去看一下上面用线程池的submit方法返回的Future实际类型正是FutureTask对象,有兴趣的可以设置个断点去看看。

FutureTask类还是相当重要的,标记一下。


接下来,我们要学习:

  • 介绍CompletableFuture
  • 介绍CompletionService
  • 介绍ExecutorCompletionService

这是学习Java并发的第19天,加油!

你可能感兴趣的:(#,Java并发,Java)