Java多线程(5)——异步编程

同步计算与异步计算


以同步方式执行的任务,我们称之为同步任务,其任务的发起与任务的执行是在同一条时间线上进行的。换而言之,任务的发起与任务的执行是串行的。

以异步方式执行的任务,我们称之为异步任务,其任务的发起与任务的执行是在不同的时间线上进行的。换而言之,任务的发起与任务的执行是并发的。

同步方式与异步方式的说法是相对的:同一个任务我们既可以说它是异步任务,也可以说它是同步任务。假设我们用一个Runnable实例task来表示一个任务,如果我们直接调用task.run()来执行该任务,那么我们就可以称该任务为同步任务;如果我们通过new Thread(task).start()调用创建并启动一个专门的工作者线程来执行该任务,或者将该任务提交给一个Executor实例executor执行,那么我们就可以称该任务为异步任务。
同步方式与异步方式的称呼不仅仅取决于一个任务的具体执行方式,还取决于我们的观察角度。在上述例子中, 假设我们将task提交给线程池执行, 那么从该任务提交线程(即ThreadPoolExecutor.submit方法的执行线程)的角度来看它是一个异步任务,而从线程池中的工作者线程(即实际执行该任务的线程)的角度来看该任务则可能是一个同步任务。

同步任务的发起线程在其发起该任务之后必须等待该任务执行结束才能够执行其他操作,这种等待可能意味着阻塞或者轮询。

异步任务的发起线程在其发起该任务之后不必等待该任务结束便可以继续执行其他操作,即异步任务的发起与实际执行可以是并发的。多线程编程本质上是异步的。比如一个线程通过ThreadPoolExecutor.submit(Callable<T>)调用向线程池提交一个任务,在该调用返回之后该线程便可以执行其他操作了,而该任务可能在此之后才被线程池中的某一个工作者线程所执行,这里任务的提交与执行是并发的,而不是串行的。

异步任务执行方式往往意味着非阻塞。然而,阻塞与非阻塞只是任务执行方式的一种属性,它与任务执行方式之间并没有必然的关系。异步任务的执行需要借助多个线程来实现。多个异步任务能够以并发的方式被执行。

同步方式的优点是代码简单、直观,缺点是它往往意味着阻塞,而阻塞会限制系统的吞吐率。异步方式往往意味着非阻塞,因而有利于提高系统的吞吐率。异步方式的代价是更为复杂的代码和更多的资源投入。

Executor

java.util.Executor接口是对任务的执行进行的抽象。

public interface Executor {
    void execute(Runnable command);
}

Executor接口使得任务的提交能够与任务执行的具体细节解耦。和对任务处理逻辑的抽象类似,对任务执行的抽象也能给我们带来信息隐藏和关注点分离的好处。

ExecutorService接口继承自Executor接口,定义了几个submit方法,这些方法能够接受Callable接口或者Runnable接口表示的任务并返回相应的Future实例,从而使客户端代码提交任务后可以获取任务的执行结果。
ExecutorService接口还定义了shutdown方法和shutdownNow方法来关闭相应的服务。
ThreadPoolExecutorExecutorService的默认实现类。

public interface ExecutorService extends Executor {

    void shutdown();

    List<Runnable> shutdownNow();

    boolean isShutdown();

    boolean isTerminated();

    boolean awaitTermination(long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> Future<T> submit(Callable<T> task);

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

    Future<?> submit(Runnable task);

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
        throws InterruptedException;

    <T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
                                  long timeout, TimeUnit unit)
        throws InterruptedException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks)
        throws InterruptedException, ExecutionException;

    <T> T invokeAny(Collection<? extends Callable<T>> tasks,
                    long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

实用工具类 Executors

  • newCachedThreadPool相当于new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>())
    即一个核心线程池大小为0,最大线程池大小不受限,工作者线程允许的最大空闲时间为 60 秒, 内部以SynchronousQueue为工作队列的一个线程池。
    这种配置意味着该线程池中的所有工作者线程在空闲了指定的时间后都可以被自动清理掉。
    SynchronousQueue内部并不维护用于存储队列元素的实际存储空间。生产者线程在执行SynchronousQueue.offer(E)的时候,如果消费者线程因执行SynchronousQueue.take()而被暂停, 那么SynchronousQueue.offer(E)调用会直接返回false,即入队列失败。因此,在该线程池中无空闲工作者线程的情况下提交任务会导致该任务无法被缓存成功。而ThreadPoolExecutor在任务缓存失败的情况下会创建并启动新的工作者线程。在极端的情况下,给该线程池每提交一个任务都会导致一个新的工作者线程被创建并启动,而这最终会导致系统中的线程过多,从而导致过多的上下文切换而使得整个系统被拖慢。
    因此,该方法所返回的线程池适合于用来执行大量耗时较短且提交频率较高的任务。
  • newFixedThreadPool相当于new ThreadPoolExecutor(n, n, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())
    即一个以无界队列为工作,核心线程池大小与最大线程池大小均为n且空闲工作者线程不会被自动清理的线程池。
    这是一种线程池大小一旦达到其核心线程池大小就既不会增加也不会减少工作者线程的固定大小的线程池。因此,这样的线程池实例一旦不再需要,我们必须主动将其关闭。
  • newSingleThreadExecutor。该方法的返回值基本相当于Executors.newFixedThreadPool(1)所返回的线程池。不过,该线程池并非ThreadPoolExecutor实例,而是一个封装了ThreadPoolExecutor实例的ExecutorService实例。该线程池便于我们实现单(多)生产者一单消费者模式。
    该线程池确保了在任意一个时刻只有一个任务会被执行,这就形成了类似锁将原本并发的操作改为串行的操作的效果。因此,该线程池适合于用来执行访问了非线程安全对象而我们又不希望因此而引入锁的任务。
    该线程池也适合于用来执行 IO 操作, 因为 IO 操作往往受限于相应的 IO 设备, 使用多个线程执行同一种 IO 操作可能并不会提 高 IO 效率,所以如果使用一个线程执行 IO 足以满足要求,那么仅使用一个线程即可,这样可以保障程序的简单性以避免一些不必要的问题(比如死锁) 。

CompletionService

java.utiL.concurrent.CompletionService接口为异步任务的批量提交以及获取这些任务的处理结果提供了便利。

public interface CompletionService<V> {

	// 提交异步任务
    Future<V> submit(Callable<V> task);

    Future<V> submit(Runnable task, V result);

    // 阻塞方法 获取异步任务处理结果
    Future<V> take() throws InterruptedException;

    // 非阻塞方法 获取异步任务处理结果
    Future<V> poll();

    Future<V> poll(long timeout, TimeUnit unit) throws InterruptedException;
}

public class ExecutorCompletionService<V> implements CompletionService<V> {
    private final Executor executor;
    private final AbstractExecutorService aes;
    private final BlockingQueue<Future<V>> completionQueue;

    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;
    }

    private RunnableFuture<V> newTaskFor(Callable<V> task) {
        if (aes == null)
            return new FutureTask<V>(task);
        else
            return aes.newTaskFor(task);
    }

    private RunnableFuture<V> newTaskFor(Runnable task, V result) {
        if (aes == null)
            return new FutureTask<V>(task, result);
        else
            return aes.newTaskFor(task, result);
    }

    public ExecutorCompletionService(Executor executor) {
        if (executor == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = new LinkedBlockingQueue<Future<V>>();
    }

    public ExecutorCompletionService(Executor executor,
                                     BlockingQueue<Future<V>> completionQueue) {
        if (executor == null || completionQueue == null)
            throw new NullPointerException();
        this.executor = executor;
        this.aes = (executor instanceof AbstractExecutorService) ?
            (AbstractExecutorService) executor : null;
        this.completionQueue = completionQueue;
    }

    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;
    }

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

    public Future<V> take() throws InterruptedException {
        return completionQueue.take();
    }

    public Future<V> poll() {
        return completionQueue.poll();
    }

    public Future<V> poll(long timeout, TimeUnit unit)
            throws InterruptedException {
        return completionQueue.poll(timeout, unit);
    }

}

ExecutorCompletionService相当于Executor实例与BlockingQueue实例的一个融合体。其中,Executor实例负责接收并执行异步任务,而BlockingQueue实例则用于存储已执行完毕的异步任务对应的Future实例。ExecutorCompletionService会为其客户端提交的每个异步任务都创建一个相应的Future实例,通过该实例其客户端代码便可以获取相应异步任务的处理结果。ExecutorCompletionService每执行完一个异步任务, 就将该任务对应的Future实例存入其内部维护的BlockingQueue实例之中,而其客户端代码则可以通过ExecutorCompletionService.take()调用来获取这个Future实例。

FutureTask

java.util.concurrent.FutureTask则融合了Runnable接口和Callable接口的优点:
FutureTaskRunnable接口的一个实现类,因此由它表示的异步任务可以交给专门的工作者线程执行,也可以交给Executor实例(比如线程池)执行,还能够直接返回其代表的异步任务的处理结果。

FutureTask 是java.util.concurrent,RunnableFuture接口的一个实现类。RunnableFuture接口继承了Future接口和Runnable接口

public interface RunnableFuture<V> extends Runnable, Future<V> {
    void run();
}
public interface Future<V> {
    boolean cancel(boolean mayInterruptIfRunning);

    boolean isCancelled();

    boolean isDone();

    /**
     * Waits if necessary for the computation to complete, and then
     * retrieves its result.
     *
     * @return the computed result
     * @throws CancellationException if the computation was cancelled
     * @throws ExecutionException if the computation threw an
     * exception
     * @throws InterruptedException if the current thread was interrupted
     * while waiting
     */
    V get() throws InterruptedException, ExecutionException;

    /**
     * Waits if necessary for at most the given time for the computation
     * to complete, and then retrieves its result, if available.
     */
    V get(long timeout, TimeUnit unit)
        throws InterruptedException, ExecutionException, TimeoutException;
}

FutureTask还支持以回调(Callback)的方式处理任务的执行结果。当任务执行结束后,FutureTask.done()会被执行。FutureTask.done()是一个protected方法,子类可以覆盖该方法并在其中实现对任务执行结果的处理。
FutureTask.done()中的代码可以通过FutureTask.get()调用来获取任务的执行结果,此时由于任务已经执行结束,因此FutureTask.get()调用并不会使得当前线程暂停。但是,由于任务的执行结束既包括正常终止,也包括异常终止以及任务被取消而导致的终止,因此done()方法中的代码可能需要在调用get()前调用isCancelled()来判断任务是否被取消,以免抛出CancellationException

FutureTask基本上是被设计用来表示一次性执行的任务,其内部会维护一个表示任务运行状态的状态变量,run在执行任务处理逻辑前会先判断相应任务的运行状态,如果该任务已经被执行过,那么会直接返回。
FutureTask.runAndReset()能够打破这种限制,使得一个FutureTask实例所代表的任务能够多次被执行。但是并不记录任务的处理结果。

计划任务

在有些情况下,我们可能需要事先提交一个任务,这个任务并不是立即被执行的,而是要在指定的时间或者周期性地被执行,这种任务就被称为 计划任务(Scheduled Task)
典型的计划任务包括清理系统垃圾数据、系统监控、数据备份等。

ExecutorService接口的子类ScheduledExecutorService接口定义了一组方法用于执行计划任务。默认实现类是java.util.concurrent.ScheduledThreadPoolExecutor类,它是ThreadPoolExecutor的一个子类。

public interface ScheduledExecutorService extends ExecutorService {

	// 延迟执行提交的任务
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

    public <V> ScheduledFuture<V> schedule(Callable<V> callable,
                                           long delay, TimeUnit unit);

    // 周期性执行提交的任务
    // 间隔 = max(执行时间, period)
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    // 间隔 = 执行时间 + delay
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

}





参考资料:《Java 多线程编程实战指南(核心篇)》 黄文海 著

posted @ 2020-02-22 22:59  JL916  阅读(701)  评论(0编辑  收藏  举报