使用 Java 执行器实现线程池

在做一个 JSR 315 - servlet 规范 3.0 的报告时,我意识到理解异步 servlet 的一个关键点在于首先要理解 Java 中的异步处理机制。有因有果,很快我陷入了执行器(Executor)和执行器服务(ExecutorService)之中 - 因为它们是 Java 的异步处理的关键构件。在本博客中我将就这一主题我对掌握到的东西做一个总结。

几个概念

任务:定义为一个小的独立的活动,它表示在某个时间点启动的一系列工作,进行一些活动或者计算,之后结束。在一个 web 服务器中,每个传入的独立请求都满足这一定义。在 Java 中,任务的体现为 Runnable 或者 Callable 的实例。
线程:可以认为它是一个任务的执行实例。如果说任务表示一系列需要完成的工作的话,那么线程就表示该任务实际的执行。在 Java 中,线程体现为 Thread 的实例。
同步处理:发生在当一个任务必须在主线程执行中完成时。换句话讲,主程序必须等待当前任务执行结束之后,才可以继续处理它自己的流程。
异步处理:当主线程将一个任务的处理委托给一个分离的独立线程时。这个线程将会负责该任务的相关处理,而住线程则返回处理主程序接下来要做的事情。
线程池:表示一个或多个等待被分配工作的线程。线程池会给我们带来诸多好处。首先,它减少了线程的创建和销毁所带来的系统开销,因为池中的线程得到了复用,而不是每次从无到重新创建。其次,它能够对系统中活动线程的数量进行控制,这减小了服务器的内存和计算负担。最后,它允许你将线程管理这种棘手的问题委托给线程池,简化了你的程序。
这里,有必要指出起作用的三个重要机制 - 有一些要处理任务的到来(一些请求一系列工作的完成),有任务提交给接受器,然后是每个任务的实际执行。Java 中的 Executor 框架将后两个机制进行了分离 - 提交和处理。
请求的到来通常不在程序的控制范围之内 - 这个取决于来自客户的请求。一个请求的提交通常是这样完成:被要求的任务被添加到进入任务的队列,而处理则实现为分配一个进入的任务给线程池中空闲等待的一个线程去处理。

Java 5.0 和线程池

Java 5.0 引入了自己的线程池实现 - Executor 和 ExecutorService 接口。这让你在自己的程序中使用线程池变得更加容易。
Executor 给应用程序对于任务的考虑提供了一个便利的抽象。不需要从线程的方面进行考虑,应用现在只需简单地处理 Runnable 的实例,然后将其传给一个 Executor 去处理。
ExecutorService 接口继承了非常简化的 Executor 接口,它添加了一些生命周期方法来管理线程池中的线程。比如,你可以关掉池中所有的线程。
另外,Executor 允许你提交一个简单的任务给池中的某个线程执行,ExecutorService 还能允许你提交一个任务的集合,或者获得一个 Futrure 对象以跟踪该任务的执行情况。

Runnable 和 Callable

Executor 框架代表了使用 Runnable 或者 Callable 实例的一系列任务。Runnable 的 run() 方法限制是它既没有返回值,也不会抛 checked 异常。Callable 是一个加强版,定义了一个 call() 方法以允许一些计算值的返回,甚至能够抛出一个异常,如果需要的话。

控制你的任务

你可以通过使用 FutureTask 类获取任务的详细信息,它能够对 Callable 或 Runnable 的实例进行包装。你可以通过调用一个 ExecutorService 的 submit() 方法的返回值获得一个 FutureTask 实例,或者你也可以在调用 execute() 方法之前手工将你的任务包装到一个 FutureTask。
FutureTask 的实例,因为其实现了 Future 接口,通过它你能够监控一个执行中的任务、取消该任务、获取其执行结果(就像 Callable 的 call() 方法有返回值那样)。

ThreadPoolExecutor

最常见的 ExecutorService 实现是 ThreadPoolExecutor。
任务作为一个 Runnable 的实例提交给 ThreadPoolExecutor,后者负责实际处理,而你的应用则无须关心在这个抽象的背后到底发生了什么事情。
ThreadPoolExecutor 的定义如下:
  1. 一个线程池(定义了最多线程和最少线程的数量);
  2. 一个工作队列:这个队列持有提交的任务,这些任务将被依次分配给线程池中的某个线程。主要有两种类型的队列 - 有界和无界的。给一个有界队列添加任务永远是成功添加的,但是有界队列(比如一个固定容量的 LinkedBlockingQueue)在挂起的任务达到其最大容量的时候会拒绝新任务添加。
  3. 一个定义了如何处理被拒绝任务的处理器(饱和策略):当一个任务无法被添加到队列的时候,线程池将会调用其注册的拒绝处理器来决定将会发生什么事情。默认的拒绝策略是简单地抛出一个 RejectedExecutionException 运行时异常,并由程序捕捉该异常并进行处理。还有其他的一些个策略,比如 DiscardPolicy,它会默默地丢弃任务而没有任何通知。
  4. 一个线程工厂:默认情况下,ThreadPoolExecutor 执行器构造的新线程将会具有特定属性 - 比如线程优先级,以及一个根据线程池数量、线程池中线程数来决定的线程名。你可以使用一个自定义工厂来重写这些默认值。

使用执行器的算法

1. 创建一个执行器

你首先要在一个全局环境下创建一个 Executor 或 ExecutorService 的实例(比如一个 servlet 容器下的应用的上下文)。
Executors 类提供了很多创建一个 ExecutorService 的静态工厂方法。比如,newFixedThreadPool() 返回一个具有无界队列和固定线程数的 ThreadPoolExecutor 实例;newCachedThreadPool() 方法返回一个具有无界队列和无界线程数的 ThreadPoolExecutor 实例。对于后者,如果有空闲线程的话,该线程会被复用;如果没有空闲线程,会新建一个线程并将其添加到线程池。超过固定时间始终闲置的线程将会被移出线程池。
private static final Executor executor = Executors.newFixedThreadPool(10);

如果不使用这些便利的方法,你也许会发现使用自己定义的 ThreadPoolExecutor 更合适 - 使用它的众多构造子。
private static final Executor executor = new ThreadPoolExecutor(10, 10, 50000L,   TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(100));

这个构造子将创建一个大小 100 的有界队列、线程池固定大小为 10 的 ThreadPoolExecutor。

2. 创建一个或多个任务

你需要创建一个或多个由 Runnable 或者 Callable 实例执行的任务。

3. 提交任务到执行器

一旦你有了一个 ExecutorService,你就可以通过使用 submit() 或者 execute() 方法将任务提交给它了,之后来自线程池中的一个空闲线程将该任务出列并执行之。

4. 执行任务

执行器除了管理线程池和队列之外还会负责管理任务的执行。这里具体会发生什么取决于线程池的大小限制、空闲线程的数量以及队列的边界。
通常情况下,如果线程池具备线程的数量小于其定义的最小线程数,新的线程会被创建以处理队列中的任务,直到达到该数目限制。
如果池中的线程数超过了配置的最小线程数,线程池将会不再创建更多线程,该任务将会被放入任务队列,直到一个线程空闲出来去处理它。如果队列满了的话,就必须得启动一个新的线程去处理这个新任务了。
如果池中的线程数到达了配置的最大线程数,线程池将不再启动新的线程,于是新提交的任务要么被添加到任务队列,要么因为该队列已满而被拒绝。
线程池中的线程会持续监控该队列以获取任务去执行。等待了超过配置的最大空闲时间的线程将会被终止。

5. 执行器的关闭

程序关闭期间,我们通过调用执行器的 shutdown() 方法将其关闭。你可以选择是暴利关闭还是优雅关闭。

参考资料

原文链接:http://www.softwareengineeringsolutions.com/blogs/2010/07/21/implementing-thread-pools-using-java-executors/
展开阅读全文

没有更多推荐了,返回首页