Java并发编程(十八)------Executor框架和FutureTask

1. Executor两级调度模型

在HotSpot虚拟机中,Java中的线程将会被一一映射为操作系统的线程。 

在Java虚拟机层面,用户将多个任务提交给Executor框架,Executor负责分配线程执行它们; 在操作系统层面,操作系统再将这些线程分配给处理器执行。

这种两级调度模型的示意图如下所示,从图中可以看出,应用程序通过Executor框架控制上层的调度;而下层的调度由操作系统内核(OSkernel)控制,下层的调度不受应用程序的控制。

title

2. Executor框架的结构

Executor框架中的所有类可以分成三类:

  1. 任务 
    任务有两种类型:Runnable和Callable。
  2. 任务执行器 
    Executor框架最核心的接口是Executor,它表示任务的执行器。 
    Executor的子接口为ExecutorService。 
    ExecutorService有两大实现类:ThreadPoolExecutor和ScheduledThreadPoolExecutor。

  3. 执行结果 
    Future接口表示异步的执行结果,它的实现类为FutureTask。

Executor框架包含的主要的类与接口如图所示:

  • Executor是一个接口,它是Executor框架的基础,它将任务的提交与任务的执行分离开来。
  • ThreadPoolExecutor是线程池的核心实现类,用来执行被提交的任务。
  • ScheduledThreadPoolExecutor是一个实现类,可以在给定的延迟后运行命令,或者定期执行命令。ScheduledThreadPool Executor比Timer更灵活,功能更强大。
  • Future接口和实现Future接口的FutureTask类,代表异步计算的结果。
  • Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。

Executor框架的使用示意图如图所示:

title

  • 主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。
  • 然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(ExecutorService.submit(Runnable task))或(ExecutorService.submit(Callable<T>task))。
  • 如果执行ExecutorService.submit(.....),ExecutorService将返回一个实现Future接口的对象(目前为止的JDK中返回的是FutureTask对象)。由于FutureTask实现了Runnable,程序员也可以创建FutureTask,然后直接交给ExecutorService执行。
  • 最后主线程可以执行FutureTask.get()方法来等待任务执行完成。主线程也可以执行FutureTask.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。
     

3. Executor框架的成员

Executor框架的成员主要有:ThreadPoolExecutor、ScheduledThreadPoolExecutor、Future接口、Runnable接口、Callable接口和Executors。

3.1 ThreadPoolExecutor

ThreadPoolExecutor通常使用工厂类Executors来创建

Executors可以创建3种类型的ThreadPoolExecutor:SingleThreadExecutor、FixedThreadPool和CachedThreadPool。

3.1.1 FixedThreadPool

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

title

  • 是一种固定大小的线程池;
  • corePoolSize和maximunPoolSize都为用户设定的线程数量nThreads;
  • keepAliveTime为0,意味着一旦有多余的空闲线程,就会被立即停止掉;但这里keepAliveTime无效;
  • 阻塞队列采用了LinkedBlockingQueue,它是一个无界队列;
  • 由于阻塞队列是一个无界队列,因此永远不可能拒绝任务;
  • 由于采用了无界队列,实际线程数量将永远维持在nThreads,因此maximumPoolSize和keepAliveTime将无效。

3.1.2 CachedThreadPool

public static ExecutorService newCachedThreadPool(){
    return new ThreadPoolExecutor(0,Integer.MAX_VALUE,60L,TimeUnit.MILLISECONDS,new SynchronousQueue<Runnable>());
}

title

  • 是一个可以无限扩大的线程池;
  • 它比较适合处理执行时间比较小的任务;
  • corePoolSize为0,maximumPoolSize为无限大,意味着线程数量可以无限大;
  • keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;
  • 采用SynchronousQueue装等待的任务,这个阻塞队列没有存储空间,这意味着只要有请求到来,就必须要找到一条工作线程处理他,如果当前没有空闲的线程,那么就会再创建一条新的线程。

3.1.3 SingleThreadExecutor

public static ExecutorService newSingleThreadExecutor(){
    return new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

title

  • 只会创建一条工作线程处理任务;
  • 采用的阻塞队列为LinkedBlockingQueue;
  • 适用于需要保证顺序地执行各个任务,并且在任意时间点不会有多个线程是活动的应用场景

3.2 ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor

  • 主要用来在给定的延迟之后运行任务,或者定期执行任务
  • 与Timer类似,但Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以在构造函数中指定多个对应的后台线程数。

ScheduledThreadPoolExecutor通常也是使用工厂类Executors来创建2种类型的ScheduledThreadPoolExecutor:

  • ScheduledThreadPoolExecutor:包含若干个线程的ScheduledThreadPoolExecutor;
  • SingleThreadScheduledExecutor:只包含一个线程的ScheduledThreadPoolExecutor;

运行机制

ScheduledThreadPoolExecutor的运行主要分为两大部分:

  1. 当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。
  2. 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。

ScheduledThreadPoolExecutor的执行示意图(基于JDK 6)如下:

title

  • 它接收SchduledFutureTask类型的任务,有两种提交任务的方式:

    1. scheduledAtFixedRate
    2. scheduledWithFixedDelay
  • SchduledFutureTask接收的参数:

    1. time:任务开始的时间
    2. sequenceNumber:任务的序号
    3. period:任务执行的时间间隔
  • 它采用DelayQueue存储等待的任务

    • DelayQueue内部封装了一个PriorityQueue,它会根据time的先后时间排序,若time相同则根据sequenceNumber排序;
    • DelayQueue也是一个无界队列,所以maximumPoolSize在ScheduledThreadPoolExecutor中没有什么意义(设置maximumPoolSize的大小没有什么效果)。
  • 工作线程的执行过程:

    • 工作线程会从DelayQueue取已经到期的任务去执行;
    • 执行结束后重新设置任务的到期时间,再次放回DelayQueue

3.3 Future接口

Future接口和实现Future接口的FutureTask类用来表示异步计算的结果。当我们把Runnable接口或Callable接口的实现类提交(submit)给ThreadPoolExecutor或ScheduledThreadPoolExecutor时,ThreadPoolExecutor或ScheduledThreadPoolExecutor会向我们返回一个FutureTask对象。对应的API:

<T> Future<T> submit(Callable<T> task)
<T> Future<T> submit(Runnable task, T result)
Future<> submit(Runnable task)

到目前最新的JDK 8为止,返回的是一个FutureTask对象。但从API可以看到,Java仅仅保证返回的是一个实现了Future接口的对象。在将来的JDK实现中,返回的可能不一定是FutureTask。

3.4 Runnable接口和Callable接口

Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。两者区别是Runnable不会返回结果,而Callable可以返回结果。除了创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个Runnable包装成一个Callable。

Executors提供将一个Runnable包装成一个Callable:

public static Callable<Object> callable(Runnable task) // 假设返回对象Callable1

Executors还提供把一个Runnable和一个待返回的结果包装成一个Callable:

public static <T> Callable<T> callable(Runnable task, T result) // 假设返回对象Callable2

当我们把一个Callable对象(比如上面的Callable1或Callable2)提交给ThreadPoolExecutor或ScheduledThreadPoolExecutor执行时,submit(....)会向我们返回一个FutureTask对象。然后执行FutureTask.get()方法来等待任务执行完成,当任务成功完成后
FutureTask.get()将返回该任务的结果。例如,如果提交的是对象Callable1,FutureTask.get()方法将返回null;如果提交的是对象Callable2,FutureTask.get()方法将返回result对象。

 

4. FutureTask

4.1 基本概念

FutureTask为什么叫做“未来的任务”?这个“Future”体现在哪里呢?

FutureTask的Future就源自于它的异步工作机制,如果我们在主线程中直接写一个函数来执行任务,这是同步的任务,也就是说必须要等这个函数返回以后我们才能继续做接下的事情,但是如果这个函数返回的结果对接下来的任务并没有意义,那么我们等在这里是很浪费时间的,而FutureTask就提供了这么一个异步的返回结果的机制,当执行一个FutureTask的时候,我们可以接着做别的任务,在将来的某个时间,FutureTask任务完成后会返回FutureTask对象来包装返回的结果,只要调用这个对象的get()方法即可获取返回值。

当然多线程中继承ThreadPoolExecutor和实现Runnable也可以实现异步工作机制,可是他们没有返回值。这时可以使用FutureTask包装Runnable或者Callable对象,再使用FutureTask来执行任务。

4.2 FutureTask的使用

FutureTask除了实现Future接口外,还实现了Runnable接口。因此FutureTask既可以交给Executor执行,也可以由调用线程直接执行(FutureTask.run())。

根据FutureTask被执行的进度,FutureTask对象可以处于一下3种状态

  • 未启动:创建了一个FutureTask对象但没有执行futureTask.run();
  • 已启动:futureTask.run()方法被执行的过程中;
  • 已完成:futureTask.run()正常执行结束,或者futureTask被取消(futureTask.cancel()),或者执行futureTask.run()时抛出异常而异常结束;

4.2.1 FutureTask的启动

FutureTask实现了Future接口和Runnable接口,因此FutureTask对象的执行有两种方式:

(1)交给线程池的Execute或submit方法执行;

import java.util.concurrent.*;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
 
 
class test{
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutor tpe = new ThreadPoolExecutor(5, 10,100, MILLISECONDS, new ArrayBlockingQueue<Runnable>(5));
        //用FutureTask包装Runnable或者Callable对象
        FutureTask<String> future = new FutureTask<String>(new Callable<String>() {
            @Override
            public String call() {
                try{
                    String a = "return String";
                    return a;
                }
                catch(Exception e){
                    e.printStackTrace();
                    return "exception";
                }
            }
        });
        //交给线程池的Execute或submit方法执行
        tpe.submit(future);
        try{
            System.out.println(future.get());
        }
        catch(Exception e){
            e.printStackTrace();
        }
        finally{
            tpe.shutdown();
        }
    }
}

(2)由调用线程直接执行;

在调用线程中执行futureTask.run()方法,只需将上述代码tpe.submit(future)替换为future.run()即可。  

4.2.2 FutureTask执行完后结果的获取

(1)在已启动的状态调用futureTask.get()或导致调用线程阻塞,直到FutureTask执行完毕,然后得到返回的FutureTask对象,调用futureTask.get( )获得任务返回值;

(2)在已完成状态调用futureTask.get()将导致调用线程立即返回(正常完成,得到FutureTask对象)或者抛出异常(被取消或者因异常而结束);

4.2.3 futureTask被取消

(1)在未启动状态调用futureTask.cancel( )会导致该任务永远不会再执行;

(2)在已启动状态:

  • 调用futureTask.cancel(true )会以中断的方式尝试停止任务,如果该任务不响应中断则无法停止;
  • 调用futureTask.cancel(false)将不会对正在执行的线程产生影响,也就是已启动的线程会让他执行完毕;

(3)在已完成状态调用futureTask.cancel( )则会返回false;

4.3 FutureTask的实现

4.3.1 AQS

FutureTask的实现基于AbstractQueuedSynchronizer(简称为AQS)。java.util.concurrent中的很多可阻塞类(比如ReentrantLock)都是基于AQS来实现的。AQS是一个同步框架,它提供通用机制来原子性管理同步状态、阻塞和唤醒线程,以及维护被阻塞线程的队列。JDK中基于AQS实现的同步器包括:ReentrantLock、Semaphore、ReentrantReadWriteLock、
CountDownLatch和FutureTask。

每一个基于AQS实现的同步器都会包含两种类型的操作:

acquire:acquire操作会阻塞调用线程,除非或者直到AQS的状态允许这个线程继续执行。FutureTask的acquire操作为get()/get(long timeout,TimeUnit unit)方法调用。

release:release操作会改变AQS的状态,改变后的状态可允许一个或多个阻塞线程被解除阻塞。FutureTask的release操作包括run()方法和cancel(....)方法。

4.3.2 FutureTask的实现过程

FutureTask的内部类Sync实现了AQS接口,通过对tryAcquire等抽象方法的重写和模板方法的调用来实现内部类Sync的tryAcquireShared等方法,然后聚合Sync的方法来实现FutureTask的get,cancel等方法;

FutureTask的get方法最终会调用AQS.acquireSharedInterruptibly方法,这个方法操作成功的条件是同步状态为RAN或者CANCELLED,也就是说如果这个FutureTask有线程E正在执行,那么这个FutureTask的状态是RUN,因此AQS.acquireSharedInterruptibly方法调用失败,此时调用get方法的线程被阻塞,添加到等待队列中(如下图线程D,其中A,B,C是已经被阻塞添加到等待队列中的线程)。当前面执行FutureTask的线程E执行完毕,那么以原子方式更新同步状态state的值为RAN,并执行AQS.release方法,然后唤醒等待队列中的第一个节点中的线程A,此时线程A出队列获得同步状态,并原子设置state为RUN,当线程A执行完毕,把state原子更新为RUN,然后唤醒线程B,以此类推,因此可以看出对于一个FutureTask同一时间只有一个线程执行这个任务。

4.4 FutureTask使用场景

  • 当一个线程需要等待另一个线程把某个任务执行完以后它才能继续执行时;
  • 有若干线程执行若干任务,每个任务最多只能被执行一次;
  • 当多个线程试图执行同一个任务,但只能允许一个线程执行此任务,其它线程需要等这个任务被执行完毕以后才能继续执行时;

猜你喜欢

转载自blog.csdn.net/DjokerMax/article/details/81325793