Java 동시 프로그래밍(3): 실행자 프레임워크

1. Executor 프레임워크의 구조와 멤버

스레드 2단계 스케줄링 모델에서 Java 스레드는 기본 운영 체제 스레드에 일대일로 매핑됩니다. 기본 운영 체제 스레드는 Java 스레드가 시작될 때 생성되고 Java 스레드가 종료되면 운영 체제 스레드도 재활용됩니다. 운영 체제는 모든 스레드를 예약하고 사용 가능한 CPU에 할당합니다.

상위 계층에서 Java 다중 스레드 프로그램은 일반적으로 응용 프로그램을 여러 작업으로 분해한 다음 사용자 수준 스케줄러(Executor 프레임워크)를 사용하여 이러한 작업을 고정된 수의 스레드로 매핑합니다. 스레드를 장치의 하드웨어 처리에 연결합니다.

이름 없는 파일.png

1.1, Executor 프레임워크의 구조

Executor 프레임워크는 주로 세 부분으로 구성됩니다.

  • 태스크 : 실행된 태스크가 구현해야 하는 인터페이스를 포함합니다: Runnable 인터페이스 또는 Callable 인터페이스.

  • 작업 실행 : 작업 실행 메커니즘의 핵심 인터페이스 Executor 및 Executor에서 상속된 ExecutorService 인터페이스를 포함합니다. Executor 프레임워크에는 ExecutorService 인터페이스를 구현하는 두 가지 주요 클래스(ThreadPoolExecutor 및 ScheduledThreadPoolExecutor)가 있습니다.

  • 비동기 계산의 결과 : Future 인터페이스와 Future 인터페이스를 구현하는 FutureTask 클래스를 포함합니다.

Executor 프레임워크에 포함된 주요 클래스 및 인터페이스:

  • Executor는 작업 제출과 작업 실행을 분리하는 Executor 프레임워크의 기반이 되는 인터페이스입니다.

  • ThreadPoolExecutor는 제출된 작업을 실행하는 데 사용되는 스레드 풀의 핵심 구현 클래스입니다.

  • ScheduledThreadPoolExecutor는 주어진 지연 후에 명령을 실행하거나 주기적으로 명령을 실행할 수 있는 구현 클래스입니다. ScheduledThreadPoolExecutor는 Timer보다 유연하고 강력합니다.

  • Future 인터페이스와 Future 인터페이스를 구현하는 FutureTask 클래스는 비동기 계산의 결과를 나타냅니다.

  • Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或Scheduled-ThreadPoolExecutor执行。

Executor框架的使用示意图:

이름 없는 파일.png

  • 主线程首先要创建实现Runnable或者Callable接口的任务对象。工具类Executors可以把一个Runnable对象封装为一个Callable对象(Executors.callable(Runnable task)或Executors.callable(Runnable task,Object resule))。

  • 然后可以把Runnable对象直接交给ExecutorService执行(ExecutorService.execute(Runnable command));或者也可以把Runnable对象或Callable对象提交给ExecutorService执行(Executor-Service.submit(Runnable task)或ExecutorService.submit(Callabletask))。

  • 如果执行ExecutorService.submit(…),ExecutorService将返回一个实现Future接口的对象。

  • 最后,主线程可以执行future.get()方法来等待任务执行完成。主线程也可以执行future.cancel(boolean mayInterruptIfRunning)来取消此任务的执行。

1.2、Executor框架的成员

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

1、ThreadPoolExecutor:

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

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

  • FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactorythreadFactory)

复制代码

创建使用固定线程数的FixedThreadPool的API。 FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。

  • SingleThreadExecutor
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
复制代码

创建使用单个线程的SingleThreadExecutor的API。SingleThreadExecutor适用于需要保证顺序地执行各个任务;并且在任意时间点,不会有多个线程是活动的应用场景。

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

复制代码

创建一个会根据需要创建新线程的CachedThreadPool的API。 CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。

2、ScheduledThreadPoolExecutor

ScheduledThreadPoolExecutor通常使用工厂类Executors来创建。Executors可以创建2种类型的ScheduledThreadPoolExecutor:

  • ScheduledThreadPoolExecutor:包含若干个线程的ScheduledThreadPoolExecutor。

  • SingleThreadScheduledExecutor:只包含一个线程的ScheduledThreadPoolExecutor。

创建固定个数线程的ScheduledThreadPoolExecutor的API。

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

复制代码

ScheduledThreadPoolExecutor适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景。

创建单个线程的SingleThreadScheduledExecutor的API。

public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newSingleThreadScheduledExecutor
(ThreadFactory threadFactory)
复制代码

SingleThreadScheduledExecutor适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。

3、Future接口

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

<T> Future<T> submit(Callable<T> task)
<T> Future<T> submit(Runnable task, T result)
Future<> submit(Runnable task)
复制代码

4、Runnable接口和Callable接口

Runnable接口和Callable接口的实现类,都可以被ThreadPoolExecutor或ScheduledThreadPoolExecutor执行。它们之间的区别是Runnable不会返回结果,而Callable可以返回结果。

除了可以自己创建实现Callable接口的对象外,还可以使用工厂类Executors来把一个Runnable包装成一个Callable。

下面是Executors提供的,把一个Runnable包装成一个Callable的API。

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

2、ThreadPoolExecutor详解

2.1、FixedThreadPool详解

FixedThreadPool被称为可重用固定线程数的线程池。下面是FixedThreadPool的源代码实现。

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

FixedThreadPool的corePoolSize和maximumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads。

当线程池中的线程数大于corePoolSize时,keepAliveTime为多余的空闲线程等待新任务的最长时间,超过这个时间后多余的线程将被终止。这里把keepAliveTime设置为0L,意味着多余的空闲线程会被立即终止。

执行流程:

  • 如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。

  • 在线程池完成预热之后(当前运行的线程数等于corePoolSize),将任务加入LinkedBlockingQueue。

  • 线程执行完1中的任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。

FixedThreadPool使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。使用无界队列作为工作队列会对线程池带来如下影响。

  • 当线程池中的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池中的线程数不会超过corePoolSize。

  • 使用无界队列时maximumPoolSize将是一个无效参数。

  • 使用无界队列时keepAliveTime将是一个无效参数。

  • 由于使用无界队列,运行中的FixedThreadPool(未执行方法shutdown()或shutdownNow())不会拒绝任务(不会调用RejectedExecutionHandler.rejectedExecution方法)。

2.2、SingleThreadExecutor详解

SingleThreadExecutor是使用单个worker线程的Executor。下面是SingleThreadExecutor的源代码实现。

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

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列对线程池带来的影响与FixedThreadPool相同。

执行流程:

  • 如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线程来执行任务。

  • 在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入LinkedBlockingQueue。

  • 线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来执行。

2.3、CachedThreadPool详解

CachedThreadPool是一个会根据需要创建新线程的线程池。下面是创建CachedThread-Pool的源代码。

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

CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为Integer.MAX_VALUE,即maximumPool是无界的。这里把keepAliveTime设置为60L,意味着CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。

CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

执行流程:

  • 首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行完成;否则执行下面的步骤2。

  • 当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1将失败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。

  • 在步骤2中新创建的线程将任务执行完后,会执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执行步骤1),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。

SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的任务传递给空闲线程执行。CachedThreadPool中任务传递的示意图:

이름 없는 파일.png

3、ScheduledThreadPoolExecutor详解

ScheduledThreadPoolExecutor继承自ThreadPoolExecutor。它主要用来在给定的延迟之后运行任务,或者定期执行任务

3.1、ScheduledThreadPoolExecutor的运行机制

部分源码如下:

public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE,
          DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
          new DelayedWorkQueue());
}
复制代码

DelayQueue是一个无界队列,所以ThreadPoolExecutor的maximumPoolSize在ScheduledThreadPoolExecutor中没有什么意义。

执行流程:

  • 当调用ScheduledThreadPoolExecutor的scheduleAtFixedRate()方法或者scheduleWithFixedDelay()方法时,会向ScheduledThreadPoolExecutor的DelayQueue添加一个实现了RunnableScheduledFutur接口的ScheduledFutureTask。

  • 线程池中的线程从DelayQueue中获取ScheduledFutureTask,然后执行任务。

public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                              long initialDelay,
                                              long period,
                                              TimeUnit unit) {
    if (command == null || unit == null)
        throw new NullPointerException();
    if (period <= 0L)
        throw new IllegalArgumentException();
    ScheduledFutureTask<Void> sft =
        new ScheduledFutureTask<Void>(command,
                                      null,
                                      triggerTime(initialDelay, unit),
                                      unit.toNanos(period),
                                      sequencer.getAndIncrement());
    RunnableScheduledFuture<Void> t = decorateTask(command, sft);
    sft.outerTask = t;
    delayedExecute(t);
    return t;
}
复制代码

ScheduledThreadPoolExecutor为了实现周期性的执行任务,对ThreadPoolExecutor做了如下的修改。

  • 使用DelayQueue作为任务队列。

  • 获取任务的方式不同

  • 执行周期任务后,增加了额外的处理。

3.2、ScheduledThreadPoolExecutor的实现

前面提到过,ScheduledThreadPoolExecutor会把待调度的任务(ScheduledFutureTask)放到一个DelayQueue中。

ScheduledFutureTask主要包含3个成员变量:

  • long型成员变量time,表示这个任务将要被执行的具体时间。

  • long型成员变量sequenceNumber,表示这个任务被添加到ScheduledThreadPoolExecutor中的序号。

  • long型成员变量period,表示任务执行的间隔周期。

DelayQueue封装了一个PriorityQueue,这个PriorityQueue会对队列中的Scheduled-FutureTask进行排序。排序时,time小的排在前面(时间早的任务将被先执行)。如果两个ScheduledFutureTask的time相同,就比较sequenceNumber,sequenceNumber小的排在前面(也就是说,如果两个任务的执行时间相同,那么先提交的任务将被先执行)。

이름 없는 파일.png 执行流程:

  • 线程1从DelayQueue中获取已到期的ScheduledFutureTask(DelayQueue.take())。到期任务是指ScheduledFutureTask的time大于等于当前时间。

  • 线程1执行这个ScheduledFutureTask。

  • 线程1修改ScheduledFutureTask的time变量为下次将要被执行的时间。

  • 线程1把这个修改time之后的ScheduledFutureTask放回DelayQueue中(DelayQueue.add())。

下面是DelayQueue.take()方法的源代码实现。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();                      // 1
    try {
        for (;;) {
            E first = q.peek();
            if (first == null) {
                available.await();                   // 2.1
            } else {
                long delay =  first.getDelay(TimeUnit.NANOSECONDS);
                if (delay > 0) {
                    long tl = available.awaitNanos(delay);   // 2.2
                } else {
                    E x = q.poll();                  // 2.3.1
                    assert x != null;
                    if (q.size() != 0)
                        available.signalAll();         // 2.3.2
                    return x;
                }
            }
        }
    } finally {
        lock.unlock();                            // 3
    }
}
复制代码

이름 없는 파일.png 获取任务执行流程:

  • 获取Lock。

  • 获取周期任务。

    • 如果PriorityQueue为空,当前线程到Condition中等待;否则执行下面的2.2。

    • 如果PriorityQueue的头元素的time时间比当前时间大,到Condition中等待到time时间;否则执行下面的2.3。

    • 获取PriorityQueue的头元素(2.3.1);如果PriorityQueue不为空,则唤醒在Condition中等待的所有线程(2.3.2)。

  • 释放Lock。

ScheduledThreadPoolExecutor在一个循环中执行步骤2,直到线程从PriorityQueue获取到一个元素之后(执行2.3.1之后),才会退出无限循环(结束步骤2)。

最后,让我们看看ScheduledThreadPoolExecutor中的线程执行任务的步骤4,把ScheduledFutureTask放入DelayQueue中的过程。下面是DelayQueue.add()的源代码实现。

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();                // 1
    try {
        E first = q.peek();
        q.offer(e);              // 2.1
        if (first == null || e.compareTo(first) < 0)
            available.signalAll();    // 2.2
        return true;
    } finally {
        lock.unlock();           // 3
    }
}

复制代码

ScheduledFutureTask放入DelayQueue中的过程:

  • 获取Lock。

  • 添加任务。

    • 向PriorityQueue添加任务。

    • 如果在上面2.1中添加的任务是PriorityQueue的头元素,唤醒在Condition中等待的所有线程。

  • 释放Lock。

4、FutureTask详解

4.1、FutureTask简介

Future 인터페이스를 구현하는 것 외에도 FutureTask는 Runnable 인터페이스도 구현합니다. 따라서 FutureTask는 실행을 위해 Executor로 전달되거나 호출 스레드(FutureTask.run())에서 직접 실행할 수 있습니다. FutureTask.run() 메서드가 실행되는 시점에 따라 FutureTask는 다음 세 가지 상태가 될 수 있습니다.

  • 시작하지 않았습니다. FutureTask는 FutureTask.run() 메서드가 실행되기 전에 시작되지 않은 상태입니다. FutureTask가 생성되고 FutureTask.run() 메서드가 실행되지 않으면 FutureTask가 시작되지 않습니다.

  • 활성화되었습니다. FutureTask.run() 메서드를 실행하는 동안 FutureTask는 시작 상태입니다.

  • 완전한. FutureTask.run() 메서드가 실행된 후 정상적으로 종료되거나 취소(FutureTask.cancel(…))되거나, FutureTask.run() 메서드가 실행되어 비정상적으로 종료되고 FutureTask가 실행되면 예외가 throw됩니다. 완료된 상태에서.

FutureTask가 시작되지 않았거나 시작되지 않았을 때 FutureTask.get() 메서드를 실행하면 호출 스레드가 차단되고 FutureTask가 완료 상태일 때 FutureTask.get() 메서드를 실행하면 호출 스레드가 즉시 결과를 얻거나 예외를 던집니다.

FutureTask가 시작되지 않은 경우 FutureTask.cancel() 메서드를 실행하면 작업이 실행되지 않으며 FutureTask가 시작 상태일 때 FutureTask.cancel(true) 메서드를 실행하면 작업을 실행하는 스레드가 중단됩니다. 작업을 중지하려면 FutureTask가 시작됨 상태일 때 FutureTask.cancel(false) 메서드를 실행해도 작업을 실행하는 스레드에 영향을 주지 않습니다(실행 중인 작업이 완료될 때까지 실행). 완료된 상태에서 FutureTask.cancel(…) 메서드를 실행하면 false가 반환됩니다.

이름 없는 파일.png

추천

출처juejin.im/post/7086749464081203208