Effective Java 第三版——80. EXECUTORS, TASKS, STREAMS 优于线程

时间:2022-11-20 03:59:42

Tips

书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code

注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

Effective Java 第三版——80. EXECUTORS, TASKS, STREAMS 优于线程

80. EXECUTORS, TASKS, STREAMS 优于线程

本书的第一版包含一个简单工作队列的代码[Bloch01,条目 49]。 此类允许客户端将后台线程的异步处理工作排入队列。 当不再需要工作队列时,客户端可以调用一个方法,要求后台线程在完成队列中已有的任何工作后正常终止自身。 实现只不过是个玩具,但即便如此,它还需要一整页精细,细致的代码,如果你没有恰到好处的话,这种代码很容易出现安全和活性失败。 幸运的是,没有理由再编写这种代码了。

到本书第二版出版时,java.util.concurrent包已添加到Java中。 该包包含一个Executor Framework,它是一个灵活的基于接口的任务执行工具。 创建一个比本书第一版更好的工作队列只需要一行代码:

ExecutorService exec = Executors.newSingleThreadExecutor();

下面是如何提交一个可运行的(runnable)执行:

exec.execute(runnable);

下面是如何告诉executor优雅地终止(如果做不到这一点,你的虚拟机很可能不会退出):

exec.shutdown();

可以使用执行器服务(executor service)做更多的事情。例如,可以等待一个特定任务完成(条目 79中使用get方法, 319页),可以等待任何或全部任务完成的集合(使用invokeAny或invokeAll方法),也可以等待执行者服务终止(使用awaitTermination方法),可以在完成任务时逐个检索任务结果(使用ExecutorCompletionService),可以安排任务在特定时间运行或定期运行(使用ScheduledThreadPoolExecutor),等等。

如果希望多个线程处理来自队列的请求,只需调用另一个静态工厂,该工厂创建一种称为线程池的不同类型的执行器服务。 可以创建具有固定或可变数量线程的线程池。 java.util.concurrent.Executors类包含静态工厂,它们提供了你需要的大多数执行程序。 但是,如果想要一些与众不同的东西,可以直接使用ThreadPoolExecutor类。 此类允许你配置线程池操作的几乎每个方面。

为特定应用程序选择执行程序服务可能很棘手。 对于小程序或负载较轻的服务器,Executors.newCachedThreadPool通常是一个不错的选择,因为它不需要配置,通常“做正确的事情”。但是对于负载很重的生产服务器来说,缓存线程池不是一个好的选择! 在缓存线程池中,提交的任务不会排队,而是立即传递给线程执行。 如果没有可用的线程,则创建一个新线程。 如果服务器负载过重以至于所有CPU都被充分利用并且更多任务到达时,则会创建更多线程,这只会使事情变得更糟。 因此,在负载很重的生产服务器中,最好使用Executors.newFixedThreadPool,它提供具有固定线程数的池,或直接使用ThreadPoolExecutor类,以实现最大程度的控制。

不仅应该避免编写自己的工作队列,而且通常应该避免直接使用线程。 当直接使用Thread类时,线程既可以作为工作单元,也可以作为执行它的机制。 在executor framework中,工作单元和执行机制是分开的。 关键的抽象是工作单元,称为任务。 有两种任务:Runnable及其近亲Callable(类似于Runnable,除了它返回一个值并且可以抛出任意异常)。 执行任务的一般机制是executor service。 如果从任务的角度来看,让executor service为你执行它们,可以灵活地选择适当的执行策略以满足你的需求,并在需求发生变化时更改策略。 本质上本质上,Executor Framework执行的功能与Collections Framework聚合(aggregation)功能是相同的。

在Java 7中,Executor Framework被扩展为支持fork-join任务,这些任务由称为fork-join池的特殊executor service运行。 由ForkJoinTask实例表示的fork-join任务可以拆分为较小的子任务,而包含ForkJoinPool的线程不仅处理这些任务,而且还“彼此”窃取“任务”以确保所有线程都保持忙碌,从而导致更高的任务 CPU利用率,更高的吞吐量和更低的延迟。 编写和调优fork-join任务很棘手。 并行流(Parallel streams)(条目 48)是在fork-join池之上编写的,假设它们适合当前的任务,那么你可以轻松地利用它们的性能优势。

对Executor Framework的完整处理超出了本书的范围,但感兴趣的读者可以参考 《Java Concurrency in Practice》一书[Goetz06]。