Java并发编程实战 任务执行总结

在线程中执行任务
在围绕 任务执行 来设计应用程序结构时 第一步就是要找出清晰的任务边界 在理想情况下 各个任务之间是相互独立的:任务并不依赖于其他任务的状态 结果或边界效应 独立性有助于实现并发 因为如果存在足够多的处理资源 那么这些独立的任务都可以并行执行 为了在调度与负载均衡等过程中实现更高的灵活性 每项任务还应该表示应用程序的一小部分处理能力

串行地执行任务
在应用程序中可以通过多种策略来调度任务 而其中一些策略能够更好地利用潜在的并发性 最简单的策略就是在单个线程中串行地执行各项任务

串行的Web服务器

public class SingleThreadWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            Socket connection = socket.accept();
            handleRequest(connection);
        }
    }

    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

显式地为任务创建线程
通过为每个请求创建一个新的线程来提供服务 从而实现更高的响应性

在Web服务器中为每个请求启动一个新的线程(不要这么做)

public class ThreadPerTaskWebServer {
    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
            new Thread(task).start();
        }
    }

    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

在正常负载情况下 为每个任务分配一个线程 的方法能提升串行执行的性能 只要请求的到达速率不超出服务器的请求处理能力 那么这种方法可以同时带来更快的响应性和更高的吞吐率

无限制创建线程的不足
在生产环境中 为每个任务分配一个线程 这种方法存在一些缺陷 尤其是当需要创建大量的线程时:

  • 线程生命周期的开销非常高
  • 资源消耗
  • 稳定性

Executor框架
虽然Executor是个简单的接口 但它却为灵活且强大的异步任务执行框架提供了基础 该框架能支持多种不同类型的任务执行策略 它提供了一种标准的方法将任务的提交过程与执行过程解耦开来 并用Runnable来表示任务 Executor的实现还提供了对生命周期的支持 以及统计信息收集 应用程序管理机制和性能监视等机制

示例:基于Executor的Web服务器

基于线程池的Web服务器

public class TaskExecutionWebServer {
    private static final int NTHREADS = 100;
    private static final Executor exec
            = Executors.newFixedThreadPool(NTHREADS);

    public static void main(String[] args) throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (true) {
            final Socket connection = socket.accept();
            Runnable task = new Runnable() {
                public void run() {
                    handleRequest(connection);
                }
            };
            exec.execute(task);
        }
    }

    private static void handleRequest(Socket connection) {
        // request-handling logic here
    }
}

我们可以很容易地将TaskExecutionWebServer修改为类似ThreadPerTaskWebServer的行为 只需使用一个为每个请求都创建新线程的Executor

为每个请求启动一个新线程的Executor

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

同样 还可以编写一个Executor使TaskExecutionWebServer的行为类似于单线程的行为 即以同步的方式执行每个任务 然后再返回

在调用线程中以同步方式执行所有任务的Executor

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

执行策略
通过将任务的提交与执行解耦开来 从而无须太大的困难就可以为某种类型的任务指定和修改执行策略 在执行策略中定义了任务执行的 What Where When How 等方面 包括:

  • 在什么(What)线程中执行任务
  • 任务按照什么(What)顺序执行(FIFO LIFO 优先级)
  • 有多少个(How Many)任务能并发执行
  • 在队列中有多少个(How Many)任务在等待执行
  • 如果系统由于过载而需要拒绝一个任务 那么应该选择哪一个(Which)任务 另外 如何(How)通知应用程序有任务被拒绝
  • 在执行一个任务之前或之后 应该进行哪些(What)动作

每当看到下面这种形式的代码时:
new Thread(runnable).start()
并且你希望获得一种更灵活的执行策略时 请考虑使用Executor来代替Thread

线程池
线程池 从字面含义来看 是指管理一组同构工作线程的资源池 线程池是与工作队列(Work Queue)密切相关的 其中在工作队列中保存了所有等待执行的任务 工作者线程(Worker Thread)的任务很简单:从工作队列中获取一个任务 执行任务 然后返回线程池并等待下一个任务
在线程池中执行任务 比 为每个任务分配一个线程 优势更多 通过重用现有的线程而不是创建新线程 可以在处理多个请求时分摊在线程创建和销毁过程中产生的巨大开销 另一个额外的好处是 当请求到达时 工作线程通常已经存在 因此不会由于等待创建线程而延迟任务的执行 从而提高了响应性 通过适当调整线程池的大小 可以创建足够多的线程以便使处理器保持忙碌状态 同时还可以防止过多线程相互竞争资源而使应用程序耗尽内存或失败

Executor的生命周期
我们已经知道如何创建一个Executor 但并没有讨论如何关闭它 Executor的实现通常会创建线程来执行任务 但JVM只有在所有(非守护)线程全部终止后才会退出 因此 如果无法正确地关闭Executor 那么JVM将无法结束

为了解决执行服务的生命周期问题 Executor扩展了ExecutorService接口 添加了一些用于生命周期管理的方法(同时还有一些用于任务提交的便利方法)

ExecutorService的生命周期有3种状态:运行 关闭和已终止 ExecutorService在初始创建时处于运行状态 shutdown方法将执行平缓的关闭过程:不再接受新的任务 同时等待已经提交的任务执行完成-包括那些还未开始执行的任务 shutdownNow方法将执行粗暴的关闭过程:它将尝试取消所有运行中的任务 并且不再启动队列中尚未开始执行的任务

支持关闭操作的Web服务器

public class LifecycleWebServer {
    private final ExecutorService exec = Executors.newCachedThreadPool();

    public void start() throws IOException {
        ServerSocket socket = new ServerSocket(80);
        while (!exec.isShutdown()) {
            try {
                final Socket conn = socket.accept();
                exec.execute(new Runnable() {
                    public void run() {
                        handleRequest(conn);
                    }
                });
            } catch (RejectedExecutionException e) {
                if (!exec.isShutdown())
                    log("task submission rejected", e);
            }
        }
    }

    public void stop() {
        exec.shutdown();
    }

    private void log(String msg, Exception e) {
        Logger.getAnonymousLogger().log(Level.WARNING, msg, e);
    }

    void handleRequest(Socket connection) {
        Request req = readRequest(connection);
        if (isShutdownRequest(req))
            stop();
        else
            dispatchRequest(req);
    }

    interface Request {
    }

    private Request readRequest(Socket s) {
        return null;
    }

    private void dispatchRequest(Request r) {
    }

    private boolean isShutdownRequest(Request r) {
        return false;
    }
}

延迟任务与周期任务
Timer类负责管理延迟任务(在100ms后执行该任务)以及周期任务(每10ms执行一次该任务) 然而 Timer存在一些缺陷 因此应该考虑使用ScheduledThreadPoolExecutor来代替它 可以通过ScheduledThreadPoolExecutor的构造函数或newScheduledThreadPool工厂方法来创建该类的对象

错误的Timer行为

public class OutOfTime {
    public static void main(String[] args) throws Exception {
        Timer timer = new Timer();
        timer.schedule(new ThrowTask(), 1);
        SECONDS.sleep(1);
        timer.schedule(new ThrowTask(), 1);
        SECONDS.sleep(5);
    }

    static class ThrowTask extends TimerTask {
        public void run() {
            throw new RuntimeException();
        }
    }
}

找出可利用的并行性
Executor框架帮助指定执行策略 但如果要使用Executor 必须将任务表述为一个Runnable 在大多数服务器应用程序中都存在一个明显的任务边界:单个客户请求 但有时候 任务边界并非是显而易见的 例如在很多桌面应用程序中 即使是服务器应用程序 在单个客户请求中仍可能存在可发掘的并行性 例如数据库服务器

示例:串行的页面渲染器

public abstract class SingleThreadRenderer {
    void renderPage(CharSequence source) {
        renderText(source);
        List imageData = new ArrayList();
        for (ImageInfo imageInfo : scanForImageInfo(source))
            imageData.add(imageInfo.downloadImage());
        for (ImageData data : imageData)
            renderImage(data);
    }

    interface ImageData {
    }

    interface ImageInfo {
        ImageData downloadImage();
    }

    abstract void renderText(CharSequence s);
    abstract List scanForImageInfo(CharSequence s);
    abstract void renderImage(ImageData i);
}

携带结果的任务Callable与Future
Executor框架使用Runnable作为其基本的任务表示形式 Runnable是一种有很大局限的抽象 虽然run能写入到日志文件或者将结果放入某个共享的数据结构 但它不能返回一个值或抛出一个受检查的异常
许多任务实际上都是存在延迟的计算-执行数据库查询 从网络上获取资源 或者计算某个复杂的功能 对于这些任务 Callable是一种更好的抽象:它认为主入口点(即call)将返回一个值 并可能抛出一个异常 在Executor中包含了一些辅助方法能将其他类型的任务封装为一个Callable 例如Runnable和java.security.PrivilegedAction
Future表示一个任务的生命周期 并提供了相应的方法来判断是否已经完成或取消 以及获取任务的结果和取消任务等 在Future规范中包含的隐含意义是 任务的生命周期只能前进 不能后退 就像ExecutorService的生命周期一样 当某个任务完成后 它就永远停留在 完成 状态上

示例:使用Future实现页面渲染器
为了使页面渲染器实现更高的并发性 首先将渲染过程分解为两个任务 一个是渲染所有的文本 另一个是下载所有的图像 (因为其中一个任务是CPU密集型 而另一个任务是I/O密集型 因此这种方法即使在单CPU系统上也能提升性能)

使用Future等待图像下载

public abstract class FutureRenderer {
    private final ExecutorService executor = Executors.newCachedThreadPool();

    void renderPage(CharSequence source) {
        final List imageInfos = scanForImageInfo(source);
        Callable> task =
                new Callable>() {
                    public List call() {
                        List result = new ArrayList();
                        for (ImageInfo imageInfo : imageInfos)
                            result.add(imageInfo.downloadImage());
                        return result;
                    }
                };

        Future> future = executor.submit(task);
        renderText(source);

        try {
            List imageData = future.get();
            for (ImageData data : imageData)
                renderImage(data);
        } catch (InterruptedException e) {
            // Re-assert the thread's interrupted status
            Thread.currentThread().interrupt();
            // We don't need the result, so cancel the task too
            future.cancel(true);
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }

    interface ImageData {
    }

    interface ImageInfo {
        ImageData downloadImage();
    }

    abstract void renderText(CharSequence s);

    abstract List scanForImageInfo(CharSequence s);

    abstract void renderImage(ImageData i);
}

FutureRenderer使得渲染文本任务与下载图像数据的任务并发地执行 当所有图像下载完后 会显示到页面上 这将提升用户体验 不仅使用户更快地看到结果 还有效利用了并行性 但我们还可以做得更好 用户不必等到所有的图像都下载完成 而希望看到每当下载完一幅图像时就立即显示出来

在异构任务并行化中存在的局限
FutureRenderer使用了两个任务 其中一个负责渲染文本 另一个负责下载图像 如果渲染文本的速度远远高于下载图像的速度(可能性很大) 那么程序的最终性能与串行执行时的性能差别不大 而代码却变得更复杂了 当使用两个线程时 至多能将速度提高一倍 因此 虽然做了许多工作来并发执行异构任务以提高并发度 但从中获得的并发性却是十分有限的

只有当大量相互独立且同构的任务可以并发进行处理时 才能体现出将程序的工作负载分配到多个任务中带来的真正性能提升

CompletionService:Executor与BlockingQueue
CompletionService将Executor和BlockingQueue的功能融合在一起 你可以将Callable任务提交给它来执行 然后使用类似于队列操作的take和poll等方法来获得已完成的结果 而这些结果会在完成时将被封装为Future ExecutorCompletionService实现了CompletionService 并将计算部分委托给一个Executor

示例:使用CompletionService实现页面渲染器
可以通过CompletionService从两个方面来提高页面渲染器的性能:缩短总运行时间以及提高响应性 为每一幅图像的下载都创建一个独立任务 并在线程池中执行它们 从而将串行的下载过程装换为并行的过程:这将减少下载所有图像的总时间 此外 通过从CompletionService中获取结果以及使每张图片在下载完成后立刻显示出来 能使用户获得一个更加动态和更高响应性的用户界面

使用CompletionService 使页面元素在下载完成后立即显示出来

public abstract class Renderer {
    private final ExecutorService executor;

    Renderer(ExecutorService executor) {
        this.executor = executor;
    }

    void renderPage(CharSequence source) {
        final List info = scanForImageInfo(source);
        CompletionService completionService =
                new ExecutorCompletionService(executor);
        for (final ImageInfo imageInfo : info)
            completionService.submit(new Callable() {
                public ImageData call() {
                    return imageInfo.downloadImage();
                }
            });

        renderText(source);

        try {
            for (int t = 0, n = info.size(); t < n; t++) {
                Future f = completionService.take();
                ImageData imageData = f.get();
                renderImage(imageData);
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }

    interface ImageData {
    }

    interface ImageInfo {
        ImageData downloadImage();
    }

    abstract void renderText(CharSequence s);

    abstract List scanForImageInfo(CharSequence s);

    abstract void renderImage(ImageData i);

}

为任务设置时限
在有限时间内执行任务的主要困难在于 要确保得到答案的时间不会超过限定的时间 或者在限定的时间内无法获得答案 在支持时间限制的Future.get中支持这种需求:当结果可用时 它将立即返回 如果在指定时限内没有计算出结果 那么将抛出TimeoutException

在指定时间内获取广告信息

public class RenderWithTimeBudget {
    private static final Ad DEFAULT_AD = new Ad();
    private static final long TIME_BUDGET = 1000;
    private static final ExecutorService exec = Executors.newCachedThreadPool();

    Page renderPageWithAd() throws InterruptedException {
        long endNanos = System.nanoTime() + TIME_BUDGET;
        Future f = exec.submit(new FetchAdTask());
        // Render the page while waiting for the ad
        Page page = renderPageBody();
        Ad ad;
        try {
            // Only wait for the remaining time budget
            long timeLeft = endNanos - System.nanoTime();
            ad = f.get(timeLeft, NANOSECONDS);
        } catch (ExecutionException e) {
            ad = DEFAULT_AD;
        } catch (TimeoutException e) {
            ad = DEFAULT_AD;
            f.cancel(true);
        }
        page.setAd(ad);
        return page;
    }

    Page renderPageBody() { return new Page(); }


    static class Ad {
    }

    static class Page {
        public void setAd(Ad ad) { }
    }

    static class FetchAdTask implements Callable {
        public Ad call() {
            return new Ad();
        }
    }

}

示例:旅行预定门户网站
预订时间 方法可以很容易地扩展到任意数量的任务上

在预订时间内请求旅游报价

public class TimeBudget {
    private static ExecutorService exec = Executors.newCachedThreadPool();

    public List getRankedTravelQuotes(TravelInfo travelInfo, Set companies,
                                                   Comparator ranking, long time, TimeUnit unit)
            throws InterruptedException {
        List tasks = new ArrayList();
        for (TravelCompany company : companies)
            tasks.add(new QuoteTask(company, travelInfo));

        List> futures = exec.invokeAll(tasks, time, unit);

        List quotes =
                new ArrayList(tasks.size());
        Iterator taskIter = tasks.iterator();
        for (Future f : futures) {
            QuoteTask task = taskIter.next();
            try {
                quotes.add(f.get());
            } catch (ExecutionException e) {
                quotes.add(task.getFailureQuote(e.getCause()));
            } catch (CancellationException e) {
                quotes.add(task.getTimeoutQuote(e));
            }
        }

        Collections.sort(quotes, ranking);
        return quotes;
    }

}

class QuoteTask implements Callable {
    private final TravelCompany company;
    private final TravelInfo travelInfo;

    public QuoteTask(TravelCompany company, TravelInfo travelInfo) {
        this.company = company;
        this.travelInfo = travelInfo;
    }

    TravelQuote getFailureQuote(Throwable t) {
        return null;
    }

    TravelQuote getTimeoutQuote(CancellationException e) {
        return null;
    }

    public TravelQuote call() throws Exception {
        return company.solicitQuote(travelInfo);
    }
}

interface TravelCompany {
    TravelQuote solicitQuote(TravelInfo travelInfo) throws Exception;
}

interface TravelQuote {
}

interface TravelInfo {
}

小结
通过围绕任务执行来设计应用程序 可以简化开发过程 并有助于实现并发 Executor框架将任务提交与执行策略解耦开来 同时还支持多种不同类型的执行策略 当需要创建线程来执行任务时 可以考虑使用Executor 要想在将应用程序分解为不同的任务时获得最大的好处 必须定义清晰地任务边界 某些应用程序中存在着比较明显的任务边界 而在其他一些程序中则需要进一步分析才能揭示出粒度更细的并行性

你可能感兴趣的:(Java并发)