对Java执行服务的深入研究


Java ExecutorService是一种结构,它允许您传递要由线程异步执行的任务。执行器服务创建并维护用于执行提交的任务的可重用线程池。该服务还管理一个队列,当池中的任务多于线程数,并且需要将任务排队,直到有空闲线程可用于执行任务时,才会使用该队列。

在本文中,我们将重点介绍ExecutorService接口的ThreadPoolExecutor实现。实例化线程池执行器有两种方法。您可以使用它的构造函数重载之一直接实例化它,也可以使用Executors类中的一个工厂方法。

让我们看几个例子。

直接实例化具有10个线程的ThreadPoolExecutor、0毫秒的Keep AliveTime和一个LinkedBlockingQueue:

ExecutorService executorService = 
          new ThreadPoolExecutor(10, 10, 0L, TimeUnit.MILLISECONDS,
          new LinkedBlockingQueue<Runnable>());


使用Executors工厂方法实例化具有10个线程的ThreadPoolExecutor:

ExecutorService executor = Executors.newFixedThreadPool(10);


使用Executors工厂方法使用单个线程实例化ThreadPoolExecutor:

ExecutorService executor = Executors.newSingleThreadExecutor();


使用Executors工厂方法实例化ThreadPoolExecutor,该ThreadPoolExecutor根据需要向池中添加线程:

ExecutorService executor = Executors.newCachedThreadPool();


实例化Executor服务时,会初始化一些参数。根据实例化Executor服务的方式,您可以手动指定这些参数,也可以默认为您提供这些参数。这些参数是:

  • 核心池大小
  • 最大池大小
  • 工作队列
  • Keep AliveTime
  • 线程工厂
  • RejectedExecutionHandler

使用Executors类中可用的工厂方法之一,只需根据您的输入为以上内容选择一些默认值。Executors.newSingleThreadExecutor()创建核心大小为1、最大大小为1、mitAliveTime为0ms(这意味着池中的线程将保持活动状态,除非显式关闭)、无界LinkedBlockingQueue、默认threadFactory和默认rejectedExecutionHandler的池。

同时,Executors.newFixedThreadPool(10)创建核心大小为10、最大大小为10、Keep AliveTime为0MS、无界LinkedBlockingQueue、默认threadFactory和默认rejectedExecutionHandler的池。

那么所有这些参数意味着什么呢?

核心池大小是池中应保留的最小线程数。线程数可能会增长到达到最大池大小(如果它高于核心池大小),但通常,它表示您期望池中有活动的线程数。当任务提交给执行器时,它会检查实际运行的线程数是否小于核心池大小。如果是,则使用指定的threadFactory创建一个新的Worker。

最大池大小是池中可以容纳的最大工作进程数。如果最大池大小大于核心池大小,则意味着池的大小可以增加,即可以向池中添加更多的工作进程。提交任务但工作队列已满时,会将工作人员添加到池中。每次发生这种情况时,都会添加一个新的工作进程,直到达到最大池大小。如果已达到最大池大小并且工作队列已满,则下一个任务将被拒绝。

工作队列用于对可用工作线程的任务进行排队。队列可以是有界的,也可以是无界的。对于有界队列,设置队列大小是一项重要的工作,因为它会影响工作池的增长方式以及何时开始进入RejectedExecutionExceptions。如果您有一个预计会增长的工作池;例如,从20个工作进程的核心池大小增加到最大100个工作进程,那么您可能不希望将队列大小设置为太高的数字,例如10,000,因为这意味着在将每个额外的工作进程添加到池之前,必须将10,000个任务排入队列。容量非常高的无界队列和有界队列更适合与固定大小的池(即核心池大小和最大池大小相同的池)一起使用。

如果线程池增长到最大大小,它如何缩小回核心大小?这就是KeepAliveTime的用武之地。如果当前的工作线程数超过了核心池大小并且设置了KeepAliveTime,那么当没有更多的工作要做时,工作线程将被关闭,直到工作线程数回到核心池大小;线程将等待工作,等待KeepAlive时间,当超过该时间并且没有工作到达时,它将关闭。

  • 附注1:您可以在ThreadPoolExecutor实例上将allowCoreThreadTimeOut设置为true,如果这样做,那么不仅超过核心池大小的工作线程会在空闲时关闭,而且核心工作线程也会在空闲时关闭。默认情况下,它设置为False。

  • 附注2:如果您的工作线程获取和维护昂贵的资源,并且只在关机时释放这些资源,那么优化配置您的KeepAlive时间就变得很重要。0毫秒的Keep Alive时间表示,除非关闭Executor服务本身,否则您的工作进程在创建后永远不会关闭。

大多数情况下,使用默认的ThreadFactory就足够了。默认线程工厂创建具有正常优先级且不是守护程序线程的工作线程。它还为线程命名,格式为:“pool-{poolNumber}-thread-{threadNumber}”。如果您想要自定义这些属性中的任何一个,比如线程名称或优先级,那么您应该提供您自己的threadFactory实现。提供您自己的threadFactory实现的另一个好处是,您可以设置线程的未捕获异常处理程序,这在对抗静默故障方面非常有用。

说到异常,我提到过,对于具有有限工作队列的池,当队列已满并且不能添加更多工作线程时,就会发生任务拒绝。您可以将处理程序配置为在发生此类拒绝时运行。这些处理程序称为“策略”。默认情况下,使用AbortPolicy,它会引发RejectedExecutionException。您可以选择使用另一个策略,例如DiscardPolicy,它只是以静默方式丢弃任务;Celler RunsPolicy,它在调用线程(而不是其中一个工作线程)上执行任务;或者您创建的任何其他策略实现。

总而言之,Java Executor Service隐藏了许多复杂性,但也使您可以轻松地深入研究和调整内部工作(如果您愿意的话)。Executors类提供了很多工厂方法,可以针对不同的用例;`newFixedThreadPool()`用于当您只需要固定数量的线程执行任务时,`newCachedThreadPool()`用于您需要创建新线程时,以及在不需要时缩小池等。在很多情况下,这些预定义的池可以满足您的需求。但是,如果您有更多已定义的参数,那么了解一些可以调整的旋钮以及这将如何影响线程池的行为会很有帮助。