在任务与执行策略之间的隐性耦合
Executor框架可以将任务的提交与任务的执行策略解耦开来 将像许多对复杂过程的解耦操作那样 这种论断多少有些言过其实了 虽然Executor框架为制定和修改执行策略都提供了相当大的灵活性 但并非所有的任务都能适用所有的执行策略 有些类型的任务需要明确地指定执行策略 包括:
- 依赖性任务
- 使用线程封闭机制的任务
- 对响应时间敏感的任务
- 使用ThreadLocal的任务
只有当任务都是同类型的并且相互独立时 线程池的性能才能达到最佳 如果将运行时间较长的与运行时间较短的任务混合在一起 那么除非线程池很大 否则将可能造成 拥塞 如果提交的任务依赖于其他任务 那么除非线程池无限大 否则将可能造成死锁
在一些任务中 需要拥有或排除某种特定的执行策略 如果某些任务依赖于其他的任务 那么会要求线程池足够大 从而确保它们依赖任务不会被放入等待队列中或被拒绝 而采用线程封闭机制的任务需要串行执行 通过将这些需求写入文档 将来的代码维护人员就不会由于使用了某种不合适的执行策略而破坏安全性或活跃性
线程饥饿死锁
只要线程池中的任务需要无限期地等待一些必须由池中其他任务才能提供的资源或条件 例如某个任务等待另一个任务的返回值或执行结果 那么除非线程池足够大 否则将发生线程饥饿死锁
在单线程Executor中任务发生死锁(不要这么做)
public class ThreadDeadlock {
ExecutorService exec = Executors.newSingleThreadExecutor();
public class LoadFileTask implements Callable<String> {
private final String fileName;
public LoadFileTask(String fileName) {
this.fileName = fileName;
}
public String call() throws Exception {
// Here's where we would actually read the file
return "";
}
}
public class RenderPageTask implements Callable<String> {
public String call() throws Exception {
Future<String> header, footer;
header = exec.submit(new LoadFileTask("header.html"));
footer = exec.submit(new LoadFileTask("footer.html"));
String page = renderBody();
// Will deadlock -- task waiting for result of subtask
return header.get() + page + footer.get();
}
private String renderBody() {
// Here's where we would actually render the page
return "";
}
}
}
每当提交了一个有依赖性的Executor任务时 要清楚地知道可能会出现线程 饥饿 死锁 因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制
运行时间较长的任务
如果任务阻塞的时间过长 那么即使不出现死锁 线程池的响应性也会变得糟糕 有一项技术可以缓解执行时间较长任务造成的影响 即限定任务等待资源的时间 而不要无限制地等待
设置线程池的大小
线程池的理想大小取决于被提交任务的类型以及所部署系统的特性 在代码中通常不会固定线程池的大小 而应该通过某种配置机制来提供 或者根据Runtime.availableProcessors来动态计算
配置ThreadPoolExecutor
ThreadPoolExecutor为一些Executor提供了基本的实现 这些Executor是由Executors中的newCachedThreadPool newFixedThreadPool和newScheduledThreadExecutor等工厂方法返回的 ThreadPoolExecutor是一个灵活的 稳定的线程池 允许进行各种定制
ThreadPoolExecutor的通用构造函数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) { ... }
线程的创建与销毁
线程池的基本大小(Core Pool Size) 最大大小(Maximum Pool Size)以及存活时间等因素共同负责线程的创建与销毁 基本大小也就是线程池的目标大小 即在没有任务执行时线程池的大小 并且只有在工作队列满了的情况下才会创建超出这个数量的线程 线程池的最大大小表示可同时活动的线程数量的上限 如果某个线程的空闲时间超过了存活时间 那么将被标记为可回收的 并且当线程池的当前大小超过了基本大小时 这个线程将被终止
管理队列任务
在有限的线程池中会限制可并发执行的任务数量
如果无限制地创建线程 那么将导致不稳定性 并通过采用固定大小的线程池(而不是每收到一个请求就创建一个新线程)来解决这个问题 然而 这个方案并不完整 在高负载情况下 应用程序仍可能耗尽资源 只是出现问题的概率较小
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务 基本的任务排队方法有3种:无界队列 有界队列和同步移交(Synchronous Handoff) 队列的选择与其他的配置参数有关 例如线程池的大小等
newFixedThreadPool和newSingleThreadExecutor在默认情况下将使用一个无界的LinkedBlockingQueue 如果所有工作者线程都处于忙碌状态 那么任务将在队列中等候 如果任务持续快速地到达 并且超过了线程池处理它们的速度 那么队列将无限制地增加
一种更稳妥的资源管理策略是使用有界队列 例如ArrayBlockingQueue 有界的LinkedBlockingQueue PriorityBlockingQueue 有界队列有助于避免资源耗尽的情况发生 但它又带来了新的问题:当队列填满后 新的任务该怎么办?在使用有界的工作队列时 队列的大小与线程池的大小必须一起调节 如果线程池较小而队列较大 那么有助于减少内存使用量 降低CPU的使用率 同时还可以减少上下文切换 但付出的代价是可能会限制吞吐量
对于非常大的或者无界的线程池 可以通过使用SynchronousQueue来避免任务排队 以及直接将任务从生产者移交给工作者线程 SynchronousQueue不是一个真正的队列 而是一种在线程之间进行移交的机制 要将一个元素放入SynchronousQueue中 必须有另一个线程正在等待接受这个元素 如果没有线程正在等待 并且线程池的当前大小小于最大值 那么ThreadPoolExecutor将创建一个新的线程 否则根据饱和策略 这个任务将被拒绝 使用直接移交将更高效 因为任务会直接移交给执行它的线程 而不是被首先放在队列中 然后由工作者线程从队列中提取该任务 只有当线程池是无界的或者可以拒绝任务时 SynchronousQueue才有实际价值 在newCachedThreadPool工厂方法中就使用了SynchronousQueue
当使用像LinkedBlockingQueue或ArrayBlockingQueue这样的FIFO(先进先出)队列时 任务的执行顺序与它们的到达顺序相同 如果想进一步控制任务执行顺序 还可以使用PriorityBlockingQueue 这个队列将根据优先级来安排任务 任务的优先级是通过自然顺序或Comparator(如果任务实现了Comparable)来定义的
对于Executor newCachedThreadPool工厂方法是一种很好的默认选择 它能提供比固定大小的线程池更好的排队性能 当需要限制当前任务的数量以满足资源管理需求时 那么可以选择固定大小的线程池 就像在接受网络客户请求的服务器应用程序中 如果不进行限制 那么很容易发生过载问题
只有当任务相互独立时 为线程池或工作队列设置界限才是合理的 如果任务之间存在依赖性 那么有界的线程池或队列就可能导致线程 饥饿 死锁问题 此时应该使用无界的线程池 例如newCachedThreadPool
饱和策略
当有界队列被填满后 饱和策略开始发挥作用 ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改(如果某个任务被提交到一个已被关闭的Executor时 也会用到饱和策略) JDK提供了几种不同的RejectedExecutionHandler实现 每种实现都包含有不同的饱和策略:AbortPolicy CallerRunsPolicy DiscardPolicy和DiscardOldestPolicy
中止(Abort) 策略是默认的饱和策略 该策略将抛出未检查的RejectedExecutionExecution 调用者可以捕获这个异常 然后根据需求编写自己的处理代码 当新提交的任务无法保存到队列中等待执行时 抛弃(Discard) 策略会悄悄抛弃该任务 抛弃最旧的(Discard-Oldest) 策略则会抛弃下一个将被执行的任务 然后尝试重新提交新的任务 (如果工作队列是一个优先队列 那么 抛弃最旧的 策略将导致抛弃优先级最高的任务 因此最好不要将 抛弃最旧的 饱和策略和优先级队列放在一起使用)
调用者运行(Caller-Runs) 策略实现了一种调节机制 该策略既不会抛弃任务 也不会抛出异常 而是将某些任务回退到调用者 从而降低新任务的流量 它不会在线程池的某个线程中执行新提交的任务 而是在一个调用了execute的线程中执行该任务
当创建Executor时 可以选择饱和策略或者对执行策略进行修改
创建一个固定大小的线程池 并采用有界队列以及 调用者运行 饱和策略
ThreadPoolExecutor executor
= new ThreadPoolExecutor(N_THREADS, N_THREADS,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(CAPACITY)) ;
executor.setRejectedExecutionHandler(
new ThreadPoolExecutor.CallerRunsPolicy());
当工作队列被填满后 没有预定义的饱和策略来阻塞execute 然而 通过使用Semaphore(信号量)来限制任务的到达率 就可以实现这个功能
使用Semaphore来控制任务的提交速率
@ThreadSafe
public class BoundedExecutor {
private final Executor exec;
private final Semaphore semaphore;
public BoundedExecutor(Executor exec, int bound) {
this.exec = exec;
this.semaphore = new Semaphore(bound);
}
public void submitTask(final Runnable command)
throws InterruptedException {
semaphore.acquire();
try {
exec.execute(new Runnable() {
public void run() {
try {
command.run();
} finally {
semaphore.release();
}
}
});
} catch (RejectedExecutionException e) {
semaphore.release();
}
}
}
线程工厂
每当线程池需要创建一个线程时 都是通过线程工厂方法来完成的 默认的线程工厂方法将创建一个新的 非守护的线程 并且不包含特殊的配置信息 通过指定一个线程工厂方法 可以定制线程池的配置信息 在ThreadFactory中只定义了一个方法newThread 每当线程池需要创建一个新线程时都会调用这个方法
ThreadFactory接口
public interface ThreadFactory {
Thread newThread(Runnable r);
}
然而 在许多情况下都需要使用定制的线程工厂方法 例如 你希望为线程池中的线程指定一个UncaughtExceptionHandler 或者实例化一个定制的Thread类用于执行调试信息的记录 你还可能希望修改线程的优先级或者守护状态 或许你只是希望给线程取一个更有意义的名称 用来解释线程的转储信息和错误日志
自定义的线程工厂
public class MyThreadFactory implements ThreadFactory {
private final String poolName;
public MyThreadFactory(String poolName) {
this.poolName = poolName;
}
public Thread newThread(Runnable runnable) {
return new MyAppThread(runnable, poolName);
}
}
在MyAppThread中还可以定制其他行为
定制Thread基类
public class MyAppThread extends Thread {
public static final String DEFAULT_NAME = "MyAppThread";
private static volatile boolean debugLifecycle = false;
private static final AtomicInteger created = new AtomicInteger();
private static final AtomicInteger alive = new AtomicInteger();
private static final Logger log = Logger.getAnonymousLogger();
public MyAppThread(Runnable r) {
this(r, DEFAULT_NAME);
}
public MyAppThread(Runnable runnable, String name) {
super(runnable, name + "-" + created.incrementAndGet());
setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
public void uncaughtException(Thread t,
Throwable e) {
log.log(Level.SEVERE,
"UNCAUGHT in thread " + t.getName(), e);
}
});
}
public void run() {
// Copy debug flag to ensure consistent value throughout.
boolean debug = debugLifecycle;
if (debug) log.log(Level.FINE, "Created " + getName());
try {
alive.incrementAndGet();
super.run();
} finally {
alive.decrementAndGet();
if (debug) log.log(Level.FINE, "Exiting " + getName());
}
}
public static int getThreadsCreated() {
return created.get();
}
public static int getThreadsAlive() {
return alive.get();
}
public static boolean getDebug() {
return debugLifecycle;
}
public static void setDebug(boolean b) {
debugLifecycle = b;
}
}
如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限 那么可以通过Executor中的privilegedThreadFactory工厂来定制自己的线程工厂 通过这种方式创建出来的线程 将与创建privilegedThreadFactory的线程拥有相同的访问权限 AccessControllerContext和contextClassLoader 如果不使用privilegedThreadFactory 线程池创建的线程将从在需要新线程时调用execute或submit的客户程序中继承访问权限 从而导致令人困惑的安全性异常
在调用构造函数后再定制ThreadPoolExecutor
在调用完ThreadPoolExecutor的构造函数后 仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数(例如线程池的基本大小 最大大小 存活时间 线程工厂以及拒绝执行处理器(Rejected Execution Handler)) 如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的 那么可以将结果的类型转换为ThreadPoolExecutor以访问设置器
对通过标准工厂方法创建的Executor进行修改
ExecutorService exec = Executors.newCachedThreadPoolExecutor();
if (exec instanceof ThreadPoolExecutor)
((ThreadPoolExecutor) exec).setCorePoolSize(10);
else
throw new AssertionError("Oops, bad assumption");
扩展ThreadPoolExecutor
ThreadPoolExecutor是可扩展的 它提供了几个可以在子类化中改写的方法:beforeExecute afterExecute和terminated 这些方法可以用于扩展ThreadPoolExecutor的行为
在执行任务的线程中将调用beforeExecute和afterExecute等方法 在这些方法中还可以添加日志 计时 监视或统计信息收集的功能 无论任务是从run中正常返回 还是抛出一个异常而返回 afterExecute都会被调用 (如果任务在完成后带有一个Error 那么就不会调用afterExecute) 如果beforeExecute抛出一个RuntimeExecution 那么任务将不被执行 并且afterExecute也不会被调用
在线程池完成关闭操作时调用terminated 也就是在所有任务都已经完成并且所有工作者线程也已经关闭后 terminated可以用来释放Executor在其生命周期里分配的各种资源 此外还可以执行发送通知 记录日志或者收集finalize统计信息等操作
示例:给线程池添加统计信息
public class TimingThreadPool extends ThreadPoolExecutor {
public TimingThreadPool() {
super(1, 1, 0L, TimeUnit.SECONDS, null);
}
private final ThreadLocal<Long> startTime = new ThreadLocal<Long>();
private final Logger log = Logger.getLogger("TimingThreadPool");
private final AtomicLong numTasks = new AtomicLong();
private final AtomicLong totalTime = new AtomicLong();
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
log.fine(String.format("Thread %s: start %s", t, r));
startTime.set(System.nanoTime());
}
protected void afterExecute(Runnable r, Throwable t) {
try {
long endTime = System.nanoTime();
long taskTime = endTime - startTime.get();
numTasks.incrementAndGet();
totalTime.addAndGet(taskTime);
log.fine(String.format("Thread %s: end %s, time=%dns",
t, r, taskTime));
} finally {
super.afterExecute(r, t);
}
}
protected void terminated() {
try {
log.info(String.format("Terminated: avg time=%dns",
totalTime.get() / numTasks.get()));
} finally {
super.terminated();
}
}
}
递归算法的并行化
如果循环中的迭代操作都是独立的 并且不需要等待所有的迭代操作都完成再继续执行 那么就可以使用Executor将串行循环转化为并行循环
将串行执行转换为并行执行
public abstract class TransformingSequential {
void processSequentially(List<Element> elements) {
for (Element e : elements)
process(e);
}
void processInParallel(Executor exec, List<Element> elements) {
for (final Element e : elements)
exec.execute(new Runnable() {
public void run() {
process(e);
}
});
}
public abstract void process(Element e);
interface Element {
}
}
当串行循环中的各个迭代操作之间彼此独立 并且每个迭代操作执行的工作量比管理一个新任务时带来的开销更多 那么这个串行循环就适合并行化
在一些递归设计中同样可以采用循环并行化的方法 在递归算法中通常都会存在串行循环 一种简单的情况是:在每个迭代操作中都不需要来自于后续递归迭代的结果
将串行递归转换为并行递归
public <T> void sequentialRecursive(List<Node<T>> nodes,
Collection<T> results) {
for (Node<T> n : nodes) {
results.add(n.compute());
sequentialRecursive(n.getChildren(), results);
}
}
public <T> void parallelRecursive(final Executor exec,
List<Node<T>> nodes,
final Collection<T> results) {
for (final Node<T> n : nodes) {
exec.execute(new Runnable() {
public void run() {
results.add(n.compute());
}
});
parallelRecursive(exec, n.getChildren(), results);
}
}
interface Node <T> {
T compute();
List<Node<T>> getChildren();
}
当parallelRecursive返回时 树中的各个节点都已经访问过了(但是遍历过程仍然是串行的 只有compute调用才是并行执行的) 并且每个节点的计算任务也已经放入Executor的工作队列 parallelRecursive的调用者可以通过以下方式等待所有的结果:创建一个特定于遍历过程的Executor 并使用shutdown和awaitTermination等方法
等待通过并行方式计算的结果
public <T> Collection<T> getParallelResults(List<Node<T>> nodes)
throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
Queue<T> resultQueue = new ConcurrentLinkedQueue<T>();
parallelRecursive(exec, nodes, resultQueue);
exec.shutdown();
exec.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
return resultQueue;
}
示例:谜题框架
我们将 谜题 定义为:包含了一个初始位置 一个目标位置 以及用于判断是否是有效移动的规则集 规则集包含两部分:计算从指定位置开始的所有合法移动 以及每次移动的结果位置
表示 搬箱子 之类谜题的抽象类
public interface Puzzle <P, M> {
P initialPosition();
boolean isGoal(P position);
Set<M> legalMoves(P position);
P move(P position, M move);
}
其中的类型参数P和M表示位置类和移动类 根据这个接口 我们可以写一个简单的串行求解程序 该程序将在谜题空间(PuzzleSpace)中查找 直到找到一个解答或者找遍了整个空间都没有发现答案
用于谜题解决框架的链表节点
@Immutable
public class PuzzleNode <P, M> {
final P pos;
final M move;
final PuzzleNode<P, M> prev;
public PuzzleNode(P pos, M move, PuzzleNode<P, M> prev) {
this.pos = pos;
this.move = move;
this.prev = prev;
}
List<M> asMoveList() {
List<M> solution = new LinkedList<M>();
for (PuzzleNode<P, M> n = this; n.move != null; n = n.prev)
solution.add(0, n.move);
return solution;
}
}
Node代表通过一系列的移动到达的一个位置 其中保存了到达该位置的移动以及前一个Node 只要沿着Node链接逐步回溯 就可以重新构建出到达当前位置的移动序列
串行的谜题解答器
public class SequentialPuzzleSolver <P, M> {
private final Puzzle<P, M> puzzle;
private final Set<P> seen = new HashSet<P>();
public SequentialPuzzleSolver(Puzzle<P, M> puzzle) {
this.puzzle = puzzle;
}
public List<M> solve() {
P pos = puzzle.initialPosition();
return search(new PuzzleNode<P, M>(pos, null, null));
}
private List<M> search(PuzzleNode<P, M> node) {
if (!seen.contains(node.pos)) {
seen.add(node.pos);
if (puzzle.isGoal(node.pos))
return node.asMoveList();
for (M move : puzzle.legalMoves(node.pos)) {
P pos = puzzle.move(node.pos, move);
PuzzleNode<P, M> child = new PuzzleNode<P, M>(pos, move, node);
List<M> result = search(child);
if (result != null)
return result;
}
}
return null;
}
}
SequentialPuzzleSolver中给出了谜题框架的串行解决方案 它在谜题空间中执行一个深度优先搜索 当找到解答方案(不一定是最短的解决方案)后结束搜索
通过修改解决方案以利用并发性 可以以并行方式来计算下一步移动以及目标条件 因为计算某次移动的过程在很大程度上与计算其他移动的过程是相互独立的(之所以说 在很大程度上 是因为在各个任务之间会共享一些可变状态 例如已遍历位置的集合) 如果有多个处理器可用 那么这将减少寻找解决方案所花费的时间
并发的谜题解答器
public class ConcurrentPuzzleSolver <P, M> {
private final Puzzle<P, M> puzzle;
private final ExecutorService exec;
private final ConcurrentMap<P, Boolean> seen;
protected final ValueLatch<PuzzleNode<P, M>> solution = new ValueLatch<PuzzleNode<P, M>>();
public ConcurrentPuzzleSolver(Puzzle<P, M> puzzle) {
this.puzzle = puzzle;
this.exec = initThreadPool();
this.seen = new ConcurrentHashMap<P, Boolean>();
if (exec instanceof ThreadPoolExecutor) {
ThreadPoolExecutor tpe = (ThreadPoolExecutor) exec;
tpe.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy());
}
}
private ExecutorService initThreadPool() {
return Executors.newCachedThreadPool();
}
public List<M> solve() throws InterruptedException {
try {
P p = puzzle.initialPosition();
exec.execute(newTask(p, null, null));
// block until solution found
PuzzleNode<P, M> solnPuzzleNode = solution.getValue();
return (solnPuzzleNode == null) ? null : solnPuzzleNode.asMoveList();
} finally {
exec.shutdown();
}
}
protected Runnable newTask(P p, M m, PuzzleNode<P, M> n) {
return new SolverTask(p, m, n);
}
protected class SolverTask extends PuzzleNode<P, M> implements Runnable {
SolverTask(P pos, M move, PuzzleNode<P, M> prev) {
super(pos, move, prev);
}
public void run() {
if (solution.isSet()
|| seen.putIfAbsent(pos, true) != null)
return; // already solved or seen this position
if (puzzle.isGoal(pos))
solution.setValue(this);
else
for (M m : puzzle.legalMoves(pos))
exec.execute(newTask(puzzle.move(pos, m), m, this));
}
}
}
ConcurrentPuzzleSolver中使用了一个内部类SolverTask 这个类扩展了Node并实现了Runnable 大多数工作都是在run方法中完成的:首先计算出下一步可能到达的所有位置 并去掉已经到达的位置 然后判断(这个任务或者其他某个任务)是否已经成功地完成 最后将尚未搜索过的位置提交给Executor
为了避免无限循环 在串行版本中引入了一个Set对象 其中保存了之前已经搜索过的所有位置 在ConcurrentPuzzleSolver中使用ConcurrentHashMap来实现相同的功能 这种做法不仅提供了线程安全性 还避免了在更新共享集合时存在的竞态条件 因为putIfAbsent只有在之前没有遍历过的某个位置才会通过原子方法添加到集合中 ConcurrentPuzzleSolver使用线程池的内部工作队列而不是调用栈来保存搜索的状态
这种并发方法引入了一种新形式的限制并去掉了一种原有的限制 新的限制在这个问题域中更合适 串行版本的程序执行深度优先搜索 因此搜索过程将受限于栈的大小 并发版本的程序执行广度优先搜索 因此不会受到栈大小的限制(但如果待搜索的或者已搜索的位置集合大小超过了可用的内存总量 那么仍可能耗尽内存)
为了在找到某个解答后停止搜索 需要通过某种方式来检查是否有线程已经找到了一个解答 如果需要第一个找到的解答 那么还需要在其他任务都没有找到解答时更新解答 这些需求描述的是一种闭锁(Latch)机制 具体地说 是一种包含结果的闭锁
由ConcurrentPuzzleSolver使用的携带结果的闭锁
@ThreadSafe
public class ValueLatch <T> {
@GuardedBy("this") private T value = null;
private final CountDownLatch done = new CountDownLatch(1);
public boolean isSet() {
return (done.getCount() == 0);
}
public synchronized void setValue(T newValue) {
if (!isSet()) {
value = newValue;
done.countDown();
}
}
public T getValue() throws InterruptedException {
done.await();
synchronized (this) {
return value;
}
}
}
ValueLatch中使用CountDownLatch来实现所需的闭锁行为 并且使用锁定机制来确保解答只会被设置一次
每个任务首先查询solution闭锁 找到一个解答就停止 而在此之前 主线程需要等待 ValueLatch中的getValue将一直阻塞 直到有线程设置了这个值 ValueLatch提供了一种方式来保存这个值 只有第一次调用才会设置它 调用者能够判断这个值是否已经被设置 以及阻塞并等候它被设置 在第一次调用setValue时 将更新解答方案 并且CountDownLatch会递减 从getValue中释放主线程
第一个找到解答的线程还会关闭Executor 从而阻止接受新的任务 要避免处理RejectedExecutionException 需要将拒绝执行处理器设置为 抛弃已提交的任务 然后 所有未完成的任务最终将执行完成 并且在执行任何新任务时都会失败 从而使Executor结束(如果任务运行的时间过长 那么可以中断它们而不是等它们完成)
如果不存在解答 那么ConcurrentPuzzleSolver就不能很好地处理这种情况:如果已经遍历了所有的移动和位置都没有找到解答 那么在getSolution调用中将永远等待下去 当遍历了整个搜索空间时 串行版本的程序将结束 但要结束并发程序会更困难 其中一种方法是:记录活动任务的数量 当该值为零时将解答设置为null
在解决器中找不到解答
public class PuzzleSolver <P,M> extends ConcurrentPuzzleSolver<P, M> {
PuzzleSolver(Puzzle<P, M> puzzle) {
super(puzzle);
}
private final AtomicInteger taskCount = new AtomicInteger(0);
protected Runnable newTask(P p, M m, PuzzleNode<P, M> n) {
return new CountingSolverTask(p, m, n);
}
class CountingSolverTask extends SolverTask {
CountingSolverTask(P pos, M move, PuzzleNode<P, M> prev) {
super(pos, move, prev);
taskCount.incrementAndGet();
}
public void run() {
try {
super.run();
} finally {
if (taskCount.decrementAndGet() == 0)
solution.setValue(null);
}
}
}
}
找到解答的时间可能比等待的时间要长 因此在解决器中需要包含几个结束条件 其中一个结束条件是时间限制 这很容易实现:在ValueLatch中实现一个限时的getValue(其中将使用限时版本的await) 如果getValue超时 那么关闭Executor并声明出现了一个失败 另一个结束条件是某种特定于谜题的标准 例如只搜索特定数量的位置 此外 还可以提供一种取消机制 由用户自己决定何时停止搜索
小结
对于并发执行的任务 Executor框架是一种强大且灵活的框架 它提供了大量可调节的选项 例如创建线程和关闭线程的策略 处理队列任务的策略 处理过多任务的策略 并且提供了几个钩子方法来扩展它的行为 然而 与大多数功能强大的框架一样 其中有些设置参数并不能很好地工作 某些类型的任务需要特定的执行策略 而一些参数组合则可能产生奇怪的结果