Executor(执行器)框架和Future框架

来源:互联网 发布:拍照搞怪表情软件 编辑:程序博客网 时间:2024/06/07 15:29

Future框架
Future接口定义了对线程执行计算结果的获取或取消线程执行该任务等操作

两个重要的功能:

1.获取结果。

2.可取消性。

Future 表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并获取计算的结果。计算完成后只能使用 get 方法来获取结果,如有必要,计算完成前可以阻塞此方法。取消则由cancel 方法来执行。还提供了其他方法,以确定任务是正常完成还是被取消了。一旦计算完成,就不能再取消计算。如果为了可取消性而使用Future 但又不提供可用的结果,则可以声明 Future<?> 形式类型、并返回 null 作为底层任务的结果。


FutureTask<V>类实现了Future接口所定义的基本功能。

public class FutureTask<V>
extends Object
implements RunnableFuture<V>

此类提供了对 Future 的基本实现。仅在计算完成时才能获取结果;如果计算尚未完成,则阻塞get 方法。一旦计算完成,就不能再重新开始或取消计算。

可使用 FutureTask 包装 CallableRunnable 对象。因为FutureTask 实现了 Runnable,所以可将 FutureTask 提交给 Executor 执行。

除了作为一个独立的类外,此类还提供了 protected 功能,这在创建自定义任务类时可能很有用。 

构造方法摘要FutureTask(Callable<V> callable)
          创建一个 FutureTask,一旦运行就执行给定的 CallableFutureTask(Runnable runnable,V result)
          创建一个 FutureTask,一旦运行就执行给定的 Runnable,并安排成功完成时 get 返回给定的结果 。 

FutureTask可视为Runnable和Callable的包装器,包装后便可调用FutureTask中的方法执行对任务的控制。



Executor框架


Executor提供了不同于Thread类执行线程的另一种机制。

      前言:

      在此应将Runnable和Callable接口视为提交给线程运行的任务,用 Thead创建线程,也是将Runnable作为任务提交给了线程。

      构建一个新的线程是有一定代价的,涉及到与操作系统的交互,操作系统为线程分配TCB块等。如果程序中创建了大量的生命期很短的线程,将严重影响性能开销。这时就应使用线程池(thread pool)。一个线程池中包含许多准备运行的空闲线程。将一个Runnable对象提交给线程池,就会有一个线程调用run方法。当run方法退出时,线程不会死亡,而是在池中准备为下一个请求提供服务。

      另一个使用线程池的目的是控制并发线程的数目。创建大量的线程会大大降低性能,如果需要创建多个线程运行多个任务,则应该使用一个线程数固定的线程池,来限制并发线程的总数。(若使用Thead并发,操作系统维护了过多的TCB块,但一个时间内只有一个线程执行,可以看出严重浪费了资源;若使用Thead Pool,操作系统只维护一定数目的TCB块,多个任务以队列的方式共用线程池内的线程执行自己的任务)。

      那么想象中设计一个线程池就需要有线程池大小、线程生命周期管理、等待队列等等功能。

                                                     

    Executor 接口定义了最基本的 execute 方法,用于接收用户提交任务。

          ExecutorService 定义了线程池终止shutdown及submit提交 futureTask 任务支持的方法。(submit方法提交的是Runnable或Callable,返回了一个Future可引用的实例,可以认为是FutureTask的实例)。

          AbstractExecutorService 是抽象类,提供ExecutorService 执行方法的默认实现。其内部实现的submit方法描述为:submit(Runnable) 的实现创建了一个关联RunnableFuture 类,该类将被执行并返回。子类可以重写 newTaskFor 方法,以返回 FutureTask 之外的RunnableFuture 实现。 

    ThreadPoolExecutor 是最核心的一个类,是线程池的内部实现。线程池的功能都在这里实现了,平时用的最多的基本就是这个了。其源码很精练,远没当时想象的多。

          ScheduledExecutorService接口具有为预定执行或重复执行任务而设计的方法。可以预定Runnable或Callable任务在初始的延迟之后只运行一次,也可以预定一个Runnable对象周期性的运行。

          ScheduledThreadPoolExecutor  ThreadPoolExecutor 的基础上提供了支持定时调度的功能。线程任务可以在一定延时时间后才被触发执行。


关于线程池的使用:

ThreadPoolExecutor的描述:

         一个 ExecutorService,它使用可能的几个池线程之一执行每个提交的任务,通常使用Executors 工厂方法配置。

         强烈建议程序员使用较为方便的 Executors 工厂方法 Executors.newCachedThreadPool()(无界线程池,可以进行自动线程回收)、Executors.newFixedThreadPool(int)(固定大小线程池)和Executors.newSingleThreadExecutor()(单个后台线程),它们均为大多数使用场景预定义了设置。但也可以手动配置。

1.ThreadPoolExecutor 原理

1.1 ThreadPoolExecutor内部的几个重要属性

1.线程池本身的状态

Java代码  收藏代码
  1. volatile int runState;   
  2. static final int RUNNING = 0;   
  3. static final int SHUTDOWN = 1;   
  4. static final int STOP = 2;   
  5. static final int TERMINATED = 3;   

 

2.等待任务队列和工作集

Java代码  收藏代码
  1. private final BlockingQueue<Runnable> workQueue; //等待被执行的Runnable任务   
  2. private final HashSet<Worker> workers = new HashSet<Worker>(); //正在被执行的Worker任务集   


3.线程池的主要状态锁。线程池内部的状态变化 ( 如线程大小 ) 都需要基于此锁。

Java代码  收藏代码
  1. private final ReentrantLock mainLock = new ReentrantLock();  

 

4.线程的存活时间和大小

Java代码  收藏代码
  1. private volatile long keepAliveTime;// 线程存活时间   
  2. private volatile boolean allowCoreThreadTimeOut;// 是否允许核心线程存活   
  3. private volatile int corePoolSize;// 核心池大小   
  4. private volatile int maximumPoolSize; // 最大池大小   
  5. private volatile int poolSize; //当前池大小   
  6. private int largestPoolSize; //最大池大小,区别于maximumPoolSize,是用于记录线程池曾经达到过的最大并发,理论上小于等于maximumPoolSize。   

 

5.线程工厂和拒绝策略

Java代码  收藏代码
  1. private volatile RejectedExecutionHandler handler;// 拒绝策略,用于当线程池无法承载新线程是的处理策略。  
  2.  private volatile ThreadFactory threadFactory;// 线程工厂,用于在线程池需要新创建线程的时候创建线程  

 

6.线程池完成任务数

Java代码  收藏代码
  1. private long completedTaskCount;//线程池运行到当前完成的任务数总和  

 

1.2 ThreadPoolExecutor 的内部工作原理

有了以上定义好的数据,下面来看看内部是如何实现的 。 Doug Lea 的整个思路总结起来就是 5 句话:

1.  如果当前池大小 poolSize 小于 corePoolSize ,则创建新线程执行任务。

2.  如果当前池大小 poolSize 大于 corePoolSize ,且等待队列未满,则进入等待队列

3.  如果当前池大小 poolSize 大于 corePoolSize 且小于 maximumPoolSize ,且等待队列已满,则创建新线程执行任务。

4.  如果当前池大小 poolSize 大于 corePoolSize 且大于 maximumPoolSize ,且等待队列已满,则调用拒绝策略来处理该任务。

5.  线程池里的每个线程执行完任务后不会立刻退出,而是会去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出。

 

下面看看代码实现 :

线程池最重要的方法是由 Executor 接口定义的 execute 方法 , 是任务提交的入口。

我们看看 ThreadPoolExecutor.execute(Runnable cmd) 的实现:

 

Java代码  收藏代码
  1. public void execute(Runnable command) {  
  2.         if (command == null)  
  3.             throw new NullPointerException();  
  4.         if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {  
  5.             if (runState == RUNNING && workQueue.offer(command)) {  
  6.                 if (runState != RUNNING || poolSize == 0)  
  7.                     ensureQueuedTaskHandled(command);  
  8.             }  
  9.             else if (!addIfUnderMaximumPoolSize(command))  
  10.                 reject(command); // is shutdown or saturated  
  11.         }  
  12. }  

解释如下:

当提交一个新的 Runnable 任务:

分支1 :  如果当前池大小小于 corePoolSize, 执行 addIfUnderCorePoolSize(command) ,如果线程池处于运行状态且 poolSize < corePoolSize addIfUnderCorePoolSize(command) 会做如下事情,将 Runnable 任务封装成 Worker 任务 , 创建新的 Thread ,执行 Worker 任务。如果不满足条件,则返回 false 。代码如下:

 

Java代码  收藏代码
  1.  private boolean addIfUnderCorePoolSize(Runnable firstTask) {  
  2.         Thread t = null;  
  3.         final ReentrantLock mainLock = this.mainLock;  
  4.         mainLock.lock();  
  5.         try {  
  6.             if (poolSize < corePoolSize && runState == RUNNING)  
  7.                 t = addThread(firstTask);  
  8.         } finally {  
  9.             mainLock.unlock();  
  10.         }  
  11.         if (t == null)  
  12.             return false;  
  13.         t.start();  
  14.         return true;  
  15. }  

    分支2 : 如果大于 corePoolSize 或 1 失败,则:

  •   如果等待队列未满,把 Runnable 任务加入到 workQueue 等待队列 

    workQueue .offer(command)

  •   如多等待队列已经满了,调用 addIfUnderMaximumPoolSize(command) ,和addIfUnderCorePoolSize 基本类似,只不过判断条件是 poolSize < maximumPoolSize 。如果大于 maximumPoolSize ,则把 Runnable 任务交由 RejectedExecutionHandler 来处理。

问题:如何实现线程的复用 ?  

Doug Lea 的实现思路是 线程池里的每个线程执行完任务后不立刻退出,而是去检查下等待队列里是否还有线程任务需要执行,如果在 keepAliveTime 里等不到新的任务了,那么线程就会退出。这个功能的实现 关键在于Worker 。线程池在执行 Runnable 任务的时候,并不单纯把 Runnable 任务交给创建一个 Thread 。而是会把Runnable 任务封装成 Worker 任务。

下面看看 Worker 的实现:

 代码很简单,可以看出, worker 里面包装了 firstTask 属性,在构造worker 的时候传进来的那个 Runnable 任务就是 firstTask 。 同时也实现了Runnable接口,所以是个代理模式,看看代理增加了哪些功能。 关键看 woker 的 run方法:

Java代码  收藏代码
  1. public void run() {  
  2.            try {  
  3.                Runnable task = firstTask;  
  4.                firstTask = null;  
  5.                while (task != null || (task = getTask()) != null) {  
  6.                    runTask(task);  
  7.                    task = null;  
  8.                }  
  9.            } finally {  
  10.                workerDone(this);  
  11.            }  
  12.        }  

 可以看出 worker 的 run 方法是一个循环,第一次循环运行的必然是 firstTask ,在运行完 firstTask 之后,并不会立刻结束,而是会调用 getTask 获取新的任务( getTask 会从等待队列里获取等待中的任务),如果keepAliveTime 时间内得到新任务则继续执行,得不到新任务则那么线程才会退出。这样就保证了多个任务可以复用一个线程,而不是每次都创建新任务。 keepAliveTime 的逻辑在哪里实现的呢?主要是利用了 BlockingQueue的 poll 方法支持等待。可看 getTask 的代码段:

 

Java代码  收藏代码
  1. if (state == SHUTDOWN)  // Help drain queue  
  2.     r = workQueue.poll();  
  3. else if (poolSize > corePoolSize || allowCoreThreadTimeOut)  
  4.     r = workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS);  
  5. else  
  6.     r = workQueue.take();  


2.ThreadFactory 和RejectedExecutionHandler

ThreadFactory 和 RejectedExecutionHandler是ThreadPoolExecutor的两个属性,也 可以认为是两个简单的扩展点 . ThreadFactory 是创建线程的工厂。

默认的线程工厂会创建一个带有“ pool-poolNumber-thread-threadNumber ”为名字的线程,如果我们有特别的需要,如线程组命名、优先级等,可以定制自己的 ThreadFactory 。

RejectedExecutionHandler 是拒绝的策略。常见有以下几种:

AbortPolicy :不执行,会抛出 RejectedExecutionException 异常。

CallerRunsPolicy :由调用者(调用线程池的主线程)执行。

DiscardOldestPolicy :抛弃等待队列中最老的。

DiscardPolicy: 不做任何处理,即抛弃当前任务。

 

3.Executors类

Executors里面有许多静态方法来构建线程池:

三个返回ThreadPoolExecutor类对象的方法:

1.FixedThreadPool

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

 实际上就是个不支持keepalivetime,且corePoolSize和maximumPoolSize相等的线程池。

2.SingleThreadExecutor

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

  实际上就是个不支持keepalivetime,且corePoolSize和maximumPoolSize都等1的线程池。

3.CachedThreadPool

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

 实际上就是个支持keepalivetime时间是60秒(线程空闲存活时间),且corePoolSize为0,maximumPoolSize无穷大的线程池。

返回SheduledThreadPoolExecutor类对象的方法:

4.SingleThreadScheduledExecutor

Java代码  收藏代码
  1. public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {  
  2.     return new DelegatedScheduledExecutorService  
  3.         (new ScheduledThreadPoolExecutor(1, threadFactory));  
  4. }  

 实际上是个corePoolSize为1的ScheduledExecutor。上文说过ScheduledExecutor采用无界等待队列,所以maximumPoolSize没有作用。

5.ScheduledThreadPool

Java代码  收藏代码
  1. public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {  
  2.     return new ScheduledThreadPoolExecutor(corePoolSize);  
  3. }  

  实际上是corePoolSize可设定的ScheduledExecutor。上文说过ScheduledExecutor采用无界等待队列,所以maximumPoolSize没有作用。

 

下面总结了在使用线程池时的一般步骤:

1:调用Exectors类中的静态方法newCachedThreadPool或newFixedThreadPool创建一个ThreadPoolExecutor(一般用ExecutorService接口引用)的线程池;

2:调用submit提交Runnable或Callable对象;

3:有三种submit方法,方法都返回一个Future类型对象,可用来取消该任务,或获取该任务执行完毕得到的结果;

4:当不再提交其他任务时,调用shutdown关闭线程池。


       另外关于ExecutorService的submit与execute方法都能执行任务,但在使用过程,发现其对待run方法抛出的异常处理方式不一样。

两者执行任务最后都会通过Executor的execute方法来执行,可以将submit视为对execute方法的包装。对于submit,会将runnable物件包装成FutureTask其run方法会捕捉被包装的Runnable Object的run方法抛出的Throwable异常,待submit方法所返回的的Future Object调用get方法时,将执行任务时捕获的Throwable Object包装成java.util.concurrent.ExecutionException来抛出。

 

0 0
原创粉丝点击