Java并发编程:并发任务执行及结果获取

Executor:任务执行

关系:Executor <- ExecutorService => AbstractExecutorService => ThreadPoolExecutor => ScheduledThreadPoolExecutor

Executor

在Java中,任务的抽象不是Thread,而是Executor。当出现new Thread(new(RunnableTask())).start(),尽量替换成:

Executor executor = anExecutor;
executor.execute(new RunnableTask1());
executor.execute(new RunnableTask2());

Executor接口只保证在某个时刻会执行这个任务。但是在哪儿执行,什么时候执行,都由其实现者自主决定。
比如,在本线程(caller)中执行(串行执行,没什么意义):

public class CallerExecutor implements Executor {
    public void execute(Runnable r) {
        r.run();
    }
}

一般情况下都是另起线程去执行,毕竟并发才会更快:

public class ThreadPerTaskExecutor implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}

还可以使用其他的方式,反正一切由实现者自主裁决。

ExecutorService

Executor是有限制的。毕竟Executor一般都是采用异步执行的方式,那么任务一旦提交,其可能在运行,也可能在队列中等待执行,还可能执行完毕了。而且,任务也应该是可以被关闭的,这些对任务状态进行查询或操作的方法Executor接口都没有提供。
因此,ExecutorService接口出现了。使用ExecutorService可以增加对任务状态的管理,并且可以生产Future作为结果,用于追踪异步执行的任务。

任务生命周期管理

可以使用showdown()不再接受和执行新任务,也可以使用showdownNow(),除了不接受新任务,还尝试关停正在执行的任务。
所以在关停一个ExecutorService的时候,先使用shotdown()拒绝接受新任务,然后awaitTermination(timeout)等待正在执行的任务结束,如果超过了timeout时间还未结束,尝试强行关停shutdownNow()正在执行的任务。

void shutdownAndAwaitTermination(ExecutorService pool) {
    pool.shutdown(); // Disable new tasks from being submitted
    try {
        // Wait a while for existing tasks to terminate
        if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
            pool.shutdownNow(); // Cancel currently executing tasks
            // Wait a while for tasks to respond to being cancelled
            if (!pool.awaitTermination(60, TimeUnit.SECONDS)) {
                System.err.println("Pool did not terminate");
            }
        }
    } catch (InterruptedException ie) {
        // (Re-)Cancel if current thread also interrupted
        pool.shutdownNow();
        // Preserve interrupt status
        Thread.currentThread().interrupt();
    }
}

对任务提交方法的拓展

另外新增加的submit(Runnable)/submit(Runnable, T)/submit(Callable<T>)可以返回Future<T>类型的结果,用于异步任务的追踪。这是对Executor.execute(Runnable)的拓展,毕竟很多任务不是说只要执行了就完事儿了,还是要返回一些东西的。Callable是对Runnable的一种更好的抽象,它认为任务执行可能返回一个值,或者抛出一个异常。而使用Runnable则比较受限,当需要返回值的时候,只能做到将值写到一个文件或者共享数据域中。

AbstractExecutorService,是对ExecutorService的抽象实现,其submit方法将Runnable/Callable任务封装成为RunnableFuture类型的任务(使用newTaskFor()方法实现,默认返回一个FutureTask<T>),并调用execute()方法执行该任务,最终将RunnableFuture返回给调用者,调用者可以使用get()获取其结果。

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

RunnableFuture的默认实现是FutureTask,是通过newTaskFor(Runnable/Callable)方法完成转换的。
1. Callable转换过程:Callable -> FutureTask;
2. Runnable转换过程:Runnable -> Callable -> FutureTask。

具体的Runnable转FutureTask过程:

    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

In class Executors.java:
    static final class RunnableAdapter<T> implements Callable<T> {
        final Runnable task;
        final T result;
        RunnableAdapter(Runnable task, T result) {
            this.task = task;
            this.result = result;
        }
        public T call() {
            task.run();
            return result;
        }
    }

Future:未来对任务结果的获取

关系:Future <- RunnableFuture => FutureTask

FutureTask是对Callable和Runnable的封装。其中维护的有Callable callableObject outcome,当然还有一大堆代表callable的状态。可能的状态转换为:

NEW -> COMPLETING -> NORMAL
NEW -> COMPLETING -> EXCEPTIONAL
NEW -> CANCELLED
NEW -> INTERRUPTING -> INTERRUPTED

当使用get()方法获取Future的结果时,其实就是获取outcome。
获取结果是,任务的执行状态可能有三种情况:
1. 如果此时状态为NORMAL,说明正常完成,返回outcome;
2. 如果状态为CANCELLED/INTERRUPTING/INTERRUPTED,说明任务被撤销了,返回CancellationException
3. 否则状态就是EXCEPTIONAL,说明任务执行过程中出现了异常,返回ExecutionException

Future接口代表异步计算的结果,可以通过isDone()/isCancelled()检查计算状态,可以通过cancel(boolean)撤销任务,也可以通过get()/get(long, TimeUnit)阻塞地去获取计算结果。
FutureTask实现了RunnableFuture接口,所以既实现了Future,又实现了Runnable。因此可以使用FutureTask代表一个可以提交到Executor的任务,并在未来获取其结果,比如:

ExecutorService executor = ...;
FutureTask<String> future = new FutureTask<String>(
                new Callable<String>() {
                    public String call() {
                        return xxx;
                    }
                });
Future<String> future = executor.execute(future);
String result = future.get();

CompletionService:及时获取已完成任务的结果

关系:CompletionService => ExecutorCompletionService

如果向Executor提交了一组任务,并希望每一个任务完成之后我们都能立刻获取其结果。

我们可以这么实现:使用ExecutorService的invokeAll(Collection<? extends Callable<T>> tasks)获取所有的Future对象List<Future<T>>。但是此时并不知道哪一个Future对象是已完成的,哪一个是未完成的,因此需要不断使用get(timeout=0)去轮询该列表,判断任务是否已经成功返回。当然,使用轮询,这种实现并不好。

可以使用CompletionService接口完成该功能。CompletionService结合了Executor和BlockingQueue的功能。

BlockingQueue可以很方便的实现生产者-消费者模式。而CompletionService就是将生产者(新的异步线程生产的Future)和消费者(获取生产出来的Future,并进行处理)进行解耦生产者只需要submit(Callable/Runnable)将新的任务提交,就可以生产Future,而消费者只要take()即可获取消费对象

其默认具体实现类为ExecutorCompletionService。具体实现也很简单:
1. 首先构造ExecutorCompletionService对象的时候需要传入一个Executor,该executor为具体的任务提交者;
2. 然后在任务完成的时候,将结果Future加入BlockingQueue;
3. 最后使用take()获取结果,其实就是直接使用BlockingQueue的take()去获取结果Future,然后对结果调用get()获取返回结果就行了。

所以CompletionService其实就是BlockingQueue + FutureTask(在ExecutorCompletionService中是QueueingFuture)的组合。

值得一提的是submit(Callable/Runnable)方法:

    public Future<V> submit(Callable<V> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<V> f = newTaskFor(task);
        executor.execute(new QueueingFuture(f));
        return f;
    }

先使用newTaskFor(Callable)将Callable对象封装成了RunnableFuture(FutureTask的接口),然后进一步包装成QueueingFuture,交给Executor执行即可。

QueueingFuture是私有静态嵌套类,其继承FutureTask并覆写了方法done()

    /**
     * FutureTask extension to enqueue upon completion
     */
    private class QueueingFuture extends FutureTask<Void> {
        QueueingFuture(RunnableFuture<V> task) {
            super(task, null);
            this.task = task;
        }
        protected void done() { completionQueue.add(task); }
        private final Future<V> task;
    }

为什么覆写done()?因为在FutureTask中,一个任务执行完毕之后(无论成功还是抛异常,都叫完成),会调用done()方法进行后续操作。FutureTask给的默认操作是什么都不做:protected void done() { }。所以这里QueueingFuture通过覆写done()方法protected void done() { completionQueue.add(task); },将其语义变为“一旦执行完成,就将结果加入到BlockingQueue队列中”,从而使得submit(Callable/Runnable)会将任务结果RunnableFuture<T>加入到阻塞队列中。

示例:

package example.concurrency.executor;

import org.apache.commons.lang3.RandomUtils;

import java.util.concurrent.*;

/**
 * 提交一大堆并行任务,然后使用{@link CompletionService}及时获取{@link Future}结果。
 * 避免了轮询。
 *
 * @author liuhaibo on 2018/06/13
 */
public class CompletionServiceDemo {

    private static final int TASK_NUM = 20;

    private static Callable<String> createCallableTask(String str) {
        return () -> {
            int time = RandomUtils.nextInt(10, 1000);
            TimeUnit.MILLISECONDS.sleep(time);
            System.out.println("Finished: " + Thread.currentThread().getName() + " time=" + time);
            return str;
        };
    }

    private static void doAnotherJob() throws InterruptedException {
        Thread.sleep(1000);
    }

    public static void main(String... args) throws InterruptedException {
        Executor executor = new ThreadPoolExecutor(
                4,
                8,
                20L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadPoolExecutor.AbortPolicy());

        CompletionService<String> getStringService = new ExecutorCompletionService<>(executor);

        // 提交一堆并行任务,最好是和CPU无关的,比如I/O密集型的下载任务
        for (int i = 0; i < TASK_NUM; i++) {
            getStringService.submit(createCallableTask("Hello => " + i));
        }

        // 同时并行做一些耗时的任务
        doAnotherJob();

        // 使用CompletionService,去BlockingQueue中取一些已完成的任务
        // 另一种低级实现方法:保留每个任务关联的Future,并反复调用future.get(timeout=0)去轮询,获取任务完成情况
        // 或者使用invokeAll,获取返回的List<Future<T>>,然后反复调用future.get(timeout=0)去轮询
        try {
            for (int i = 0; i < TASK_NUM; i++) {
                Future<String> future = getStringService.take();
                String s = future.get();
                System.out.println(s);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }

        System.out.println(executor);
    }
}

一个可能的结果:

    Finished: pool-1-thread-1 time=27
    Finished: pool-1-thread-4 time=235
    Finished: pool-1-thread-3 time=345
    Finished: pool-1-thread-4 time=282
    Finished: pool-1-thread-2 time=770
    Finished: pool-1-thread-2 time=160
    Finished: pool-1-thread-1 time=948
    Hello => 0
    Hello => 3
    Hello => 2
    Hello => 5
    Hello => 1
    Hello => 8
    Hello => 4
    Finished: pool-1-thread-2 time=104
    Hello => 9
    Finished: pool-1-thread-3 time=740
    Hello => 6
    Finished: pool-1-thread-2 time=228
    Hello => 11
    Finished: pool-1-thread-4 time=890
    Hello => 7
    Finished: pool-1-thread-4 time=30
    Hello => 14
    Finished: pool-1-thread-2 time=189
    Hello => 13
    Finished: pool-1-thread-3 time=375
    Hello => 12
    Finished: pool-1-thread-2 time=228
    Hello => 16
    Finished: pool-1-thread-1 time=826
    Hello => 10
    Finished: pool-1-thread-3 time=446
    Hello => 17
    Finished: pool-1-thread-2 time=487
    Hello => 18
    Finished: pool-1-thread-4 time=757
    Hello => 15
    Finished: pool-1-thread-1 time=406
    Hello => 19
    java.util.concurrent.ThreadPoolExecutor@1f17ae12[Running, pool size = 4, active threads = 0, queued tasks = 0, completed tasks = 20]

可以看到先完成的任务先放入BlockingQueue,然后在take()的时候被立刻消费了,输出“Hello => i”。

猜你喜欢

转载自blog.csdn.net/puppylpg/article/details/80683101