这本书的第一版包含了一个简单工作队列的代码[Bloch01,49项)。这个类允许客户端通过后台线程为异步处理排队。当不再需要工作队列时,客户机可以调用一个方法,要求后台线程在完成队列上的任何工作后优雅地终止自己。个实现只不过是一个玩具,但即便如此,它也需要一整页微妙、微妙的代码,如果你做得不对,就很容易出现安全和活性失败。幸运的是,没有理由再编写这种代码了。
当这本书的第二版出版时,java.util.concurrent已经添加到Java中。这个包包含一个Executor框架,它是一个灵活的基于接口的任务执行工具。创建一个工作队列,在任何方面都比在这本书的第一版更好,只需要一行代码:
ExecutorService exec = Executors.newSingleThreadExecutor();
下面是如何提交一个可运行的执行:
exec.execute(runnable);
下面是如何告诉执行器优雅地终止(如果你做不到这一点,你的虚拟机很可能不会退出):
exec.shutdown();
您可以使用executor service做更多的事情。例如,您可以等待特定的任务完成(使用get方法,如第319页第79项所示),您可以等待任何或所有任务集合完成(使用invokeAny或invokeAll方法),您可以等待executor服务终止(使用awaitterminate方法),您可以在任务完成时一个一个地检索它们的结果(使用ExecutorCompletionService),您可以安排任务在特定的时间运行或定期运行(使用
ScheduledThreadPoolExecutor),等等。
如果希望多个线程处理来自队列的请求,只需调用一个不同的静态工厂,该工厂创建一种称为线程池的不同类型的执行器服务。您可以使用固定或可变数量的线程创建线程池。java.util.concurrent.Executors类包含静态工厂,这些工厂提供了您需要的大多数执行器。然而,如果你想要一些不同寻常的,您可以直接使用ThreadPoolExecutor类。这个类允许您配置线程池操作的几乎每个方面。
为特定的应用程序选择executor服务可能比较棘手。对于小程序或负载较轻的服务器,Executors.newCachedThreadPool通常是一个不错的选择,因为它不需要配置,而且通常“做正确的事情”。但是对于负载沉重的生产服务器来说,缓存的线程池不是一个好的选择!在缓存的线程池中,提交的任务不会排队,而是立即传递给线程执行。如果没有可用的线程,则创建一个新的线程。如果服务器负载过重,所有cpu都被充分利用,并且有更多的任务到达,就会创建更多的线程,这只会使情况变得更糟。因此,在负载沉重的生产服务器中,最好使用executor.newFixedThreadPool,它为您提供一个线程数量固定的池,或者直接使用ThreadPoolExecutor类来实现最大限度的控制。
您不仅应该避免编写自己的工作队列,而且通常还应该避免直接使用线程.当您直接使用线程时,线程既是工作单元,又是执行它的机制。在executor框架中,工作单元和执行机制是分开的。关键的抽象是工作单元,即任务。
有两种任务:Runnable和它的近亲Callable(与Runnable类似,只是它返回一个值并可以抛出任意异常)。执行任务的一般机制是executor service。如果您从任务的角度考虑问题,并让executor服务为您执行这些任务,那么您就可以灵活地选择合适的执行策略来满足您的需求,并在您的需求发生变化时更改策略。本质上,Executor框架执行的功能与Collections框架聚合的功能相同。
在Java 7中,Executor框架被扩展为支持fork-join任务,它们由一种特殊的执行器服务(称为fork-join池)运行。一个 fork-join任务,由一个ForkJoinTask实例表示,可以分解为更小的子任务,以及包含fork join池的线程不仅仅是进程 这些任务只是互相“窃取”任务,以确保所有线程都保持不变繁忙,导致更高的CPU利用率、更高的吞吐量和更低的延迟。编写和调优fork-join任务非常棘手。并行流(item48 )是在fork连接池之上编写的,假设它们适合当前的任务,那么您可以轻松地利用它们的性能优势。
对Executor框架的完整处理超出了本书的范围,但是感兴趣的读者可以在 Java Concurrency in Practice接触Java并发[Goetz06]。
本文写于2019.7.23,历时1天
网友评论