定制 ThreadPoolExecutor |
Executors
中的 newFixedThreadPool
和 newCachedThreadPool
工厂方法返回的 Executor
是类 ThreadPoolExecutor
的实例,是高度可定制的。
通过使用包含 ThreadFactory
变量的工厂方法或构造函数的版本,可以定义池线程的创建。ThreadFactory
是工厂对象,其构造执行程序要使用的新线程。使用定制的线程工厂,创建的线程可以包含有用的线程名称,并且这些线程是守护线程,属于特定线程组或具有特定 优先级。
下面是线程工厂的例子,它创建守护线程,而不是创建用户线程:
public class DaemonThreadFactory implements ThreadFactory {
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setDaemon(true);
return thread;
}
}
有时,Executor
不能执行任务,因为它已经关闭或者因为 Executor
使用受限制队列存储等待任务,而该队列已满。在这种情况下,需要咨询执行程序的 RejectedExecutionHandler
来确定如何处理任务 —— 抛出异常(默认情况),放弃任务,在调用者的线程中执行任务,或放弃队列中最早的任务以为新任务腾出空间。ThreadPoolExecutor.setRejectedExecutionHandler
可以设置拒绝的执行处理程序。
还可以扩展 ThreadPoolExecutor
,并覆盖方法 beforeExecute
和 afterExecute
,以添加装置,添加记录,添加计时,重新初始化线程本地变量,或进行其他执行定制。
需要特别考虑的问题 |
使用 Executor
框架会从执行策略中删除任务提交,一般情况下,人们希望这样,那是因为它允许我们灵活地调整执行策略,不必更改许多位置的代码。然而,当提交代码暗含假设 特定执行策略时,存在多种情况,在这些情况下,重要的是选择的 Executor 实现一致的执行策略。
这类情况中的其中的一种就是一些任务同时等待其他任务完成。在这种情况下,当线程池没有足够的线程时,如果所有当前执行的任务都在等待另一项任务,而该任 务因为线程池已满不能执行,那么线程池可能会死锁。
另一种相似的情况是一组线程必须作为共同操作组一起工作。在这种情况下,需要确保线程池能够容纳所有线程。
如果应用程序对特定执行程序进行了特定假设,那么应该在 Executor
定义和初始化的附近对这些进行说明,从而使善意的更改不会破坏应用程序的正确功能。
调整线程池 |
创建 Executor
时,人们普遍会问的一个问题是“线程池应该有多大?”。当然,答案取决于硬件和将执行的任务类型(它们是受计算限制或是受 IO 的限制?)。
如果线程池太小,资源可能不能被充分利用,在一些任务还在工作队列中等待执行时,可能会有处理器处于闲置状态。
另一方面,如果线程池太大,则将有许多有效线程,因为大量线程或有效任务使用内存,或者因为每项任务要比使用少量线程有更多上下文切换,性能可能会受损。
所以假设为了使处理器得到充分使用,线程池应该有多大?如果知道系统有多少处理器和任务的计算时间和等待时间的近似比率,Amdahl 法则提供很好的近似公式。
用 WT 表示每项任务的平均等待时间,ST 表示每项任务的平均服务时间(计算时间)。则 WT/ST 是每项任务等待所用时间的百分比。对于 N 处理器系统,池中可以近似有 N*(1+WT/ST) 个线程。
好的消息是您不必精确估计 WT/ST。“合适的”池大小的范围相当大;只需要避免“过大”和“过小”的极端情况即可。
Future 接口 |
Future
接口允许表示已经完成的任务、正在执行过程中的任务或者尚未开始执行的任务。通过 Future
接口,可以尝试取消尚未完成的任务,查询任务已经完成还是取消了,以及提取(或等待)任务的结果值。
FutureTask
类实现了 Future
,并包含一些构造函数,允许将 Runnable
或 Callable
(会产生结果的 Runnable
)和 Future
接口封装。因为 FutureTask
也实现 Runnable
,所以可以只将 FutureTask
提供给 Executor
。一些提交方法(如ExecutorService.submit()
) 除了提交任务之外,还将返回 Future
接口。
Future.get()
方法检索任务计算的结果(或如果任务完成,但有异常,则抛出 ExecutionException
)。 如果任务尚未完成,那么Future.get()
将被阻塞,直到任务完成;如果任务已经完成,那么它将立即返回结果。
使用 Future 构建缓存 |
该示例代码与 java.util.concurrent
中的多个类关联,突出显示了 Future
的功能。它实现缓存,使用 Future
描述缓存值,该值可能已经计算,或者可能在其他线程中“正在构造”。
它利用 ConcurrentHashMap
中的原子 putIfAbsent()
方法,确保仅有一个线程试图计算给定关键字的值。如果其他线程随后请求同一关键字的值,它仅能等待(通过 Future.get()
的帮助)第一个线程完成。因此两个线程不会计算相同的值。
public class Cache<K, V> {
ConcurrentMap<K, FutureTask<V>> map = new ConcurrentHashMap();
Executor executor = Executors.newFixedThreadPool(8);
public V get(final K key) {
FutureTask<V> f = map.get(key);
if (f == null) {
Callable<V> c = new Callable<V>() {
public V call() {
// return value associated with key
}
};
f = new FutureTask<V>(c);
FutureTask old = map.putIfAbsent(key, f);
if (old == null)
executor.execute(f);
else
f = old;
}
return f.get();
}
}
CompletionService
CompletionService
将执行服务与类似 Queue
的接口组合,从任务执行中删除任务结果的处理。CompletionService
接口包含用来提交将要执行的任务的 submit()
方法和用来询问下一完成任务的 take()
/poll()
方法。
CompletionService
允许应用程序结构化,使用 Producer/Consumer
模式,其中生产者创建任务并提交,消费者请求完成任务的结果并处理这些结果。CompletionService
接口由 ExecutorCompletionService
类实现,该类使用 Executor
处理任务并从 CompletionService
导出
submit/poll/take 方法。
下列代码使用 Executor
和 CompletionService
来启动许多“solver”任务,并使用第一个生成非空结果的任务的结果,然后取消其余任务:
void solve(Executor e, Collection<Callable<Result>> solvers)
throws InterruptedException {
CompletionService<Result> ecs = new ExecutorCompletionService<Result>(e);
int n = solvers.size();
List<Future<Result>> futures = new ArrayList<Future<Result>>(n);
Result result = null;
try {
for (Callable<Result> s : solvers)
futures.add(ecs.submit(s));
for (int i = 0; i < n; ++i) {
try {
Result r = ecs.take().get();
if (r != null) {
result = r;
break;
}
} catch(ExecutionException ignore) {}
}
}
finally {
for (Future<Result> f : futures)
f.cancel(true);
}
if (result != null)
use(result);
}