java并发之线程执行器(Executor)

线程执行器和不使用线程执行器的对比(优缺点)

1.线程执行器分离了任务的创建和执行,通过使用执行器,只需要实现Runnable接口的对象,然后把这些对象发送给执行器即可。

2.使用线程池来提高程序的性能。当发送一个任务给执行器时,执行器会尝试使用线程池中的线程来执行这个任务。避免了不断创建和销毁线程导致的性能开销。

3.执行器可以处理实现了Callable接口的任务。Callable接口类似于Runnable接口,却提供了两方面的增强:

a.Callable主方法名称为call(),可以返回结果

b.当发送一个Callable对象给执行器时,将获得一个实现了Future接口的对象。可以使用这个对象来控制Callable对象的状态和结果。

4.提供了一些操作线程任务的功能

使用线程执行器的例子

  • 执行继承了Runnable接口的任务类

声明任务类Task

 1 public class Task implements Runnable {
 2     private String name;
 3
 4     public Task(String name){
 5         this.name=name;
 6     }
 7     @Override
 8     public void run() {
 9         }
10 }

使用执行器调用任务类

 1 public class Server {
 2     private ThreadPoolExecutor executor;
 3
 4     public Server(){
 5         executor=(ThreadPoolExecutor)Executors.newCachedThreadPool();
 6     }
 7     public void executeTask(Task task){
 8         System.out.printf("Server: A new task has arrived\n");
 9         executor.execute(task);
10         System.out.printf("Server: Active Count: %d\n",executor.getActiveCount());
11         System.out.printf("Server: Completed Tasks: %d\n",executor.getCompletedTaskCount());
12     }
13     public void endServer() {
14         executor.shutdown();
15   }
16 }

需要注意的地方:

1、ThreadPoolExecutor提供了好几种构造函数,由于这些构造函数的参数比较多,难于记忆,所以这里使用Executors类对其构造函数进行了封装,封装后的静        态函数可以通过函数名称更加直观的表述其含义。

2、执行实现Runnable接口的任务类使用的方式是:executor.execute(task);后面可以看到它和调用实现Callable接口的任务类还是有区别的。

3、使用执行器时要显示结束执行器。如果不关闭,那么执行器会一直执行而程序不会结束。如果执行器没有任务执行了,它将继续等待新任务的到来,而不会          结束执行。结束执行器这里使用的方式是shutdown();

  • 执行实现了Callable<T>接口的任务
 1 public class FactorialCalculator implements Callable<Integer> {
 2     private Integer number;
 3     public FactorialCalculator(Integer number){
 4         this.number=number;
 5     }
 6
 7     @Override
 8     public Integer call() throws Exception {
 9         int num, result;
10
11         num=number.intValue();
12         result=1;
13
14         // If the number is 0 or 1, return the 1 value
15         if ((num==0)||(num==1)) {
16             result=1;
17         } else {
18             // Else, calculate the factorial
19             for (int i=2; i<=number; i++) {
20                 result*=i;
21                 Thread.sleep(20);
22             }
23         }
24         System.out.printf("%s: %d\n",Thread.currentThread().getName(),result);
25         // Return the value
26         return result;
27     }
28 }

交给执行器去执行:

1 ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(2);//实例化执行器
2 FactorialCalculator calculator = new FactorialCalculator(number);//实例化任务
3 Future<Integer> result = executor.submit(calculator);//执行任务,并返回Future<T>实例

需要注意的地方:

1、Callable接口是一个泛型接口,该接口声明了call()方法。

1 public interface Callable<V> {
2     V call() throws Exception;
3 }

2、执行器调用submit()方法执行任务之后,返回一个Future<T>类型对象。Future是一个异步任务的结果。意思就是任务交给执行器后,执行器就会立刻返回一个Future对象,而此时任务正在执行中。Future对象声明了一些方法来获取由Callable对象产生的结果,并管理他们的状态。Future包含的方法如下:


线程执行器的四种实例方式

前面提到由于ThreadPoolExecutor类的构造函数比较难记忆(参数多,形式也差不多),Java提供了一个工厂类Executors来实现执行器对象的创建。具体函数如下:

这些函数以new开头。

1、newCachedThreadPool():缓存线程池

1   public static ExecutorService newCachedThreadPool() {
2         return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
3                                       60L, TimeUnit.SECONDS,
4                                       new SynchronousQueue<Runnable>());
5     }

需要注意的地方:

如果需要执行新任务,缓存线程池就会创建新线程;如果线程所运行的任务执行完成后并且这个线程可用,那么缓存线程池将会重用这些线程。

优点:减少了创建新线程所花费的时间

缺点:如果任务过多,系统的负荷会过载

使用条件:线程数量合理(不太多)或者线程运行只会运行很短的时间

2、newFixedThreadPool():固定线程池,(fixed:固定)

1 public static ExecutorService newFixedThreadPool(int nThreads) {
2         return new ThreadPoolExecutor(nThreads, nThreads,
3                                       0L, TimeUnit.MILLISECONDS,
4                                       new LinkedBlockingQueue<Runnable>());
5     }

需要注定的地方:

创建了具有线程最大数量值(即线程数量 <= nThreads)的执行器。如果发送超过数量的任务给执行器,剩余的任务将被阻塞知道线程池中有可空闲的线程来处理它们。

3、newSingleThreadExecutor():单线程执行器

1 public static ExecutorService newSingleThreadExecutor() {
2         return new FinalizableDelegatedExecutorService
3             (new ThreadPoolExecutor(1, 1,
4                                     0L, TimeUnit.MILLISECONDS,
5                                     new LinkedBlockingQueue<Runnable>()));
6 }

4、newScheduledThreadPool(int corePoolSize):定时执行器

1 public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
2         return new ScheduledThreadPoolExecutor(corePoolSize);
3 }

需要注意的地方:

使用方式如下:

1 ScheduledExecutorService executor=(ScheduledExecutorService)Executors.newScheduledThreadPool(1);
2 executor.schedule(task,i+1 , TimeUnit.SECONDS);

其中:task是实现了Callable接口的任务。schedule的参数含义:

1 public <V> ScheduledFuture<V> schedule(Callable<V> callable,//即将执行的任务
2                                            long delay,//任务执行前需要等待的时间
3                                            TimeUnit unit)//时间单位

线程执行器提供的功能

线程任务交给执行器去执行,执行器封装了一些方法来操作执行的线程。其中涉及到的类和接口的类图如下:

执行器的类图:

Executor是一个顶层接口,提供了唯一的一个方法execute(Runnable r)。ExecutorService继承Excutor接口,是比较核心的接口。提供了执行器具有的基本方法,包括执行器的提交(submit)和终止(shutdown)以及控制任务运行的invokeAll()和invokeAny()等方法。经常用到的执行器类一般是ThreadPoolExecutor和ScheduledThreadPoolExecutor。区别就是ScheduledThreadPoolExecutor一般和线程调度有关,也就是与一些周期性操作,时间间隔、定时执行任务的操作有关。

通过Future接口可以对执行器的线程进行一些操作,例如获取线程执行完成后的结果,取消线程的执行等,涉及Future的类图如下:

接下来具体学习上面这些类的用法以及他们提供的函数的使用场景。

延时执行任务和周期性执行任务

涉及到这种调度的一般使用ScheduledThreadPoolExecutor类。ScheduledThreadPoolExecutor类涉及到的和调度有关的函数如下:

延时执行任务:

1 ScheduledExecutorService executor=(ScheduledExecutorService)Executors.newScheduledThreadPool(1);
2
3         for (int i=0; i<5; i++) {
4             Task task=new Task("Task "+i);
5             executor.schedule(task,i+1 , TimeUnit.SECONDS);
6         }
7
8         executor.shutdown();

这里声明一个定时执行器,返回ScheduleExecutorService接口。然后调用schedule()方法。schedule的参数含义:

1 public <V> ScheduledFuture<V> schedule(Callable<V> callable,//即将执行的任务
2                                            long delay,      //任务执行前需要等待的时间
3                                            TimeUnit unit)   //时间单位

周期性执行任务:

周期性执行任务和延时执行任务相似,只不过调用的方法是scheduleAtFixedRate()。

1 ScheduledExecutorService executor=Executors.newScheduledThreadPool(1);
2
3         Task task=new Task("Task");
4         ScheduledFuture<?> result=executor.scheduleAtFixedRate(task, 1, 2, TimeUnit.SECONDS);

其中scheduleAtFixedRate函数的参数含义:

1 public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,     //将被周期性执行的任务
2                                                   long initialDelay,//任务第一次执行后的延后时间
3                                                   long period,      //两次执行的时间周期
4                                                   TimeUnit unit) {  //第二和第三个参数的时间单位

需要注意的地方:

两次执行之间的周期(即period)是指任务在两次执行开始时的时间间隔。如果有一个周期性的任务需要执行5秒钟,但是却让他没三秒执行一次,那么在任务的执行过程中会将有两个任务实例同时存在。

对线程任务的控制

invokeAny()和invokeAll()

这两个方法在ExecutorService中声明,ExecutorService是比较核心也是比较基础的接口。所以这两个方法应该算是执行器提供的比较宽范围(下面的子类都可以用到)的方法。

编程中比较常见的问题是,当采用多个并发任务来解决一个问题时,往往只关心这些任务中的第一个结果。比如,对一个数组排序有很多种算法,可以并发启动所有算法,对于给定的数组,第一个得到排序结果的算法就是最快的算法。这种场景可以使用invokeAny()函数实现,即:运行多个任务并返回第一个结果。 

 1     UserValidator ldapValidator=new UserValidator("LDAP");
 2         UserValidator dbValidator=new UserValidator("DataBase");
 3
 4         // Create two tasks for the user validation objects
 5         TaskValidator ldapTask=new TaskValidator(ldapValidator, username, password);
 6         TaskValidator dbTask=new TaskValidator(dbValidator,username,password);
 7
 8         // Add the two tasks to a list of tasks
 9         List<TaskValidator> taskList=new ArrayList<>();
10         taskList.add(ldapTask);
11         taskList.add(dbTask);
12
13         // Create a new Executor
14         ExecutorService executor=(ExecutorService)Executors.newCachedThreadPool();
15         String result;
16         try {
17             // Send the list of tasks to the executor and waits for the result of the first task
18             // that finish without throw and Exception. If all the tasks throw and Exception, the
19             // method throws and ExecutionException.
20             result = executor.invokeAny(taskList);
21             System.out.printf("Main: Result: %s\n",result);
22         } catch (InterruptedException e) {
23             e.printStackTrace();
24         } catch (ExecutionException e) {
25             e.printStackTrace();
26         }
27
28         // Shutdown the Executor
29         executor.shutdown();

其中UserValidator类睡眠一个随机模拟校验任务

 1 public boolean validate(String name, String password) {
 2         Random random=new Random();
 3
 4         try {
 5             Long duration=(long)(Math.random()*10);
 6             System.out.printf("Validator %s: Validating a user during %d seconds\n",this.name,duration);
 7             TimeUnit.SECONDS.sleep(duration);
 8         } catch (InterruptedException e) {
 9             return false;
10         }
11
12         return random.nextBoolean();
13     }

接下来是invokeAll(),invokeAll()方法接收一个任务列表,然后返回任务列表的所有任务的执行结果。

 1 List<Task> taskList = new ArrayList<>();
 2         for (int i = 0; i < 3; i++) {
 3             Task task = new Task("Task-" + i);
 4             taskList.add(task);
 5         }
 6         // Call the invokeAll() method
 7         List<Future<Result>> resultList = null;
 8         try {
 9             resultList = executor.invokeAll(taskList);
10         } catch (InterruptedException e) {
11             e.printStackTrace();
12         }
13         // Finish the executor
14         executor.shutdown();

Future和FutureTask

Future接口用来对接收任务执行完成后的结果以及对交给执行器执行的任务进行控制。接口提供的函数如下:

cancel()、isCancelled()

cancel()方法用来取消交给执行器的任务。isCancelled()方法用来判断是否取消成功。其中cancel(boolean)接收一个boolean类型的参数,用来表示是否要取消任务。具体用法:

 1 Task task=new Task();
 2
 3         Future<String> result=executor.submit(task);
 4
 5         try {
 6             TimeUnit.SECONDS.sleep(2);
 7         } catch (InterruptedException e) {
 8             e.printStackTrace();
 9         }
10
11         System.out.printf("Main: Cancelling the Task\n");
12         result.cancel(true);

线程交给执行器执行后会立即返回一个FutureTask<T>对象,(例如:[email protected]),通过调用cancel(true)方法显示来取消执行器中正在运行的任务。

注意的地方:

1、如果任务已经完成,或者之前已被取消,或者由于某种原因不能取消,则方法将返回false。

2、如果任务在执行器中等待分配Thread对象来执行它,那么任务被取消,并且不会开始执行。

3、如果任务已经在运行,那么依赖于调用cancel()方法时传递的参数。如果传递的参数为true,并且任务正在执行,任务将会取消。如果传递的参数为false并且任务正在执行,任务不会被取消。

4、如果Future对象所控制已经被取消,那么使用Future对象的get()方法将抛出CalcellationException异常控制任务的完成

isDone()

任务完成,返回值为boolean类型

get()、get(long,TimeUnit)

get()方法一直等待直到Callable对象的call()方法执行完成并返回结果。如果get()方法在等待结果时线程中断了,则将抛出一个InterruptException异常。如果call()方法抛出异常那么get()方法也将随之抛出ExecutionException异常。

get(long timeout,TimeUnit unit):如果调用这个方法时,任务的结果并未准备好,则方法等待所指定的timeout时间。如果等待超过了时间而任务的结果还没准备好,那么这个方法返回null。

思考:get()方法用来接收call()函数的返回值,因为call()函数是交由线程执行的,所以会等到所有线程执行完毕后才能得到正确的执行结果。所以在线程没有执行完成时,get()方法将一直阻塞。

FutureTask中的get()方法实现:可以看到,如果状态为非完成,则调用函数awaitDone()等待完成。

1 public V get() throws InterruptedException, ExecutionException {
2         int s = state;
3         if (s <= COMPLETING)
4             s = awaitDone(false, 0L);
5         return report(s);
6     }

FutureTask:done()

FutureTask是Future的实现类,除了实现Future的功能外,有一个done()方法需要注意:用来控制执行器中任务的完成

done()方法允许在执行器中的任务执行结束后,还可以执行一些后续操作。可以用来产生报表,通过邮件发送结果或者释放一些系统资源。当任务执行完成是受FutureTask类控制时,这个方法在内部被FutureTask类调用。在任务结果设置后以及任务的状态已改为isDone()之后,无论任务是否被取消或者正常结束,done()方法才被调用。

默认情况下,done()方法的实现为空,我们可以覆盖FutureTask类并实现done()方法来改变这种行为。

 1 public class ResultTask extends FutureTask<String> {
 2 @Override
 3     protected void done() {
 4         if (isCancelled()) {
 5             System.out.printf("%s: Has been cancelled\n",name);
 6         } else {
 7             System.out.printf("%s: Has finished\n",name);
 8         }
 9     }
10 }

CompletionService和ExecutorCompletionService

CompletionService:完成服务

当向Executor提交批处理任务时,并且希望在它们完成后获得结果,如果用FutureTask,你可以循环获取task,并用future.get()去获取结果,但是如果这个task没有完成,你就得阻塞在这里,这个实效性不高,其实在很多场合,其实你拿第一个任务结果时,此时结果并没有生成并阻塞,其实在阻塞在第一个任务时,第二个task的任务已经早就完成了,显然这种情况用future task不合适的,效率也不高。

自己维护list和CompletionService的区别:

1.从list中遍历的每个Future对象并不一定处于完成状态,这时调用get()方法就会被阻塞住,如果系统是设计成每个线程完成后就能根据其结果继续做后面的事,这样对于处于list后面的但是先完成的线程就会增加了额外的等待时间。

2.而CompletionService的实现是维护一个保存Future对象的BlockingQueue。只有当这个Future对象状态是结束的时候,才会加入到这个Queue中,take()方法其实就是Producer-Consumer中的Consumer。它会从Queue中取出Future对象,如果Queue是空的,就会阻塞在那里,直到有完成的Future对象加入到Queue中。

CompletionService采取的是BlockingQueue<Future<V>>无界队列来管理Future。则有一个线程执行完毕把返回结果放到BlockingQueue<Future<V>>里面。就可以通过completionServcie.take().get()取出结果。

类图如下:

对于批处理任务,完成服务一方面负责去执行(submit),一方面通过take()或者poll()方法可以获取已完成的任务,任务列表中有任务完成,结果就会返回。

处理被执行器拒绝的任务(RejectExecutionHandler)

当我们想结束执行器的执行时,调用shutdown()方法来表示执行器应该结束。但是,执行器只有等待正在运行的任务或者等待执行的任务结束后,才能真正的结束。

如果在shutdown()方法与执行器结束之间发送了一个任务给执行器,这个任务会被拒绝,因为这个时间段执行器已经不再接受任务了。ThreadPoolExecutor类提供了一套机制,当任务被拒绝时调用这套机制来处理它们。

 1 public class RejectedTaskController implements RejectedExecutionHandler {
 2
 3     @Override
 4     public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
 5         System.out.printf("RejectedTaskController: The task %s has been rejected\n",r.toString());
 6         System.out.printf("RejectedTaskController: %s\n",executor.toString());
 7         System.out.printf("RejectedTaskController: Terminating: %s\n",executor.isTerminating());
 8         System.out.printf("RejectedTaksController: Terminated: %s\n",executor.isTerminated());
 9     }
10 }

先提交一个任务,然后shutdown(),接着提交另外一个任务

 1 public static void main(String[] args) {
 2         // Create the controller for the Rejected tasks
 3         RejectedTaskController controller=new RejectedTaskController();
 4         // Create the executor and establish the controller for the Rejected tasks
 5         ThreadPoolExecutor executor=(ThreadPoolExecutor)Executors.newCachedThreadPool();
 6         executor.setRejectedExecutionHandler(controller);
 7
 8         // Lauch three tasks
 9         System.out.printf("Main: Starting.\n");
10         for (int i=0; i<3; i++) {
11             Task task=new Task("Task"+i);
12             executor.submit(task);
13         }
14
15         // Shutdown the executor
16         System.out.printf("Main: Shuting down the Executor.\n");
17         executor.shutdown();
18         // Send another task
19         System.out.printf("Main: Sending another Task.\n");
20         Task task=new Task("RejectedTask");
21         executor.submit(task);
22
23         // The program ends
24         System.out.printf("Main: End.\n");
25
26     }

执行结果如下:

Main: Starting.
Main: Shuting down the Executor.
Main: Sending another Task.
RejectedTaskController: The task [email protected] has been rejected
RejectedTaskController: [email protected][Shutting down, pool size = 3, active threads = 3, queued tasks = 0, completed tasks = 0]
RejectedTaskController: Terminating: true
RejectedTaksController: Terminated: false
Main: End.
Task Task1: Starting
Task Task0: Starting
Task Task2: Starting
Task Task1: ReportGenerator: Generating a report during 4 seconds
Task Task0: ReportGenerator: Generating a report during 7 seconds
Task Task2: ReportGenerator: Generating a report during 6 seconds
Task Task1: Ending
Task Task2: Ending
Task Task0: Ending

如果执行器调用了shutdown()方法后,原本执行的任务会执行完毕。

时间: 2024-10-12 14:01:54

java并发之线程执行器(Executor)的相关文章

java并发之线程池Executor 核心源码解析

1.什么是线程池 定义:线程池是指管理一组同构工作线程的资源池 组成部分: 线程管理器(ThreadPool):用于创建并管理线程池.包括创建线程池,销毁线程池,添加新任务 工作线程(PoolWorker):线程池中的线程 任务接口(Task):每个任务必须实现的接口,一共工作线程调度任务的执行 任务队列:用于存放没有处理的任务,提供一种缓冲机制 2.为什么要使用线程池 通过重用现有的线程而不是创建新线程,从而减少了线程创建 和 销毁过程中的巨大开销 当请求到达时,工作线程已经存在,不用再等待线

Java并发之——线程池

一. 线程池介绍 1.1 简介 线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务.线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理.当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源. 多线程技术主要解决处理器单元内多个线程执行的问题,它可以显著减少处理器单元的闲置时间,增加处理器单元的吞吐能力. 假设一个服务器完成一项

java并发之线程的创建(一)

概论 最近在学习并发,于是我在网上搜了一本<java并发编程实战>书学习. 传统创建线程的方式(jdk 1.5之前的方式) 在我印象中创建线程有两种方式 1. 继承Thread类,重写run方法,实例化自己写Thread子类,并用start()方法开启. 2.实现Runnable接口,重写run方法,把Runnable的子类的实例对象作为Thread的构造参数传递进去,创建线程,并开启. 但是我看别人代码时大部分都用第一种方式,直接new Thread 然后重写run方法.其实第二种方式更加符

Java并发之线程间协作Object的wait()、notify()、notifyAll()

wait().notify()和notifyAll()是Object类中的方法: 1)wait().notify()和notifyAll()方法是本地方法,而且为final方法,无法被重写. 2)调用某个对象的wait()方法能让当前线程堵塞.而且当前线程必须拥有此对象的monitor(即锁) 3)调用某个对象的notify()方法可以唤醒一个正在等待这个对象的monitor的线程,假设有多个线程都在等待这个对象的     monitor.则仅仅能唤醒当中一个线程: 4)调用notifyAll(

Java并发之线程(一)

目标: 线程的状态 线程的几种实现方式 三个线程轮流打印ABC十次 判断线程是否销毁 yield功能 给定三个线程t1,t2,t3,如何保证依次执行 1.基本概念 程序:是一个静态的概念; 进程:是一个动态的概念 a.进程是程序的一次动态执行过程,占用特定的地址空间; b.每个进程都是独立的,包括三部分:CPU code data c.缺点:内存的浪费,增加cpu的负担 线程:Thread,是进程中的一个'单一的连续控制流程/执行路径'; a.线程又被称为轻量级进程; b.Thread run

JAVA并发之阻塞队列浅析

背景 因为在工作中经常会用到阻塞队列,有的时候还要根据业务场景获取重写阻塞队列中的方法,所以学习一下阻塞队列的实现原理还是很有必要的.(PS:不深入了解的话,很容易使用出错,造成没有技术深度的样子) 阻塞队列是什么? 要想了解阻塞队列,先了解一下队列是啥,简单的说队列就是一种先进先出的数据结构.(具体的内容去数据结构里学习一下)所以阻塞队列就是一种可阻塞的队列.和普通的队列的不同就体现在 ”阻塞“两个字上.阻塞是啥意思? 百度看一下 在软件工程里阻塞一般指的是阻塞调用,即调用结果返回之前,当前线

Java并发——线程池Executor框架

线程池 无限制的创建线程 若采用"为每个任务分配一个线程"的方式会存在一些缺陷,尤其是当需要创建大量线程时: 线程生命周期的开销非常高 资源消耗 稳定性 引入线程池 任务是一组逻辑工作单元,线程则是使任务异步执行的机制.当存在大量并发任务时,创建.销毁线程需要很大的开销,运用线程池可以大大减小开销. Executor框架 说明: Executor 执行器接口,该接口定义执行Runnable任务的方式. ExecutorService 该接口定义提供对Executor的服务. Sched

(转)java并发之Executor

场景: 线程池在面试时候经常会碰到,在工作中用的场景更多,所以很有必要弄清楚. 1 简介 Java自1.5以来加入了处理一批线程的方法,也就是java并发包里的Executor.本文主要介绍ExecutorService的用法,Runable和Callable的用法以及ExecutorCompletionService的用法. 使用Executor来执行多个线程的好处是用来避免线程的创建和销毁的开销,以提升效率. 因此如果某些场景需要反复创建线程去处理同类事务的话,可以考虑使用线程池来处理.其实

深入浅出 Java Concurrency (30): 线程池 part 3 Executor 生命周期[转]

我们知道线程是有多种执行状态的,同样管理线程的线程池也有多种状态.JVM会在所有线程(非后台daemon线程)全部终止后才退出,为了节省资源和有效释放资源关闭一个线程池就显得很重要.有时候无法正确的关闭线程池,将会阻止JVM的结束. 线程池Executor是异步的执行任务,因此任何时刻不能够直接获取提交的任务的状态.这些任务有可能已经完成,也有可能正在执行或者还在排队等待执行.因此关闭线程池可能出现一下几种情况: 平缓关闭:已经启动的任务全部执行完毕,同时不再接受新的任务 立即关闭:取消所有正在